ext.PatchJarModuleTask = PatchJarModuleTask

/*
 * Copyright (C) 2021 Emerson Pinter - All Rights Reserved
 */


import java.nio.file.Files
import java.nio.file.Path;
import java.util.regex.Matcher
import java.util.regex.Pattern

class PatchJarModuleTask extends DefaultTask {
    @Input
    @Optional
    Boolean debug = false
    @Input
    boolean delete = true
    @Input
    String tempPatchDir
    @Input
    String tempJarDir
    @Input
    @Optional
    Map<String, List<String>> moduleDeps

    @Internal
    List<String> processed = new ArrayList<>()

    @Internal
    Map<String, String> packageMap = new HashMap<>()

    @Internal
    Map<String, String> packageDepInfo = new HashMap<>()

    @Input
    @Optional
    List<String> ignoreMissingDeps

    @Input
    @Optional
    Map<String, List<String>> moduleDepsPath

    @Input
    @Optional
    String modulesDepsDir;

    @Internal
    private List<String> patchedModules = new ArrayList<>()

    @SuppressWarnings('unused')
    List<String> getPatchedModules() {
        return patchedModules
    }

    private File fileTempPatchDir
    private File fileTempJarDir
    private final String JAR = findJava("jar")
    private final String JDEPS = findJava("jdeps")
    private final String JAVAC = findJava("javac")

    static void printStream(InputStream stream) {
        stream.eachLine {
            System.out.println("[patchModulesJar] \t" + it)
        }
    }

    private static String findJava(String tool) {
        return String.format("%2\$s%1\$sbin%1\$s%3\$s",
                File.separator,
                System.getProperty("java.home"),
                tool,
                OperatingSystemFamily.WINDOWS ? ".exe" : "")
    }

    String readPackageName(String filename, boolean automatic) {
        if (debug) System.out.println("[patchModulesJar] Verifying jar " + filename)

        ProcessBuilder processBuilder = new ProcessBuilder()
        processBuilder.command(JAR, "-d", "-f", filename, "--release", "9")
        Process jar = processBuilder.start()


        BufferedReader stdout = new BufferedReader(new InputStreamReader(jar.getInputStream()))

        String s = ""
        String line
        String packageName = null
        while ((line = stdout.readLine()) != null) {
            Pattern p = null;
            if(automatic) {
                p = Pattern.compile("(^[^@]+)@.* automatic\$")
            } else {
                p = Pattern.compile("(^[^@]+)@.*module-info.class\$")
            }
            Matcher m = p.matcher(line)
            if (m.matches() && packageName == null) {
                s += line
                packageName = m.group(1)
            }
        }
        jar.waitFor()

        return packageName
    }


    @TaskAction
    void start() {
        if (debug == null) {
            debug = false
        }

        fileTempPatchDir = new File(tempPatchDir)
        fileTempJarDir = new File(tempJarDir)

        fileTempPatchDir.deleteDir()
        fileTempPatchDir.mkdir()

        if (!fileTempJarDir.exists()) {
            fileTempJarDir.mkdir()
        }

        if(modulesDepsDir != null && Files.exists(Path.of(modulesDepsDir))) {
            new File(modulesDepsDir).eachFile {
                if (it.getName().endsWith(".jar")) {
                    String packageName = readPackageName(it.getPath(), false)
                    if (packageName != null) {
                        System.out.println(String.format("[patchModulesJar] Package found '%s' on file '%s'", packageName, it.getName()))
                        packageDepInfo.put(it.getPath(), packageName)
                    }
                }
            }
        }

        new File(tempJarDir).eachFile {
            if (it.getName().endsWith(".jar")) {
                String packageName = readPackageName(it.getPath(), true)
                if (packageName != null) {
                    System.out.println(String.format("[patchModulesJar] Package found '%s' on file '%s', generating module-info", packageName, it.getName()))
                    packageMap.put(it.getPath(), packageName)
                } else {
                    if (debug) System.out.println(String.format("[patchModulesJar] No automatic module found on file '%s', ignoring", it.getName()))
                    it.delete()
                    return
                }
            }
        }

        new File(tempJarDir).eachFile {
            if (it.getName().endsWith(".jar")) {
                processFile(it)
            }
        }
    }

    String getJarFromPackage(String packageName) {
        def first = packageMap.entrySet().stream().filter(e -> { e.value == packageName }).findFirst()
        if (first.isPresent()) {
            return first.get().key
        }
        return null
    }

    String getJarFromPackageModule(String packageName) {
        def first = packageDepInfo.entrySet().stream().filter(e -> { e.value == packageName }).findFirst()
        if (first.isPresent()) {
            return first.get().key
        }
        return null
    }

    List<String> getModuleDeps(String packageName) {
        final List<String> ret = new ArrayList<>()
        if (moduleDeps != null && moduleDeps.size() > 0 && moduleDeps.get(packageName) != null) {
            for (Map.Entry<String, List<String>> entry : moduleDeps.entrySet()) {
                if (entry.key == packageName && entry.value != null && !entry.value.isEmpty()) {
                    entry.value.forEach(d -> {
                        ret.add(d)
                        ret.addAll(getModuleDeps(d))
                    })
                }
            }
        }

        return ret
    }

    boolean processFile(File file) {
        String filename = file.getPath()

        if (processed.contains(file.getName())) {
            if (debug) System.out.printf("[patchModulesJar] File %s already processed, skipping\n", file.getName())
            return true
        }

        System.out.printf("[patchModulesJar] ***** Processing file %s\n", file.getPath())

        String packageName = packageMap.get(file.getPath())

        if (moduleDeps.get(packageName) != null) {
            moduleDeps.get(packageName).forEach(dep -> {
                packageMap.entrySet().stream().filter((e) -> {
                    dep == e.value
                }).forEach((e2) -> {
                    if (debug) System.out.printf("[patchModulesJar] processing dependency: %s / %s=%s\n", dep, e2.key, e2.value)
                    System.out.printf("[patchModulesJar] \tprocessing dependency: %s\n", dep)
                    processFile(new File(e2.key))
                })
            })
        }

        if (packageName == null) {
            throw new IllegalStateException("[patchModulesJar] package name not found :" + file)
        }

        processed.add(file.getName())

        ProcessBuilder processBuilder = new ProcessBuilder()
        processBuilder.directory(fileTempJarDir)
        String addModules = ""

        if (moduleDeps != null && moduleDeps.size() > 0 && moduleDeps.get(packageName) != null) {
            addModules = "--add-modules="
            for (Map.Entry<String, List<String>> entry : moduleDeps.entrySet()) {
                if (entry.key == packageName && entry.value != null && !entry.value.isEmpty()) {
                    addModules += String.join(",", entry.value).replaceAll(" ", "")
                }
            }
        }

        List<String> modulePath = new ArrayList<>()
        if (moduleDepsPath != null && moduleDepsPath.size() > 0 && moduleDepsPath.get(packageName) != null) {
            for (Map.Entry<String, List<String>> entry : moduleDepsPath.entrySet()) {
                if (entry.key == packageName && entry.value != null && !entry.value.isEmpty()) {
                    modulePath.add(String.join(File.pathSeparator, entry.value))
                }
            }
        } else {
            List<String> deps = getModuleDeps(packageName)
            deps.stream().forEach(dep -> {
                String jar = getJarFromPackage(dep)
                if (jar != null) {
                    modulePath.add(jar)
                }
                jar = getJarFromPackageModule(dep);
                if (jar != null) {
                    modulePath.add(jar)
                }
            })
        }

        List<String> command = new ArrayList<>()
        command.add(JDEPS)
        if(ignoreMissingDeps != null && ignoreMissingDeps.contains(packageName)) {
            command.add("--ignore-missing-deps")
        }

        if (!modulePath.isEmpty()) {
            command.add("--module-path")
            command.add(String.join(File.pathSeparator, modulePath))
        }
        if (addModules != "") {
            command.add(addModules)
        }

        command.addAll(List.of("--generate-module-info", tempPatchDir, filename))

        processBuilder.command(command)
        if (debug) System.out.println("[patchModulesJar] Running: " + processBuilder.command())

        Process jdeps = processBuilder.start()
        printStream(jdeps.getInputStream())
        printStream(jdeps.getErrorStream())
        jdeps.waitFor()

        if (jdeps.exitValue() != 0) {
            System.out.printf("[patchModulesJar] jdeps exited with error %s for %s\n", jdeps.exitValue(), packageName)
            if (delete) {
                new File(filename).delete()
            }
            return false
        }
        if (debug) System.out.println("[patchModulesJar] jdeps finished for " + packageName)

        processBuilder = new ProcessBuilder()
        processBuilder.directory(fileTempPatchDir)
        processBuilder.command(JAVAC, "--module-path", tempJarDir, "--module-path", modulesDepsDir, "--patch-module", String.format("%s=%s", packageName, filename),
                String.format("%s/module-info.java", packageName), "-nowarn", "-d", ".")

        if (debug) System.out.println("[patchModulesJar] \tRunning: " + processBuilder.command())
        Process javac = processBuilder.start()
        printStream(javac.getInputStream())
        printStream(javac.getErrorStream())
        javac.waitFor()
        if (javac.exitValue() != 0) {
            new File(filename).delete()
            return false
        }
        if (debug) System.out.println("[patchModulesJar] javac finished")

        processBuilder = new ProcessBuilder()
        processBuilder.directory(fileTempPatchDir)
        processBuilder.command(JAR, "--update", "--file", filename, "module-info.class")
        if (debug) System.out.println("[patchModulesJar] Running: " + processBuilder.command())
        Process jarUpdate = processBuilder.start()
        printStream(jarUpdate.getInputStream())
        printStream(jarUpdate.getErrorStream())
        jarUpdate.waitFor()
        if (jarUpdate.exitValue() != 0) {
            if (delete) {
                new File(filename).delete()
            }
            return false
        }
        fileTempPatchDir.deleteDir()
        if (debug) System.out.println("[patchModulesJar] jar update finished")
        if (new File(filename).exists()) {
            System.out.printf("[patchModulesJar] \tJAR file '%s' (%s) patched\n", filename, packageName)
            patchedModules.add(packageName)
            return true
        } else {
            System.out.printf("[patchModulesJar] \tJAR file '%s' (%s) discarded\n", filename, packageName)
            return false
        }
    }

}
