import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

final class Patch {
    private static final String DIRECTORY_KEY = "\"directory\": \"";
    private static final String FILE_KEY = "\"file\": \"";
    private static final String COMMAND_KEY = "\", \"command\": \"";
    private static final String ENTRY_SUFFIX = "\" }";

    public static void main(String[] args) throws IOException {
        Cli cli = Cli.parse(args);

        if (cli.inPlace()) {
            backup(cli.input());
        }

        CompileCommandsPatcher patcher = new CompileCommandsPatcher();

        String source = Files.readString(cli.input(), StandardCharsets.UTF_8);
        String fixed = patcher.patch(source);

        Files.writeString(
                cli.output(),
                fixed,
                StandardCharsets.UTF_8,
                StandardOpenOption.CREATE,
                StandardOpenOption.TRUNCATE_EXISTING
        );

        patcher.stats().print(cli.output());
    }

    private static void backup(Path input) throws IOException {
        Path backup = input.resolveSibling(input.getFileName() + ".old");
        Files.copy(input, backup, StandardCopyOption.REPLACE_EXISTING);
    }

    private record Cli(Path input, Path output) {
        static Cli parse(String[] args) {
            if (args.length < 1 || args.length > 2) {
                System.err.println("Usage: java patch <compile_commands.json> [output.json]");
                System.exit(1);
            }

            Path input = Path.of(args[0]);
            Path output = args.length == 2 ? Path.of(args[1]) : input;
            return new Cli(input, output);
        }

        boolean inPlace() {
            return input.equals(output);
        }
    }

    private static final class CompileCommandsPatcher {
        private final ShortPathResolver shortPathResolver = new ShortPathResolver();
        private final Stats stats = new Stats();

        Stats stats() {
            return stats;
        }

        String patch(String data) throws IOException {
            String[] lines = data.split("\\r?\\n", -1);
            List<String> fixedLines = new ArrayList<>(lines.length);

            for (String line : lines) {
                fixedLines.add(patchLine(line));
            }

            return String.join(System.lineSeparator(), fixedLines);
        }

        private String patchLine(String line) throws IOException {
            if (!isCompileCommandEntry(line)) {
                return line;
            }

            String beforeFile = substringBefore(line, FILE_KEY) + FILE_KEY;
            String file = readJsonField(line, FILE_KEY, COMMAND_KEY);
            String command = readJsonField(line, COMMAND_KEY, ENTRY_SUFFIX);
            String entrySuffix = line.endsWith(",") ? " }," : " }";

            List<String> arguments = CommandLine.split(removePathmapArgs(command));
            arguments = removePchArgs(arguments);
            arguments = normalizeArguments(arguments);
            fixCompilerPath(arguments);

            stats.convertedEntries++;

            return beforeFile
                    + Json.escape(new File(file).getCanonicalPath())
                    + "\", \"arguments\": "
                    + Json.array(arguments)
                    + entrySuffix;
        }

        private boolean isCompileCommandEntry(String line) {
            return line.startsWith("{ " + DIRECTORY_KEY) && line.contains(COMMAND_KEY);
        }

        private String readJsonField(String line, String startDelimiter, String endDelimiter) {
            String raw = substringBefore(substringAfter(line, startDelimiter), endDelimiter);
            return Json.unescape(raw);
        }

        private String removePathmapArgs(String command) {
            PathmapStripResult result = Pathmap.strip(command);
            stats.removedPathmapArgs += result.removedCount();
            return result.command();
        }

        private List<String> removePchArgs(List<String> arguments) {
            List<String> result = new ArrayList<>(arguments.size());

            for (String argument : arguments) {
                if (PchArgs.isPchArg(argument)) {
                    stats.removedPchArgs++;
                    continue;
                }

                result.add(argument);
            }

            return result;
        }

        private List<String> normalizeArguments(List<String> arguments) throws IOException {
            List<String> result = new ArrayList<>(arguments.size());

            for (int i = 0; i < arguments.size(); i++) {
                String argument = arguments.get(i);
                String lower = argument.toLowerCase();

                if (IncludeArgs.isJoinedIncludeArg(argument)) {
                    result.add("/I");
                    result.add(normalizePath(argument.substring(2)));
                    stats.splitIncludeArgs++;
                    continue;
                }

                if (lower.equals("-i") || lower.equals("/i")) {
                    result.add("/I");

                    if (i + 1 < arguments.size()) {
                        result.add(normalizePath(arguments.get(++i)));
                    }

                    continue;
                }

                result.add(normalizePathArgument(argument));
            }

            return result;
        }

        private String normalizePathArgument(String argument) throws IOException {
            if (OutputPathArgs.hasJoinedPath(argument)) {
                return argument.substring(0, 3) + normalizePath(argument.substring(3));
            }

            if (WindowsPath.looksLikeAbsolutePath(argument)) {
                return normalizePath(argument);
            }

            return argument;
        }

        private String normalizePath(String path) throws IOException {
            String fixed = PathNormalizer.normalize(path);

            if (!path.equals(fixed)) {
                stats.normalizedPathArgs++;
            }

            return fixed;
        }

        private void fixCompilerPath(List<String> arguments) throws IOException {
            if (arguments.isEmpty()) {
                return;
            }

            String before = arguments.get(0);
            String after = shortPathResolver.shortPathIfNeeded(before);

            if (!before.equals(after)) {
                stats.shortenedCompilerPaths++;
                arguments.set(0, after);
            }
        }
    }

    private record PathmapStripResult(String command, int removedCount) {
    }

    private static final class Pathmap {
        private static final String PREFIX = "-pathmap:";

        private Pathmap() {
        }

        static PathmapStripResult strip(String command) {
            StringBuilder fixed = new StringBuilder(command.length());

            int removedCount = 0;
            int cursor = 0;

            while (cursor < command.length()) {
                int start = command.indexOf(PREFIX, cursor);

                if (start < 0) {
                    fixed.append(command, cursor, command.length());
                    break;
                }

                int argStart = rewindWhitespace(command, cursor, start);
                int argEnd = findArgEnd(command, start + PREFIX.length());

                if (argEnd < 0) {
                    fixed.append(command, cursor, start + PREFIX.length());
                    cursor = start + PREFIX.length();
                    continue;
                }

                fixed.append(command, cursor, argStart);

                cursor = skipWhitespace(command, argEnd);
                appendSeparatorIfNeeded(fixed, command, cursor);

                removedCount++;
            }

            return new PathmapStripResult(fixed.toString(), removedCount);
        }

        private static int findArgEnd(String command, int start) {
            for (int i = start; i < command.length(); i++) {
                if (command.charAt(i) != '=') {
                    continue;
                }

                int end = pathmapValueEnd(command, i + 1);
                if (end >= 0) {
                    return end;
                }
            }

            return -1;
        }

        private static int pathmapValueEnd(String command, int valueStart) {
            if (startsWith(command, valueStart, "s")) {
                return skipTrailingQuote(command, valueStart + 1);
            }

            if (startsWith(command, valueStart, "vsi")) {
                return skipTrailingQuote(command, valueStart + 3);
            }

            return -1;
        }

        private static int skipTrailingQuote(String command, int index) {
            if (startsWith(command, index, "\\\"")) {
                return index + 2;
            }

            if (index < command.length() && command.charAt(index) == '"') {
                return index + 1;
            }

            return index;
        }

        private static int rewindWhitespace(String value, int lowerBound, int index) {
            while (index > lowerBound && Character.isWhitespace(value.charAt(index - 1))) {
                index--;
            }

            return index;
        }

        private static int skipWhitespace(String value, int index) {
            while (index < value.length() && Character.isWhitespace(value.charAt(index))) {
                index++;
            }

            return index;
        }

        private static void appendSeparatorIfNeeded(StringBuilder fixed, String command, int cursor) {
            if (fixed.length() == 0 || cursor >= command.length()) {
                return;
            }

            if (!Character.isWhitespace(fixed.charAt(fixed.length() - 1))) {
                fixed.append(' ');
            }
        }
    }

    private static final class CommandLine {
        private CommandLine() {
        }

        static List<String> split(String command) {
            List<String> result = new ArrayList<>();
            StringBuilder current = new StringBuilder();

            boolean inQuotes = false;

            for (int i = 0; i < command.length(); i++) {
                char c = command.charAt(i);

                if (c == '\\' && i + 1 < command.length() && command.charAt(i + 1) == '"') {
                    current.append('"');
                    i++;
                    continue;
                }

                if (c == '"') {
                    inQuotes = !inQuotes;
                    continue;
                }

                if (Character.isWhitespace(c) && !inQuotes) {
                    flush(result, current);
                    continue;
                }

                current.append(c);
            }

            flush(result, current);
            return result;
        }

        private static void flush(List<String> result, StringBuilder current) {
            if (current.length() == 0) {
                return;
            }

            result.add(current.toString());
            current.setLength(0);
        }
    }

    private static final class PchArgs {
        private PchArgs() {
        }

        static boolean isPchArg(String argument) {
            String lower = argument.toLowerCase();

            return lower.equals("-fiprecompiled.hpp")
                    || lower.equals("/fiprecompiled.hpp")
                    || lower.startsWith("-fp") && lower.endsWith(".pch")
                    || lower.startsWith("/fp") && lower.endsWith(".pch")
                    || lower.startsWith("-yu")
                    || lower.startsWith("/yu")
                    || lower.startsWith("-yc")
                    || lower.startsWith("/yc");
        }
    }

    private static final class IncludeArgs {
        private IncludeArgs() {
        }

        static boolean isJoinedIncludeArg(String argument) {
            if (argument.length() <= 2) {
                return false;
            }

            char prefix = argument.charAt(0);
            char option = argument.charAt(1);

            return (prefix == '-' || prefix == '/') && (option == 'I' || option == 'i');
        }
    }

    private static final class OutputPathArgs {
        private OutputPathArgs() {
        }

        static boolean hasJoinedPath(String argument) {
            String lower = argument.toLowerCase();

            return lower.startsWith("-fo")
                    || lower.startsWith("/fo")
                    || lower.startsWith("-fp")
                    || lower.startsWith("/fp");
        }
    }

    private static final class PathNormalizer {
        private PathNormalizer() {
        }

        static String normalize(String path) throws IOException {
            String fixed = collapseExtraBackslashes(path);

            if (WindowsPath.looksLikeAbsolutePath(fixed)) {
                File file = new File(fixed);

                if (file.exists()) {
                    fixed = file.getCanonicalPath();
                }
            }

            return fixed;
        }

        private static String collapseExtraBackslashes(String value) {
            String fixed = value;

            while (fixed.contains("\\\\")) {
                fixed = fixed.replace("\\\\", "\\");
            }

            return fixed;
        }
    }

    private static final class WindowsPath {
        private WindowsPath() {
        }

        static boolean looksLikeAbsolutePath(String value) {
            return value.length() >= 3
                    && Character.isLetter(value.charAt(0))
                    && value.charAt(1) == ':'
                    && (value.charAt(2) == '\\' || value.charAt(2) == '/');
        }
    }

    private static final class ShortPathResolver {
        private final Map<String, String> cache = new HashMap<>();

        String shortPathIfNeeded(String path) throws IOException {
            if (!path.contains(" ")) {
                return path;
            }

            String cached = cache.get(path);
            if (cached != null) {
                return cached;
            }

            String resolved = queryShortPath(path);
            cache.put(path, resolved);
            return resolved;
        }

        private String queryShortPath(String path) throws IOException {
            try {
                Process process = new ProcessBuilder(
                        "cmd",
                        "/c",
                        "for %I in (\"" + path + "\") do @echo %~sI"
                )
                        .redirectErrorStream(true)
                        .start();

                String output = new String(
                        process.getInputStream().readAllBytes(),
                        StandardCharsets.UTF_8
                ).trim();

                int exitCode = process.waitFor();

                if (exitCode == 0 && !output.isEmpty() && !output.contains(" ")) {
                    return output;
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }

            return path;
        }
    }

    private static final class Json {
        private Json() {
        }

        static String array(List<String> values) {
            StringBuilder result = new StringBuilder("[");

            for (int i = 0; i < values.size(); i++) {
                if (i > 0) {
                    result.append(", ");
                }

                result.append('"').append(escape(values.get(i))).append('"');
            }

            return result.append(']').toString();
        }

        static String unescape(String value) {
            StringBuilder result = new StringBuilder(value.length());

            for (int i = 0; i < value.length(); i++) {
                char c = value.charAt(i);

                if (c != '\\' || i + 1 >= value.length()) {
                    result.append(c);
                    continue;
                }

                char escaped = value.charAt(++i);

                switch (escaped) {
                    case '"':
                    case '\\':
                    case '/':
                        result.append(escaped);
                        break;
                    case 'b':
                        result.append('\b');
                        break;
                    case 'f':
                        result.append('\f');
                        break;
                    case 'n':
                        result.append('\n');
                        break;
                    case 'r':
                        result.append('\r');
                        break;
                    case 't':
                        result.append('\t');
                        break;
                    case 'u':
                        if (i + 4 >= value.length()) {
                            result.append("\\u");
                            break;
                        }

                        result.append((char) Integer.parseInt(value.substring(i + 1, i + 5), 16));
                        i += 4;
                        break;
                    default:
                        result.append('\\').append(escaped);
                        break;
                }
            }

            return result.toString();
        }

        static String escape(String value) {
            StringBuilder result = new StringBuilder(value.length() + 16);

            for (int i = 0; i < value.length(); i++) {
                char c = value.charAt(i);

                switch (c) {
                    case '"':
                        result.append("\\\"");
                        break;
                    case '\\':
                        result.append("\\\\");
                        break;
                    case '\b':
                        result.append("\\b");
                        break;
                    case '\f':
                        result.append("\\f");
                        break;
                    case '\n':
                        result.append("\\n");
                        break;
                    case '\r':
                        result.append("\\r");
                        break;
                    case '\t':
                        result.append("\\t");
                        break;
                    default:
                        result.append(c);
                        break;
                }
            }

            return result.toString();
        }
    }

    private static final class Stats {
        private int convertedEntries;
        private int removedPathmapArgs;
        private int removedPchArgs;
        private int splitIncludeArgs;
        private int normalizedPathArgs;
        private int shortenedCompilerPaths;

        void print(Path output) {
            System.out.println("Converted entries: " + convertedEntries);
            System.out.println("Removed -pathmap args: " + removedPathmapArgs);
            System.out.println("Removed PCH args: " + removedPchArgs);
            System.out.println("Split include args: " + splitIncludeArgs);
            System.out.println("Normalized path args: " + normalizedPathArgs);
            System.out.println("Shortened compiler paths: " + shortenedCompilerPaths);
            System.out.println("Wrote: " + output.toAbsolutePath());
        }
    }

    private static String substringAfter(String value, String delimiter) {
        int index = value.indexOf(delimiter);

        if (index < 0) {
            return value;
        }

        return value.substring(index + delimiter.length());
    }

    private static String substringBefore(String value, String delimiter) {
        int index = value.indexOf(delimiter);

        if (index < 0) {
            return value;
        }

        return value.substring(0, index);
    }

    private static boolean startsWith(String value, int offset, String prefix) {
        return offset >= 0
                && offset + prefix.length() <= value.length()
                && value.regionMatches(offset, prefix, 0, prefix.length());
    }
}
