/*-
 * #%L
 * SciJava polyglot kernel for Jupyter.
 * %%
 * Copyright (C) 2017 Hadrien Mary
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
package org.scijava.jupyter.kernel.evaluator;

import com.twosigma.beakerx.autocomplete.AutocompleteResult;
import com.twosigma.beakerx.evaluator.Evaluator;
import com.twosigma.beakerx.jvm.object.SimpleEvaluationObject;
import com.twosigma.beakerx.kernel.Classpath;
import com.twosigma.beakerx.kernel.EvaluatorParameters;
import com.twosigma.beakerx.kernel.ImportPath;
import com.twosigma.beakerx.kernel.Imports;
import com.twosigma.beakerx.kernel.PathToJar;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;

import org.scijava.Context;
import org.scijava.log.LogService;
import org.scijava.plugin.Parameter;
import org.scijava.script.AutoCompleter;
import org.scijava.script.AutoCompletionResult;
import org.scijava.script.ScriptLanguage;
import org.scijava.script.ScriptService;
import org.scijava.thread.ThreadService;
import org.scijava.util.FileUtils;

/**
 *
 * @author Hadrien Mary
 */
public class ScijavaEvaluator implements Evaluator {

    public static final String DEFAULT_LANGUAGE = "groovy";

    @Parameter
    private LogService log;

    @Parameter
    private transient ScriptService scriptService;

    @Parameter
    private ThreadService threadService;

    @Parameter
    Context context;

    private final Map<String, ScriptEngine> scriptEngines;
    private final Map<String, ScriptLanguage> scriptLanguages;
    private final Map<String, AutoCompleter> completers;
    private String languageName;

    protected String shellId;
    protected String sessionId;

    public ScijavaEvaluator(Context context, String shellId, String sessionId) {
        context.inject(this);

        this.shellId = shellId;
        this.sessionId = sessionId;

        this.scriptEngines = new HashMap<>();
        this.scriptLanguages = new HashMap<>();

        this.completers = new HashMap<>();

        this.languageName = DEFAULT_LANGUAGE;
    }

    @Override
    public void setShellOptions(EvaluatorParameters kp) throws IOException {
        log.debug("Set shell options : " + kp);
    }

    @Override
    public AutocompleteResult autocomplete(String code, int index) {

        // Get only the line corresponding to the index.
        List<String> lines = Arrays.asList(code.substring(0, index).split("\n"));
        String line = lines.get(lines.size() - 1);

        // TODO: we need to find a way the language related to the current cell.
        // For now, we are just using the last used language.
        AutoCompleter completer = this.completers.get(this.languageName);
        ScriptEngine scriptEngine = this.scriptEngines.get(this.languageName);

        List<String> matches;
        int startIndex;
        if (completer != null) {
            AutoCompletionResult result = completer.autocomplete(line, index, scriptEngine);

            matches = result.getMatches();
            startIndex = index;

        } else {
            matches = new ArrayList<>();
            startIndex = 0;
        }

        // Reconstruct each matches with the correct index
        List<String> newMatches = new ArrayList<>();
        String newLine;
        for (String match : matches) {
            lines.set(lines.size() - 1, match);
            newLine = lines.stream().collect(Collectors.joining("\n"));
            newMatches.add(newLine.substring(startIndex, newLine.length()));
        }

        return new AutocompleteResult(newMatches, startIndex);
    }

    @Override
    public void killAllThreads() {
        log.debug("Kill All Threads");
        // Ugly and not working :-(
        System.exit(0);
    }

    @Override
    public void evaluate(SimpleEvaluationObject seo, String code) {

        code = this.setLanguage(code);

        Worker worker = new Worker(this.context, this.scriptEngines, this.scriptLanguages);
        worker.setup(seo, code, this.languageName);
        this.threadService.queue(getClass().getName(), worker);
    }

    @Override
    public void exit() {
        log.debug("Exiting DefaultEvaluator");
        // Ugly and not working :-(
        System.exit(0);
    }

    private void addLanguage(String langName) {

        if (scriptService.getLanguageByName(langName) == null) {
            log.error("Script Language for '" + langName + "' not found.");
            System.exit(1);
        }

        if (!this.scriptLanguages.keySet().contains(langName)) {

            Bindings bindings = null;
            if (!this.scriptEngines.isEmpty()) {
                String firstLanguage = this.scriptEngines.keySet().iterator().next();
                bindings = this.scriptEngines.get(firstLanguage).getBindings(ScriptContext.ENGINE_SCOPE);
            }

            log.info("Script Language for '" + langName + "' found.");
            ScriptLanguage scriptLanguage = scriptService.getLanguageByName(langName);
            this.scriptLanguages.put(langName, scriptLanguage);

            ScriptEngine engine = this.scriptLanguages.get(langName).getScriptEngine();
            this.scriptEngines.put(langName, engine);

            AutoCompleter completer = scriptLanguage.getAutoCompleter();
            this.completers.put(languageName, completer);

            // Not implemented yet
            //engine.setBindings(this.bindings, ScriptContext.ENGINE_SCOPE);
            if (bindings != null) {
                this.initBindings(bindings, engine, scriptLanguage);
            }

        }

        log.debug("Script Language found for '" + langName + "'");
    }

    private String setLanguage(String code) {

        if (code.startsWith("#!")) {

            // If code is composed of multiple lines
            if (code.split("\n").length > 1) {
                this.languageName = code.substring(2, code.indexOf("\n")).trim();

                // Return the code string without the first line
                code = code.substring(code.indexOf("\n") + 1);
            } // If only one line
            else {
                this.languageName = code.substring(2).trim();

                code = "";
            }

        }

        this.addLanguage(this.languageName);
        return code;
    }

    private void initBindings(Bindings bindings, ScriptEngine scriptEngine, ScriptLanguage scriptLanguage) {

        Bindings currentBindings = scriptEngine.getBindings(ScriptContext.ENGINE_SCOPE);
        bindings.keySet().forEach((String key) -> {
            currentBindings.put(key, scriptLanguage.decode(bindings.get(key)));
        });

    }

    @Override
    public Classpath getClasspath() {
        log.debug("addJarToClasspath()");
        return null;
    }

    @Override
    public Imports getImports() {
        log.debug("addJarToClasspath()");
        return null;
    }

    @Override
    public void addImport(ImportPath ip) {
        log.debug("addJarToClasspath()");
    }

    @Override
    public void resetEnvironment() {
        log.debug("addJarToClasspath()");
    }

    @Override
    public void removeImport(ImportPath ip) {
        log.debug("addJarToClasspath()");
    }

    @Override
    public List<Path> addJarsToClasspath(List<PathToJar> list) {
        log.debug("addJarsToClasspath()");
        return null;
    }

    @Override
    public boolean addJarToClasspath(PathToJar ptj) {
        log.debug("addJarToClasspath()");
        return true;
    }

    @Override
    public void cancelExecution() {
        log.debug("cancelExecution()");
    }

    @Override
    public Path getTempFolder() {
        log.debug("getTempFolder()");
        try {
            return FileUtils.createTemporaryDirectory("scijava-jupyter-kernel", null).toPath();
        }
        catch (final IOException exc) {
            throw new RuntimeException(exc);
        }
    }

    @Override
    public Class<?> loadClass(String clazzName) throws ClassNotFoundException {
        log.debug("loadClass()");
        final ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        final ClassLoader cl = ccl == null ? ClassLoader.getSystemClassLoader() : ccl;
        return cl.loadClass(clazzName);
    }

}