package org.bdware.sc.compiler;

import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.DiagnosticErrorListener;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bdware.doip.audit.EndpointConfig;
import org.bdware.sc.engine.YJSFilter;
import org.bdware.sc.node.*;
import org.bdware.sc.parser.JavaScriptLexer;
import org.bdware.sc.parser.YJSParser;
import org.bdware.sc.parser.YJSParser.ProgramContext;
import org.bdware.sc.server.DoipClusterServer;
import org.bdware.sc.util.JsonUtil;
import org.bdware.sc.visitor.ContractReader;
import wrp.jdk.nashorn.internal.objects.Global;
import wrp.jdk.nashorn.internal.runtime.Context;
import wrp.jdk.nashorn.internal.runtime.ErrorManager;
import wrp.jdk.nashorn.internal.runtime.ScriptFunction;
import wrp.jdk.nashorn.internal.runtime.Source;
import wrp.jdk.nashorn.internal.runtime.options.Options;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

public class YJSCompiler {
    public boolean withProgramPointCount;
    YJSErrorListener errorListener = new YJSErrorListener();
    ContractNode contract;
    private static final Logger LOGGER = LogManager.getLogger(YJSCompiler.class);

    public YJSCompiler() {
    }

    public static ScriptFunction compileWithGlobal(Source source, Global global, Context context) {
        Global oldGlobal = Context.getGlobal();
        boolean globalChanged = (oldGlobal != global);
        try {
            if (globalChanged) {
                Context.setGlobal(global);
            }
            return context.compileScript(source).getFunction(global);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } finally {
            if (globalChanged) {
                Context.setGlobal(oldGlobal);
            }
        }
    }

    private static Context makeContext(
            final InputStream in, final OutputStream out, final OutputStream err) {
        final PrintStream pout =
                out instanceof PrintStream ? (PrintStream) out : new PrintStream(out);
        final PrintStream perr =
                err instanceof PrintStream ? (PrintStream) err : new PrintStream(err);
        final PrintWriter wout = new PrintWriter(pout, true);
        final PrintWriter werr = new PrintWriter(perr, true);

        // Set up error handler.
        final ErrorManager errors = new ErrorManager(werr);
        // Set up options.
        final Options options = new Options("nashorn", werr);
        options.process(new String[]{});
        // detect scripting mode by any source's first character being '#'
        options.set("persistent.code.cache", true);
        options.set("print.code", "true");
        options.set("print.parse", true);

        if (!options.getBoolean("scripting")) {
            for (final String fileName : options.getFiles()) {
                final File firstFile = new File(fileName);
                if (firstFile.isFile()) {
                    try (final FileReader fr = new FileReader(firstFile)) {
                        final int firstChar = fr.read();
                        // starts with '#
                        if (firstChar == '#') {
                            options.set("scripting", true);
                            break;
                        }
                    } catch (final IOException e) {
                        // ignore this. File IO errors will be reported later
                        // anyway
                    }
                }
            }
        }
        return new Context(
                options,
                errors,
                wout,
                werr,
                Thread.currentThread().getContextClassLoader(),
                new YJSFilter(),
                null);
    }

    public ContractZipBundle compile(ZipFile zf) throws Exception {
        ContractZipBundle czb = new ContractZipBundle();
        ZipEntry manifest = zf.getEntry("/manifest.json");
        if (null == manifest) {
            manifest = zf.getEntry("manifest.json");
            if (null == manifest) {
                throw new IllegalStateException("manifest.json is not exists!");
            }
        }
        InputStream manifestInput = zf.getInputStream(manifest);
        // Gson gson = new GsonBuilder().registerTypeAdapter(Contract.Type.class,
        // typeAdapter)

        ContractManifest cm =
                JsonUtil.GSON.fromJson(
                        new InputStreamReader(manifestInput), ContractManifest.class);

        // DOOP relevant logic
        ZipEntry routerConfig = zf.getEntry("/router.json");
        if (null == routerConfig) {
            routerConfig = zf.getEntry("router.json");
        }
        if(null != routerConfig) {
            InputStream routerInput = zf.getInputStream(routerConfig);
            EndpointConfig endpointConfig = JsonUtil.GSON.fromJson(new InputStreamReader(routerInput), EndpointConfig.class);
            DoipClusterServer.createDOOPServerInstance(endpointConfig);
        }

        // 如果没有就不限制，根据gas进行插装
        if (0L != cm.getInsnLimit()) {
            LOGGER.info("++++++++++++++++++++++true");
        }
        czb.setManifest(cm);
        Set<String> toParse = new HashSet<>();
        toParse.add(cm.main);
        Set<String> todo = new HashSet<>();
        while (toParse.size() > 0) {
            for (String str : toParse) {
                if (czb.containsPath(str)) {
                    continue;
                }
                ZipEntry entry = zf.getEntry("/" + str);
                if (null == entry) {
                    throw new IllegalStateException("missing import:" + str);
                }
                ContractNode cn = compile(zf.getInputStream(entry), str);
                czb.put(str, cn);
                for (ImportNode in : cn.getImports()) {
                    todo.add(in.getPath());
                }
            }
            toParse.clear();
            for (String str : todo) {
                if (!czb.containsPath(str)) {
                    toParse.add(str);
                }
            }
            todo.clear();
        }
        // add function _preSub
        // Kaidong Wu
        String preSubConName = cm.main.substring(0, cm.main.length() - 4) + "PreSub";
        String preSubContract =
                "contract "
                        + preSubConName
                        + " { function _preSub (e) { YancloudUtil.preSub(e.topic, e.content); }}";
        ContractNode preSubNode =
                compile(
                        new ByteArrayInputStream(preSubContract.getBytes(StandardCharsets.UTF_8)),
                        preSubConName + ".yjs");
        czb.put(preSubConName + ".yjs", preSubNode);
        LOGGER.info("--compile-- " + preSubConName);

        String globalBeanName = cm.main.substring(0, cm.main.length() - 4) + "GlobalBean";
        String globalBeanContract =
                "contract " + globalBeanName + "{ function setGlobal (_global) { Global = _global; }\n" +
                        " function getGlobal () { return Global; }}";
        czb.put(globalBeanName + ".yjs",
                compile(new ByteArrayInputStream(globalBeanContract.getBytes(StandardCharsets.UTF_8)),
                        globalBeanName + ".yjs"));
        LOGGER.info("--compile-- " + globalBeanName);

        return czb;
    }

    public ContractNode compile(InputStream input, String fileName) throws IOException {
        // 词法分析
        JavaScriptLexer lexer = new JavaScriptLexer(new ANTLRInputStream(input));
        lexer.setUseStrictDefault(true);
        CommonTokenStream cts = new CommonTokenStream(lexer);
        // 语法分析
        YJSParser parser = new YJSParser(cts);
        parser.removeErrorListeners();
        parser.addErrorListener(errorListener);
        parser.addErrorListener(new DiagnosticErrorListener());
        ProgramContext tree = parser.program();
        // 应该是antlr4访问器进行遍历语法树
        ContractReader reader = new ContractReader(fileName);
        contract = reader.visitProgram(tree);
        // 遍历完 获取 contract 里的 yjs type
        contract.initPlainText(cts);
        handleAnnotation(contract);//处理注解
        return contract;
    }

    private void handleAnnotation(ContractNode contractNode) {
        for (AnnotationNode node : contract.annotations) {
            AnnotationProcessor processor = findProcessor(node);
            if (processor != null) {
                processor.processContract(node, contractNode);
            }
        }
        for (FunctionNode functionNode : contractNode.getFunctions()) {
            List<AnnotationNode> annos = functionNode.annotations;//函数里的annotation
            if (annos != null)
                for (AnnotationNode anno : annos) {
                    AnnotationProcessor processor = findProcessor(anno);
                    if (processor != null)
                        processor.processFunction(anno, contractNode, functionNode);
                }
        }
    }

    private AnnotationProcessor findProcessor(AnnotationNode node) {
        try {
            String clzName = YJSCompiler.class.getPackage().getName();
            clzName += ".ap." + node.getType();
            Class<?> clz = Class.forName(clzName);
            return (AnnotationProcessor) clz.getConstructor().newInstance();
        } catch (Exception e) {
            // e.printStackTrace();
        }
        return null;
    }

    public List<String> syntaxError() {
        return errorListener.result;
    }
}
