package traceprinter.ramtools;

import javax.tools.*;
import java.io.*;
import java.util.*;
import javax.json.*;

public class CompileToBytes {

    public Writer compilerOutput = null; 
    // a Writer for additional output from the compiler; use System.err if null

    public DiagnosticListener<? super JavaFileObject> diagnosticListener = null;
    // a diagnostic (warning, etc) listener; if null use the compiler's default method for reporting diagnostics

    public Iterable<String> options = null;
    // args[] for javac, null means no options
        
    public Iterable<String> classesForAnnotation = null;
    // names of classes to be processed by annotation processing, null means no class names

    public Map<String, byte[]> bytecodes;
    // output variable: the class names and bytecodes generated by compiling
    
    private boolean used = false;

    public CompileToBytes() {}

    /***
        Compiles a single source file to bytecode.
        Returns null if compilation failed (same as JavaCompiler.getTask.call => false).
        Otherwise, returns bytecode for files defined as a result of compiling.
    ***/

    // takes a class name and its source code
    public Map<String, byte[]> compileFile(String className, String sourceCode) {
        return compileFiles(new String[][] {{className, sourceCode}});
    }

    public Map<String, byte[]> compileFiles(String[][] classCodePairs) {
        if (used) throw new RuntimeException("You already used this CompileToBytes.");
        used = true;
        
        ArrayList<RAMJavaFile> sourceFiles = new ArrayList<>();
        for (String[] pair : classCodePairs)
            sourceFiles.add(new RAMJavaFile(pair[0], pair[1]));
        
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

        RAMClassFileManager fileManager = new
            RAMClassFileManager(compiler
                                .getStandardFileManager(null, null, null));

        boolean result = compiler.getTask(compilerOutput, fileManager, diagnosticListener, 
                                          options, classesForAnnotation, sourceFiles).call();

        if (!result) return null;

        bytecodes = new TreeMap<>();
        for (Map.Entry<String, RAMClassFile> me : fileManager.contents.entrySet()) {
            bytecodes.put(me.getKey(), me.getValue().getBytes());
        }

        return bytecodes;
    }


    /* main() Method.

    standard input: A Json Object (UTF-8) 
    - whose keys are class names (optionally package qualified with . or /)
    - whose values are java source code

    Note that if there is additional data after the object, it will be silently ignored (javax.json doesn't seem to have a way to detect this)
   
    compiles these files (against the current classpath)

    output: A Json Object
    - status: "Internal Error", "Compile-Time Error", "Success"
    - if "Internal Error": errmsg: describing the internal error
    - if "Compile-Time Error": error (errmsg, filename, row, col, startpos, pos, endpos)
    - if "Success": bytecodes, [warning]
    
    bytecodes is a map from class names (possibly including . and $) to bytecodes

    */

    /* sample StdIn:

    {"Hello":"public class Hello{public static void main(String[]args){System.out.println(\"Hello\");}}","Multi":"public class Multi{}class Extra{}","Outer":"public class Outer{public class Inner{}}","pckg.Packed":"package pckg;public class Packed{}","Anon":"public class Anon{{new Anon(){void foo(){}};}}"}

    you can also use pckg/Packed or Packed for pckg.Packed

    for a compiler error:
    {"A":"public class A{{int x; x = 5.0;}}"}

    to generate a warning:
    {"Warn":"public class Warn {{ Class<Byte> x = (Class)Integer.class; }}"}

    */


    public static void main(String[] args) {
        InputStreamReader isr;
        try {
            isr = new InputStreamReader(System.in, "UTF-8");
        }
        catch (UnsupportedEncodingException e) {
            System.out.println(Json
                               .createObjectBuilder()
                               .add("status", "Internal Error")
                               .add("errmsg", "Could not set UTF-8 encoding")
                               .build());
            return;
        }

        String[][] pairs;
        try {
            JsonReader jr = Json.createReader(isr);
            JsonObject sourceFiles = jr.readObject();
            pairs = new String[sourceFiles.size()][2];
            int i = 0;
            for (Map.Entry<String, JsonValue> pair : sourceFiles.entrySet()) {
                pairs[i][0] = pair.getKey();
                if (! (pair.getValue() instanceof JsonString)) {
                    throw new RuntimeException("For key " + pair.getKey() +
                                               " value is a " +
                                               pair.getKey().getClass()+":\n"+
                                               pair.getKey().toString());
                }
                pairs[i][1] = ((JsonString)pair.getValue()).getString();
                i++;
            }
        }
        catch (Throwable t) {
            System.out.println(Json
                               .createObjectBuilder()
                               .add("status", "Internal Error")
                               .add("errmsg", "Could not parse input: " + t)
                               .build());
            return;            
        }
        
        CompileToBytes c2b = new CompileToBytes();

        c2b.compilerOutput = new StringWriter();
        c2b.options = Arrays.asList("-g -Xmaxerrs 1".split(" "));
        DiagnosticCollector<JavaFileObject> errorCollector = new DiagnosticCollector<>();
        c2b.diagnosticListener = errorCollector;

        Map<String, byte[]> classMap = c2b.compileFiles(pairs);

        JsonObject jerr = null;
        for (Diagnostic<? extends JavaFileObject> err : errorCollector.getDiagnostics()) {
            jerr = Json.createObjectBuilder()
                .add("filename", err.getSource().toString())
                .add("row", err.getLineNumber())
                .add("col", err.getColumnNumber())
                .add("errmsg", err.getMessage(null))
                .add("startpos", err.getStartPosition())
                .add("pos", err.getPosition())
                .add("endpos", err.getEndPosition())
                .build();
            if (err.getKind() == Diagnostic.Kind.ERROR) {
                System.out.println(Json
                                   .createObjectBuilder()
                                   .add("status", "Compile-time Error")
                                   .add("error", jerr)
                                   .build());
                return;
            }
        }

        if (classMap == null && jerr == null) {
            System.out.println(Json
                               .createObjectBuilder()
                               .add("status", "Internal Error")
                               .add("errmsg", "Did not compile, but gave no errors!")
                               .build());
            return;
        }
        
        JsonObjectBuilder classFiles = Json.createObjectBuilder();
        for (Map.Entry<String, byte[]> pair : classMap.entrySet()) {
            byte[] bytes = pair.getValue();
            char[] hexEncoding = new char[bytes.length*2];
            char[] hexArray = "0123456789ABCDEF".toCharArray();
            for (int i = 0; i < bytes.length; i++) {
                int v = bytes[i] & 0xFF;
                hexEncoding[i*2] = hexArray[v >>> 4];
                hexEncoding[i*2 + 1] = hexArray[v & 0x0F];
            }
            classFiles.add(pair.getKey(), new String(hexEncoding));
        }

        JsonObjectBuilder job = Json.createObjectBuilder();
        job.add("status", "Success")
            .add("bytecodes", classFiles.build());
        if (jerr != null) job.add("warning", jerr);
        System.out.println(job.build());
    }
}