package com.example.exiftool;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Stream;

/**
 * exiftool 命令的跨平台封装。
 *
 * <p>运行时根据操作系统从 {@code resources/exiftool/<platform>/} 选择对应二进制,
 * 将该目录整体释放到临时工作目录后执行。支持从 {@code target/classes} 展开运行
 * 以及从打包后的 jar 内运行两种场景。
 *
 * <p>示例:
 * <pre>{@code
 * ExifTool exiftool = new ExifTool();
 * ExifToolResult r = exiftool.addSubject("333.jpg", "contains-synthetic-performer");
 * if (!r.isSuccess()) System.err.println(r.stderr());
 * }</pre>
 */
public class ExifTool {

    private static final String RESOURCE_ROOT = "exiftool";

    /** 批量参数拼成命令行的长度阈值,超过则改用 -@ 参数文件,规避 OS 命令行长度上限。 */
    private static final int ARG_LINE_THRESHOLD = 30000;

    /** 缓存解析出的可执行文件路径,懒加载,双检锁保证线程安全。 */
    private static volatile Path resolvedExecutable;

    /** 返回当前平台目录名:windows / linux。 */
    static String platform() {
        String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
        return os.contains("win") ? "windows" : "linux";
    }

    /**
     * 通用执行:运行 {@code [exiftool] + args}。
     *
     * @param args exiftool 的参数,如 {@code "-overwrite_original", "-XMP-dc:Subject+=foo", "333.jpg"}
     * @return 执行结果(含退出码与输出);非零退出不抛异常
     */
    public ExifToolResult execute(String... args) {
        return execute(Arrays.asList(args));
    }

    /** 通用执行,参数列表形式。 */
    public ExifToolResult execute(List<String> args) {
        Path exe = executable();
        List<String> command = new ArrayList<String>(args.size() + 1);
        command.add(exe.toString());
        command.addAll(args);

        ProcessBuilder pb = new ProcessBuilder(command);
        try {
            Process process = pb.start();
            // 用独立线程读取 stderr,避免管道缓冲区填满导致死锁。
            StreamReader errReader = new StreamReader(process.getErrorStream());
            Thread errThread = new Thread(errReader, "exiftool-stderr");
            errThread.start();

            String stdout = readAll(process.getInputStream());
            int exitCode = process.waitFor();
            errThread.join();

            return new ExifToolResult(exitCode, stdout, errReader.text());
        } catch (IOException e) {
            throw new ExifToolException("启动 exiftool 进程失败: " + command, e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new ExifToolException("等待 exiftool 进程被中断", e);
        }
    }

    /** 同 {@link #execute}, 但非零退出码时抛出 {@link ExifToolException}。 */
    public ExifToolResult executeOrThrow(String... args) {
        ExifToolResult result = execute(args);
        if (!result.isSuccess()) {
            throw new ExifToolException(
                    "exiftool 退出码 " + result.exitCode() + ": " + result.stderr().trim());
        }
        return result;
    }

    /**
     * 便捷方法:向文件的 XMP-dc:Subject 追加一个值(去重追加)。
     * 等价于 {@code exiftool -overwrite_original -XMP-dc:Subject+=<value> <file>}。
     */
    public ExifToolResult addSubject(String file, String value) {
        return execute(buildSubjectArgs(file, value));
    }

    /** 构造 addSubject 的参数列表(抽出以便单测)。 */
    static List<String> buildSubjectArgs(String file, String value) {
        return Arrays.asList("-overwrite_original", "-XMP-dc:Subject+=" + value, file);
    }

    // ----- 批量处理(方案 A:一条命令处理多文件,仅 1 个进程)-----

    /**
     * 批量执行:对一批文件应用<b>相同</b>的 exiftool 选项,单个进程处理全部。
     *
     * <p>参数量小时直接走命令行;拼接长度超过 {@link #ARG_LINE_THRESHOLD} 时
     * 自动改用 exiftool 的 {@code -@ 参数文件}(临时文件, UTF-8),规避
     * Windows/Linux 的命令行长度上限。
     *
     * @param options 应用到每个文件的选项,如 {@code ["-overwrite_original", "-XMP-dc:Subject+=foo"]}
     * @param files   文件路径列表,不能为空
     * @return 单次执行结果;任一文件出错时 exitCode 非零,stderr 含逐文件错误信息
     */
    public ExifToolResult executeBatch(List<String> options, List<String> files) {
        List<String> combined = combineBatch(options, files);

        long approxLen = 0;
        for (String s : combined) {
            approxLen += s.length() + 3; // 预留引号/分隔符
        }
        if (approxLen > ARG_LINE_THRESHOLD) {
            return executeViaArgFile(combined);
        }
        return execute(combined);
    }

    /**
     * 便捷方法:给一批文件的 XMP-dc:Subject 追加<b>同一个</b>值,单进程完成。
     * 等价于 {@code exiftool -overwrite_original -XMP-dc:Subject+=<value> f1 f2 ...}。
     */
    public ExifToolResult addSubjectBatch(List<String> files, String value) {
        return executeBatch(
                Arrays.asList("-overwrite_original", "-XMP-dc:Subject+=" + value),
                files);
    }

    /** 合并选项与文件为一个参数列表(抽出以便单测);files 为空时抛异常。 */
    static List<String> combineBatch(List<String> options, List<String> files) {
        if (files == null || files.isEmpty()) {
            throw new IllegalArgumentException("批量文件列表不能为空");
        }
        List<String> combined = new ArrayList<String>();
        if (options != null) {
            combined.addAll(options);
        }
        combined.addAll(files);
        return combined;
    }

    /** 把参数写入临时 -@ 参数文件后执行,用完删除。 */
    private ExifToolResult executeViaArgFile(List<String> args) {
        Path argFile = null;
        try {
            argFile = Files.createTempFile("exiftool-args-", ".txt");
            writeArgFile(argFile, args);
            return execute(Arrays.asList("-@", argFile.toString()));
        } catch (IOException e) {
            throw new ExifToolException("写入 exiftool 参数文件失败", e);
        } finally {
            if (argFile != null) {
                try {
                    Files.deleteIfExists(argFile);
                } catch (IOException ignored) {
                    // 临时文件删除失败不影响结果。
                }
            }
        }
    }

    /** 每个参数一行写入文件(UTF-8),供 exiftool -@ 读取。 */
    static void writeArgFile(Path file, List<String> args) throws IOException {
        StringBuilder sb = new StringBuilder();
        for (String a : args) {
            sb.append(a).append('\n');
        }
        Files.write(file, sb.toString().getBytes(StandardCharsets.UTF_8));
    }

    // ----- 可执行文件解析与释放 -----

    /** 返回(必要时先释放并缓存)当前平台的 exiftool 可执行文件路径。 */
    static Path executable() {
        Path cached = resolvedExecutable;
        if (cached != null) {
            return cached;
        }
        synchronized (ExifTool.class) {
            if (resolvedExecutable == null) {
                resolvedExecutable = prepare(platform());
            }
            return resolvedExecutable;
        }
    }

    private static Path prepare(String platform) {
        String prefix = RESOURCE_ROOT + "/" + platform;
        URL res = ExifTool.class.getClassLoader().getResource(prefix);
        if (res == null) {
            throw new ExifToolException("未找到资源目录 " + prefix
                    + " ,请把对应平台的 exiftool 放入 src/main/resources/" + prefix + "/");
        }

        Path workDir = Paths.get(System.getProperty("java.io.tmpdir"), "exiftool-java", platform);
        try {
            Files.createDirectories(workDir);
            String protocol = res.getProtocol();
            if ("file".equals(protocol)) {
                copyTree(Paths.get(res.toURI()), workDir);
            } else if ("jar".equals(protocol)) {
                extractFromJar(prefix, workDir);
            } else {
                throw new ExifToolException("不支持的资源协议: " + protocol);
            }
        } catch (IOException e) {
            throw new ExifToolException("释放 exiftool 到临时目录失败: " + workDir, e);
        } catch (URISyntaxException e) {
            throw new ExifToolException("释放 exiftool 到临时目录失败: " + workDir, e);
        }

        Path exe = locateExecutable(workDir, platform);
        if (!platform.equals("windows")) {
            exe.toFile().setExecutable(true, false);
        }
        return exe;
    }

    /** 在工作目录中定位可执行文件。 */
    private static Path locateExecutable(Path workDir, final String platform) {
        String preferred = platform.equals("windows") ? "exiftool.exe" : "exiftool";
        Path exact = workDir.resolve(preferred);
        if (Files.isRegularFile(exact)) {
            return exact;
        }
        try (Stream<Path> stream = Files.list(workDir)) {
            return stream
                    .filter(new java.util.function.Predicate<Path>() {
                        public boolean test(Path p) {
                            return Files.isRegularFile(p);
                        }
                    })
                    .filter(new java.util.function.Predicate<Path>() {
                        public boolean test(Path p) {
                            return p.getFileName().toString().toLowerCase(Locale.ROOT).startsWith("exiftool");
                        }
                    })
                    .findFirst()
                    .orElseThrow(new java.util.function.Supplier<ExifToolException>() {
                        public ExifToolException get() {
                            return new ExifToolException(
                                    "在 " + workDir + " 未找到 exiftool 可执行文件,"
                                            + "请把对应平台的 exiftool 放入 src/main/resources/"
                                            + RESOURCE_ROOT + "/" + platform + "/");
                        }
                    });
        } catch (IOException e) {
            throw new ExifToolException("扫描 exiftool 工作目录失败: " + workDir, e);
        }
    }

    /** 递归复制目录树(用于从展开的 classes 目录运行)。 */
    private static void copyTree(final Path source, final Path target) throws IOException {
        Files.walkFileTree(source, new SimpleFileVisitor<Path>() {
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                Files.createDirectories(target.resolve(source.relativize(dir).toString()));
                return FileVisitResult.CONTINUE;
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.copy(file, target.resolve(source.relativize(file).toString()),
                        StandardCopyOption.REPLACE_EXISTING);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    /** 从 jar 中提取某前缀下的所有条目到工作目录(用于打包后运行)。 */
    private static void extractFromJar(String prefix, Path target) throws IOException {
        String jarPath = jarPath(prefix);
        JarFile jar = new JarFile(jarPath);
        try {
            String dirPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
            Enumeration<JarEntry> entries = jar.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                String name = entry.getName();
                if (!name.startsWith(dirPrefix) || name.equals(dirPrefix)) {
                    continue;
                }
                String relative = name.substring(dirPrefix.length());
                Path out = target.resolve(relative);
                if (entry.isDirectory()) {
                    Files.createDirectories(out);
                } else {
                    Files.createDirectories(out.getParent());
                    InputStream in = jar.getInputStream(entry);
                    try {
                        Files.copy(in, out, StandardCopyOption.REPLACE_EXISTING);
                    } finally {
                        in.close();
                    }
                }
            }
        } finally {
            jar.close();
        }
    }

    /** 从 jar 资源 URL 解析出 jar 文件的本地路径。 */
    private static String jarPath(String prefix) {
        URL res = ExifTool.class.getClassLoader().getResource(prefix);
        // 形如 jar:file:/path/app.jar!/exiftool/windows
        String path = res.getPath();
        int bang = path.indexOf('!');
        String filePart = bang >= 0 ? path.substring(0, bang) : path;
        if (filePart.startsWith("file:")) {
            filePart = filePart.substring("file:".length());
        }
        try {
            // 处理 URL 编码(空格等)与 Windows 前导斜杠。
            String decoded = URLDecoder.decode(filePart, "UTF-8");
            if (decoded.matches("/[A-Za-z]:/.*")) {
                decoded = decoded.substring(1);
            }
            return decoded;
        } catch (UnsupportedEncodingException e) {
            return filePart;
        }
    }

    private static String readAll(InputStream in) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        byte[] chunk = new byte[8192];
        int n;
        while ((n = in.read(chunk)) != -1) {
            buffer.write(chunk, 0, n);
        }
        return new String(buffer.toByteArray(), StandardCharsets.UTF_8);
    }

    /** 后台读取一个流全部内容。 */
    private static final class StreamReader implements Runnable {
        private final InputStream in;
        private volatile String text = "";

        StreamReader(InputStream in) {
            this.in = in;
        }

        String text() {
            return text;
        }

        @Override
        public void run() {
            try {
                text = readAll(in);
            } catch (IOException ignored) {
                // 流读取失败时保留已读内容(空字符串)。
            }
        }
    }
}
