package com.qihoo.qsql.codegen;

import com.github.picadoh.imc.compiler.InMemoryClassManager;
import com.github.picadoh.imc.compiler.InMemoryCompiler;
import com.github.picadoh.imc.loader.CompilationPackageLoader;
import com.github.picadoh.imc.model.CompilationPackage;
import com.github.picadoh.imc.model.CompilationUnit;
import com.github.picadoh.imc.model.JavaSourceFromString;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.qihoo.qsql.exec.Requirement;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.ToolProvider;
import com.qihoo.qsql.org.apache.calcite.util.Util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provide class header code, and methods which can compile it as a Class in memory.
 */
public abstract class ClassBodyWrapper {

    private static final Logger LOGGER = LoggerFactory.getLogger(ClassBodyComposer.class);
    private static final String CLASS_NAME_PREFIX = "Requirement";
    protected String className;
    protected ClassBodyComposer composer = new ClassBodyComposer(this.getClass());

    /**
     * ClassBodyWrapper constructor, which create a new Class name.
     */
    public ClassBodyWrapper() {
        Calendar calendar = Util.calendar();
        AtomicInteger classId = new AtomicInteger(
            calendar.get(Calendar.WEEK_OF_MONTH) * 10000
                + calendar.get(Calendar.HOUR_OF_DAY) * 1000
                + calendar.get(Calendar.MINUTE) * 100
                + calendar.get(Calendar.SECOND));

        this.className = CLASS_NAME_PREFIX + classId;
    }

    /**
     * For compiling class in memory.
     *
     * @param source a complete Java Class code
     * @param name Class name
     * @return Class instance in memory
     * @throws InMemoryCompiler.CompilerException When some error occurred, compilerException will be throw
     * @throws ClassNotFoundException When class is not found in jvm, a exception will be throw
     */
    public static Class compileSourceAndLoadClass(String source, String name, String extraJars)
        throws InMemoryCompiler.CompilerException,
        ClassNotFoundException {
        WithClassPathInMemoryCompiler compiler = new WithClassPathInMemoryCompiler();
        CompilationPackage compilationPackage = compiler.singleCompile(name, source, extraJars);
        CompilationPackageLoader loader = new CompilationPackageLoader();
        Map<String, Class<?>> classes = loader.loadAsMap(compilationPackage);
        if (! classes.containsKey(name)) {
            throw new RuntimeException("Compile or load class failed!!");
        }
        return classes.get(name);
    }

    @Override
    public String toString() {
        composer.handleComposition(ClassBodyComposer.CodeCategory.CLASS, className);
        return composer.getCompleteClass();
    }

    public String getClassName() {
        return className;
    }

    /**
     * Compile Java Code.
     *
     * @return Class
     */
    @SuppressWarnings("unchecked")
    public Class<? extends Requirement> compile() {
        composer.handleComposition(ClassBodyComposer.CodeCategory.CLASS, className);
        String code = composer.getCompleteClass();
        QueryGenerator.close();
        LOGGER.debug("The Java Code is {}", code);
        /*
          CAUTION!!
          compile and load class in memory may throw NoClassFoundException,
          reason for that is current dynamic compiler has a bug which will be
          trigger when class loaded has multiple level inner class.
         */
        try {
            return compileSourceAndLoadClass(code, className, "");
        } catch (InMemoryCompiler.CompilerException | ClassNotFoundException ex) {
            throw new RuntimeException("Compile failed!!", ex);
        }
    }

    private static class WithClassPathInMemoryCompiler extends InMemoryCompiler {

        JavaCompiler getSystemJavaCompiler() {
            return ToolProvider.getSystemJavaCompiler();
        }

        DiagnosticCollector<JavaFileObject> getDiagnosticCollector() {
            return new DiagnosticCollector<>();
        }

        InMemoryClassManager getClassManager(JavaCompiler compiler) {
            return new InMemoryClassManager(compiler.getStandardFileManager(null, null, null));
        }

        public CompilationPackage singleCompile(String className, String code, String extraJars)
            throws InMemoryCompiler.CompilerException {
            return this.compile(ImmutableMap.<String, String>builder().put(className, code).build(), extraJars);
        }

        public CompilationPackage compile(Map<String, String> classesToCompile, String extraJars)
            throws CompilerException {
            //modified classpath acquirement mode
            String belongingJarPath = ClassBodyWrapper.class.getProtectionDomain().getCodeSource()
                .getLocation()
                .getPath();

            List<String> options = Arrays
                .asList("-classpath", extraJars
                    + System.getProperty("path.separator")
                    + System.getProperty("java.class.path")
                    + System.getProperty("path.separator")
                    + belongingJarPath
                );

            List<JavaSourceFromString> strFiles = Lists.newArrayList();
            Iterator it = classesToCompile.keySet().iterator();

            String compilationReport;
            while (it.hasNext()) {
                String className = (String) it.next();
                compilationReport = classesToCompile.get(className);
                strFiles.add(new JavaSourceFromString(className, compilationReport));
            }

            JavaCompiler compiler = this.getSystemJavaCompiler();
            DiagnosticCollector<JavaFileObject> collector = this.getDiagnosticCollector();
            InMemoryClassManager manager = this.getClassManager(compiler);
            JavaCompiler.CompilationTask task = compiler.getTask(null, manager,
                collector, options, null, strFiles);
            boolean status = task.call();
            if (status) {
                List<CompilationUnit> compilationUnits = manager.getAllClasses();
                return new CompilationPackage(compilationUnits);
            } else {
                compilationReport = this.buildCompilationReport(collector, options);
                throw new CompilerException(compilationReport);
            }
        }

        String buildCompilationReport(DiagnosticCollector<JavaFileObject> collector,
            List<String> options) {
            int count = 0;
            StringBuilder resultBuilder = new StringBuilder();

            for (Object o : collector.getDiagnostics()) {
                Diagnostic<?> diagnostic = (Diagnostic) o;
                ++ count;
                JavaSourceFromString javaSource = (JavaSourceFromString) diagnostic.getSource();
                resultBuilder.append(javaSource.getCharContent(false)).append("\n");
                resultBuilder.append("Compiler options: ").append(options).append("\n\n");
                resultBuilder.append(diagnostic.getKind()).append("|").append(diagnostic.getCode())
                    .append("\n");
                resultBuilder.append("LINE:COLUMN ").append(diagnostic.getLineNumber()).append(":")
                    .append(diagnostic.getColumnNumber()).append("\n")
                    .append(diagnostic.getMessage(null)).append("\n\n");
            }

            String diagnosticString = resultBuilder.toString();
            String compilationErrorsOverview = count + " class(es) failed to compile";
            return "Compilation error\n" + compilationErrorsOverview + "\n" + diagnosticString;
        }
    }
}