package de.renew.navigator.gui;

import java.awt.event.ActionEvent;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.swing.AbstractAction;
import javax.swing.JOptionPane;
import javax.swing.SwingWorker;

/**
 * Creates a {@code .class} file for a {@code .java} source file that extends
 * {@code de.renew.net.NetInstanceImpl}.
 */
public class CreateClassFileAction extends AbstractAction {
    private final File _sourceFile;

    private static final Pattern EXTENDS_FQ_NETINSTANCEIMPL =
        Pattern.compile("\\bextends\\s+de\\.renew\\.net\\.NetInstanceImpl\\b");
    private static final Pattern IMPORT_NETINSTANCEIMPL =
        Pattern.compile("\\bimport\\s+de\\.renew\\.net\\.NetInstanceImpl\\s*;\\s*$");
    private static final Pattern EXTENDS_SIMPLE_NETINSTANCEIMPL =
        Pattern.compile("\\bextends\\s+NetInstanceImpl\\b");

    /**
     * Creates the action for the given source file.
     *
     * @param sourceFile the {@code .java} file to compile
     */
    public CreateClassFileAction(File sourceFile) {
        super("Create Class File");
        this._sourceFile = sourceFile;
    }

    /**
     * Checks whether a Java source file declares a class that extends
     * {@code de.renew.net.NetInstanceImpl}.
     *
     * @param source the Java source file
     * @return {@code true} if the file contains an appropriate {@code extends} clause
     */
    public static boolean extendsNetInstanceImpl(File source) {
        if (source == null || !source.isFile() || !source.getName().endsWith(".java")) {
            return false;
        }

        boolean hasImportNet = false;
        try (BufferedReader br = new BufferedReader(new FileReader(source))) {
            String line;
            while ((line = br.readLine()) != null) {
                String normalized = stripInlineComment(line);
                if (normalized.isEmpty()) {
                    continue;
                }
                if (EXTENDS_FQ_NETINSTANCEIMPL.matcher(normalized).find()) {
                    return true;
                }
                if (IMPORT_NETINSTANCEIMPL.matcher(normalized).find()) {
                    hasImportNet = true;
                }
                if (hasImportNet && EXTENDS_SIMPLE_NETINSTANCEIMPL.matcher(normalized).find()) {
                    return true;
                }
            }
        } catch (IOException ignore) {
            return false;
        }
        return false;
    }

    /**
     * Removes trailing {@code //} comments and trims whitespace.
     *
     * @param line a source line
     * @return the line without the inline comment
     */
    private static String stripInlineComment(String line) {
        if (line == null) {
            return "";
        }
        int idx = line.indexOf("//");
        String s = idx >= 0 ? line.substring(0, idx) : line;
        return s.trim();
    }

    @Override
    /**
     * Runs the compilation in a background worker and shows a result dialog.
     *
     * @param e the originating action event
     */
    public void actionPerformed(ActionEvent e) {
        new SwingWorker<String, Void>() {
            @Override
            protected String doInBackground() throws Exception {
                runJcompileForFile(_sourceFile);
                return _sourceFile.getName();
            }

            @Override
            protected void done() {
                try {
                    String result = get();
                    JOptionPane
                        .showMessageDialog(null, "Successfully created class file for: " + result);
                } catch (Exception ex) {
                    JOptionPane.showMessageDialog(
                        null, "Create Class File failed: " + ex.getMessage(), "Create Class File",
                        JOptionPane.ERROR_MESSAGE);
                }
            }
        }.execute();
    }

    /**
     * Compiles the given source file using Renew's {@code jcompile} if available,
     * otherwise falls back to {@code javac} with the workspace {@code dist} classpath.
     *
     * @param source the source file to compile
     * @throws Exception if compilation fails
     */
    private static void runJcompileForFile(File source) throws Exception {
        File repoRoot = findRepoRoot();
        if (repoRoot == null) {
            throw new Exception("Could not locate repo root.");
        }

        File jcompile = findJcompile(repoRoot);
        if (jcompile != null && jcompile.exists() && jcompile.isFile()) {
            runProcess(repoRoot, buildJcompileCommand(jcompile, source));
            return;
        }

        File outDir = new File(source.getParentFile(), "bin");
        if (!outDir.exists() && !outDir.mkdirs()) {
            throw new Exception("Could not create output directory: " + outDir.getAbsolutePath());
        }

        String classpath = buildDistClasspath(repoRoot);
        if (classpath.isEmpty()) {
            throw new Exception(
                "Could not locate dist jars for classpath (expected dist/plugins and/or dist/libs).");
        }

        File javac = findJavaTool("javac");
        List<String> cmd = new ArrayList<>();
        cmd.add(javac.getPath());
        cmd.add("-classpath");
        cmd.add(classpath);
        cmd.add("-d");
        cmd.add(outDir.getAbsolutePath());
        cmd.add(source.getAbsolutePath());
        runProcess(repoRoot, cmd);
    }

    /**
     * Builds a classpath consisting of all JARs in {@code dist/plugins} and {@code dist/libs}.
     *
     * @param repoRoot the repository root directory
     * @return the classpath string or an empty string if {@code dist} is missing
     */
    private static String buildDistClasspath(File repoRoot) {
        File dist = new File(repoRoot, "dist");
        if (!dist.isDirectory()) {
            return "";
        }

        List<File> jars = new ArrayList<>();

        File loaderJar = new File(dist, "de.renew.loader.jar");
        if (loaderJar.isFile()) {
            jars.add(loaderJar);
        }

        collectJarsRecursively(new File(dist, "plugins"), jars);
        collectJarsRecursively(new File(dist, "libs"), jars);

        return jars.stream().map(File::getAbsolutePath).distinct()
            .collect(Collectors.joining(File.pathSeparator));
    }

    /**
     * Collects all {@code .jar} files under the given file or directory.
     *
     * @param root the starting file or directory
     * @param out the list to append JAR files to
     */
    private static void collectJarsRecursively(File root, List<File> out) {
        if (root == null || !root.exists()) {
            return;
        }
        if (root.isFile()) {
            if (root.getName().toLowerCase(Locale.ROOT).endsWith(".jar")) {
                out.add(root);
            }
            return;
        }
        File[] children = root.listFiles();
        if (children == null) {
            return;
        }
        for (File child : children) {
            collectJarsRecursively(child, out);
        }
    }

    /**
     * Builds the command line to invoke {@code jcompile} for the given source.
     *
     * @param jcompile the {@code jcompile} script or executable
     * @param source the source file
     * @return command tokens
     */
    private static List<String> buildJcompileCommand(File jcompile, File source) {
        boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
        boolean isBat = jcompile.getName().toLowerCase(Locale.ROOT).endsWith(".bat");

        List<String> cmd = new ArrayList<>();
        if (isWindows && isBat) {
            cmd.add("cmd");
            cmd.add("/c");
        }
        cmd.add(jcompile.getAbsolutePath());
        cmd.add(source.getAbsolutePath());
        return cmd;
    }

    /**
     * Runs a process and returns only if it exits successfully.
     *
     * @param workingDir the working directory
     * @param cmd the command tokens
     * @throws Exception if the process exits with a non-zero code
     */
    private static void runProcess(File workingDir, List<String> cmd) throws Exception {
        ProcessBuilder pb = new ProcessBuilder(cmd).directory(workingDir).redirectErrorStream(true);
        Process p = pb.start();

        StringBuilder out = new StringBuilder();
        try (BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream()))) {
            String line;
            while ((line = in.readLine()) != null) {
                out.append(line).append(System.lineSeparator());
            }
        }

        int rc = p.waitFor();
        if (rc != 0) {
            throw new Exception(
                "Command failed (exit " + rc + "). Output:\n" + out + "\nCommand: "
                    + String.join(" ", cmd));
        }
    }

    /**
     * Locates a Java tool by checking {@code JAVA_HOME} first, then falling back to PATH.
     *
     * @param toolName the tool name (e.g. {@code javac})
     * @return a file representing the executable to run
     */
    private static File findJavaTool(String toolName) {
        boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
        String exe = isWindows ? toolName + ".exe" : toolName;

        String javaHome = System.getenv("JAVA_HOME");
        if (javaHome != null && !javaHome.trim().isEmpty()) {
            File candidate = new File(new File(javaHome.trim(), "bin"), exe);
            if (candidate.exists()) {
                return candidate;
            }
        }

        return new File(exe);
    }

    /**
     * Attempts to locate the {@code jcompile} script in known workspace locations.
     *
     * @param repoRoot the repository root directory
     * @return a file pointing to {@code jcompile}; may not exist
     */
    private static File findJcompile(File repoRoot) {
        boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
        String name = isWindows ? "jcompile.bat" : "jcompile";

        File[] candidates = new File[] {
            new File(repoRoot, "dist/bin/" + name), new File(repoRoot, "dist/bin/windows/" + name),
            new File(repoRoot, "dist/bin/unix/" + name),
            new File(repoRoot, "Core/bin/windows/" + name),
            new File(repoRoot, "Core/bin/unix/" + name) };

        for (File c : candidates) {
            if (c.exists() && c.isFile()) {
                return c;
            }
        }

        return new File(name);
    }

    /**
     * Searches parent directories for a Gradle project root.
     *
     * @return the repository root directory, or {@code null} if not found
     * @throws IOException if canonicalization fails
     */
    private static File findRepoRoot() throws IOException {
        File dir = new File(System.getProperty("user.dir")).getCanonicalFile();
        while (dir != null) {
            if (new File(dir, "settings.gradle").exists() || new File(dir, "build.gradle").exists()
                || new File(dir, "gradlew").exists() || new File(dir, "gradlew.bat").exists()
                || "Renew".equalsIgnoreCase(dir.getName())) {
                return dir;
            }
            dir = dir.getParentFile();
        }
        return null;
    }
}
