package jmv;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringJoiner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.eclipse.jdt.core.compiler.CategorizedProblem;
import org.eclipse.jdt.internal.compiler.ClassFile;
import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies;
import org.eclipse.jdt.internal.compiler.ICompilerRequestor;
import org.eclipse.jdt.internal.compiler.batch.CompilationUnit;
import org.eclipse.jdt.internal.compiler.batch.FileSystem;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;

/** Compiles one Java 17 source file in memory with ECJ against the Java 17 API signature jar. */
public final class Compiler {
    public static final class Problem {
        public final int line; public final String message; public final boolean error;
        Problem(int line, String message, boolean error) { this.line = line; this.message = message; this.error = error; }
    }

    public static final class Result {
        public final Map<String, byte[]> classes = new LinkedHashMap<>();
        public final List<Problem> problems = new ArrayList<>();
        public final String unitName;
        Result(String unitName) { this.unitName = unitName; }
        public boolean hasErrors() { for (Problem p : problems) if (p.error) return true; return false; }
    }

    static final Pattern PUBLIC_TYPE = Pattern.compile("public\\s+(?:final\\s+|abstract\\s+|static\\s+)*(?:class|interface|enum|record)\\s+([A-Za-z_$][A-Za-z0-9_$]*)");
    static final Pattern ANY_TYPE = Pattern.compile("(?:class|interface|enum|record)\\s+([A-Za-z_$][A-Za-z0-9_$]*)");

    /** ECJ requires the file name to match the public type, so derive it from the source. */
    public static String unitName(String source) {
        Matcher m = PUBLIC_TYPE.matcher(source);
        if (m.find()) return m.group(1) + ".java";
        m = ANY_TYPE.matcher(source);
        return (m.find() ? m.group(1) : "Main") + ".java";
    }

    public static Result compile(String source, String apiJar) {
        String unit = unitName(source);
        Result result = new Result(unit);
        Map<String, String> options = new HashMap<>();
        options.put(CompilerOptions.OPTION_Compliance, CompilerOptions.VERSION_17);
        options.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_17);
        options.put(CompilerOptions.OPTION_TargetPlatform, CompilerOptions.VERSION_17);
        options.put(CompilerOptions.OPTION_LocalVariableAttribute, CompilerOptions.GENERATE);
        options.put(CompilerOptions.OPTION_LineNumberAttribute, CompilerOptions.GENERATE);
        options.put(CompilerOptions.OPTION_SourceFileAttribute, CompilerOptions.GENERATE);
        options.put(CompilerOptions.OPTION_PreserveUnusedLocal, CompilerOptions.PRESERVE);
        options.put(CompilerOptions.OPTION_ReportUnusedLocal, CompilerOptions.IGNORE);
        FileSystem environment = new FileSystem(new String[]{apiJar}, new String[]{unit}, "UTF-8");
        ICompilerRequestor output = compiled -> {
            if (compiled.getAllProblems() != null) for (CategorizedProblem p : compiled.getAllProblems())
                result.problems.add(new Problem(p.getSourceLineNumber(), p.getMessage(), p.isError()));
            if (compiled.hasErrors()) return;
            for (ClassFile f : compiled.getClassFiles()) {
                StringJoiner name = new StringJoiner(".");
                for (char[] part : f.getCompoundName()) name.add(new String(part));
                result.classes.put(name.toString(), f.getBytes());
            }
        };
        org.eclipse.jdt.internal.compiler.Compiler compiler = new org.eclipse.jdt.internal.compiler.Compiler(
            environment, DefaultErrorHandlingPolicies.proceedWithAllProblems(), options, output, new DefaultProblemFactory(Locale.ENGLISH));
        try { compiler.compile(new CompilationUnit[]{new CompilationUnit(source.toCharArray(), unit, "UTF-8")}); }
        finally { environment.cleanup(); }
        return result;
    }
}
