/* (C) 2022 YiRing, Inc. */
package com.yiring.app.service.upload;

import cn.hutool.core.io.FileUtil;
import com.yiring.common.core.Minio;
import com.yiring.common.service.UploadProcessService;
import com.yiring.common.util.Commons;
import io.minio.ObjectWriteResponse;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import net.bramp.ffmpeg.FFmpeg;
import net.bramp.ffmpeg.FFmpegExecutor;
import net.bramp.ffmpeg.FFprobe;
import net.bramp.ffmpeg.builder.FFmpegBuilder;
import net.bramp.ffmpeg.probe.FFmpegFormat;
import net.bramp.ffmpeg.probe.FFmpegProbeResult;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.springframework.context.annotation.Primary;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

/**
 * @author Jim
 * @version 0.1
 * 2022/9/23 16:44
 */

@Slf4j
@Primary
@Component
@RequiredArgsConstructor
public class UploadProcessServiceImpl implements UploadProcessService {

    final Minio minio;

    @SneakyThrows
    @Override
    public String handle(String object, MultipartFile file) {
        String suffix = FileUtil.getSuffix(object);

        // Image: 在文件名上追加图片物理像素
        if (isSupportiveImage(suffix)) {
            object = handleImage(object, file);
        }

        // PDF: 在文件名上追加页数，同时在同目录生成 PDF 每一页的图片
        if (isPdf(suffix)) {
            object = handlePdf(object, file);
        }

        // Video/Audio: 在文件名上追加时长，视频生成封面图
        if (isSupportiveMedia(suffix)) {
            // 将上传的文件转存一份到本地临时目录
            Path tempFile = Paths.get(FileUtil.getTmpDirPath(), "T_" + Commons.uuid(), file.getOriginalFilename());
            FileUtil.mkParentDirs(tempFile);
            file.transferTo(tempFile);
            object = handleMedia(object, suffix, tempFile.toFile());
        }

        return object;
    }

    @SneakyThrows
    @Override
    public String handleMedia(String object, File file) {
        return handleMedia(object, FileUtil.getSuffix(file.getName()), file);
    }

    @SneakyThrows
    public String handleImage(String object, MultipartFile file) {
        @Cleanup
        InputStream is = file.getInputStream();
        BufferedImage image = ImageIO.read(is);
        return fillSuffix(object, image.getWidth() + "x" + image.getHeight());
    }

    @SneakyThrows
    public String handlePdf(String object, MultipartFile file) {
        @Cleanup
        InputStream is = file.getInputStream();
        @Cleanup
        PDDocument doc = Loader.loadPDF(IOUtils.toByteArray(is));
        int pages = doc.getNumberOfPages();

        // 构建具有 PDF 页数标记的存储地址
        String filepath = fillSuffix(object, "P" + pages);

        // 将 PDF 解析成图片上传
        String format = "jpg";
        PDFRenderer renderer = new PDFRenderer(doc);
        for (int i = 0; i < pages; i++) {
            @Cleanup
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            BufferedImage image = renderer.renderImageWithDPI(i, 144);
            ImageIO.write(image, format, os);
            @Cleanup
            InputStream io = new ByteArrayInputStream(os.toByteArray());
            minio.putObject(io, MediaType.IMAGE_JPEG_VALUE, filepath + "." + (i + 1) + "." + format);
        }

        return filepath;
    }

    @SneakyThrows
    public String handleMedia(String object, String suffix, File file) {
        // 判断是否配置 ffmpeg 环境
        FFmpeg ffmpeg = new FFmpeg();
        FFprobe ffprobe = new FFprobe();
        try {
            if (!ffmpeg.isFFmpeg() || !ffprobe.isFFprobe()) {
                return object;
            }
        } catch (Exception e) {
            log.warn(e.getMessage());
            return object;
        }

        // 获取文件路径
        Path path = file.toPath();

        // 解析媒体文件
        FFmpegProbeResult probeResult = ffprobe.probe(path.toString());
        FFmpegFormat format = probeResult.getFormat();

        // 构建具有时长（秒）标记的存储地址
        long sec = (long) format.duration;
        String filepath = fillSuffix(object, "T" + sec);

        // 视频截取首帧可见画面作为封面
        if (isSupportiveVideo(suffix)) {
            // 大视频文件切片上传（> 5s）
            if (sec > 5) {
                filepath = handleVideoToHls(filepath, path, ffmpeg, ffprobe);
            }

            // 使用 ffmpeg 截取视频首帧图片
            Path imagePath = Paths.get(path.getParent().toString(), FileUtil.getName(filepath) + ".jpg");
            handleVideoScreenshot(path.toString(), imagePath.toString(), filepath, ffmpeg, ffprobe);
        }

        // 删除为本次上传进行本地处理所创建的整个临时文件夹目录
        FileUtil.del(path.getParent());
        return filepath;
    }

    @SneakyThrows
    public void handleVideoScreenshot(String in, String out, String object, FFmpeg ffmpeg, FFprobe ffprobe) {
        FFmpegBuilder builder = new FFmpegBuilder()
            .setInput(in)
            .overrideOutputFiles(true)
            .addOutput(out)
            .setFrames(1)
            .done();
        FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
        executor.createJob(builder).run();

        // 判断封面图是否生成
        Path path = Paths.get(out);
        if (Files.exists(path)) {
            // 上传封面图
            minio.putObject(path.toFile(), getObjectFolder(object));
            // 手动 GC 一次
            System.gc();
        }
    }

    @SneakyThrows
    public String handleVideoToHls(String object, Path path, FFmpeg ffmpeg, FFprobe ffprobe) {
        String originName = FileUtil.getName(object);
        String objectFolder = object.replace("/" + originName, "");
        String targetName = originName.replaceAll("^(.*)\\." + FileUtil.getSuffix(originName) + "$", "$1.m3u8");

        // 使用 ffmpeg 将视频文件转换成 hls 切片文件 m3u8+ts
        // "-vsync", "2", "-c:v", "copy", "-c:a", "copy", "-tune", "fastdecode", "-hls_wrap", "0", "-hls_time", "10", "-hls_list_size", "0", "-threads", "12"
        final FFmpegProbeResult probe = ffprobe.probe(path.toString());
        Path tempFile = Paths.get(path.getParent().toString(), targetName);
        FFmpegBuilder builder = new FFmpegBuilder()
            .setInput(path.toString())
            .overrideOutputFiles(true)
            .addOutput(tempFile.toString())
            .setFormat(probe.getFormat().format_name)
            .setStrict(FFmpegBuilder.Strict.STRICT)
            .setFormat("hls")
            .setPreset("ultrafast")
            .addExtraArgs(
                "-vsync",
                "2",
                "-c:v",
                "copy",
                "-c:a",
                "copy",
                "-tune",
                "fastdecode",
                //                "-hls_wrap",
                //                "0",
                "-hls_time",
                "5",
                "-hls_list_size",
                "0",
                "-hls_segment_filename",
                tempFile.toString().replaceAll("^(.*)\\.m3u8$", "$1-%d.ts"),
                "-threads",
                "8"
            )
            .done();
        FFmpegExecutor executor = new FFmpegExecutor(ffmpeg, ffprobe);
        executor.createJob(builder).run();

        if (Files.exists(tempFile)) {
            Pattern pattern = Pattern.compile("^.*\\.ts$");
            // 解析 m3u8 文件
            List<String> lines = FileUtil.readLines(tempFile.toString(), StandardCharsets.UTF_8);
            // 获取 ts 切片文件
            List<String> tss = lines.stream().filter(line -> pattern.matcher(line).matches()).toList();
            // 上传 ts 切片文件
            for (String ts : tss) {
                Path temp = Paths.get(tempFile.getParent().toString(), ts);
                minio.putObject(temp.toFile(), objectFolder);
            }
            // 上传 m3u8 索引文件
            ObjectWriteResponse objectWriteResponse = minio.putObject(tempFile.toFile(), objectFolder);
            object = objectWriteResponse.object();

            // 手动 GC 一次
            System.gc();
        }

        return object;
    }

    public static boolean isSupportiveMedia(String suffix) {
        return isSupportiveVideo(suffix) || isSupportiveAudio(suffix);
    }

    public static boolean isSupportiveVideo(String suffix) {
        return List.of("mp4", "flv", "avi", "rmvb", "rm", "wmv", "mkv", "mpg", "mpeg").contains(suffix.toLowerCase());
    }

    public static boolean isSupportiveAudio(String suffix) {
        return List.of("mp3", "wav").contains(suffix.toLowerCase());
    }

    public static boolean isSupportiveImage(String suffix) {
        return List.of("png", "jpg", "webp", "gif", "tif", "svg", "bmp").contains(suffix.toLowerCase());
    }

    public static boolean isPdf(String suffix) {
        return "pdf".equalsIgnoreCase(suffix);
    }

    public static String fillSuffix(String object, String fill) {
        String suffix = FileUtil.getSuffix(object);
        String regex = "^(.*)\\." + suffix + "$";
        return object.replaceAll(regex, "$1." + fill + "." + suffix);
    }

    public static String getObjectFolder(String object) {
        String sourceName = FileUtil.getName(object);
        return object.replace("/" + sourceName, "");
    }
}
