package jstest;

import common.Engine;
import common.EngineException;
import org.graalvm.polyglot.HostAccess;

import javax.script.*;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Path;

/**
 * JavaScript engine.
 *
 * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
 */
public final class JSEngine {
    public static final String OPTIONS = "--module-path=<js>/graal";
    public static Path JS_ROOT = Path.of(".");

    private final ScriptEngine engine;

    public JSEngine(final Path script) {
        try {
            System.setProperty("polyglot.engine.WarnInterpreterOnly", "false");
            System.setProperty("polyglot.js.strict", "true");

            final ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
//            engine = scriptEngineManager.getEngineFactories().stream()
//                    .filter(factory -> "Graal.js".equals(factory.getEngineName()))
//                    .map(ScriptEngineFactory::getScriptEngine)
//                    .findAny().orElse(null);
            engine = scriptEngineManager.getEngineByName("Graal.js");
            if (engine == null) {
                System.err.println("Graal.js not found");
                System.err.println("Use the following options to run tests:");
                System.err.println(OPTIONS);
                System.err.println("Where <js> - path to the javascript directory of this repository");
                System.err.println("Known engines:");
                for (final ScriptEngineFactory engineFactory : scriptEngineManager.getEngineFactories()) {
                    System.out.println("    " + engineFactory.getEngineName());
                }
                throw new AssertionError("Graal.js not found");
            }

//            engine.put("polyglot.js.ecmascript-version", "2024");
            engine.put("io", new IO(engine));
            engine.put("global", engine.getContext().getBindings(ScriptContext.ENGINE_SCOPE));

            engine.eval("var println = function() { io.println(Array.prototype.map.call(arguments, String).join(' ')); };");
            engine.eval("var print   = function() { io.print  (Array.prototype.map.call(arguments, String).join(' ')); };");
            engine.eval("var include = function(file) { io.include(file); }");
            engine.eval("var expr;");
        } catch (final ScriptException e) {
            throw new EngineException("Invalid initialization", e);
        }

        try {
            include(script.toString());
        } catch (final ScriptException e) {
            throw new EngineException("Script error", e);
        }
    }

    private void include(final String script) throws ScriptException {
        final Path scriptPath = JS_ROOT.resolve(script);
        try (final Reader reader = Files.newBufferedReader(scriptPath)) {
            engine.eval(reader);
        } catch (final IOException e) {
            throw new EngineException("Script '%s' not found".formatted(scriptPath), e);
        }
    }

    public  <T> Engine.Result<T> eval(final String context, final String code, final Class<T> token) {
        try {
            final Object result = engine.eval(code);
            if (result == null) {
                throw new EngineException("Result is null", null);
            }
            if (token.isAssignableFrom(result.getClass())) {
                return new Engine.Result<>(context, token.cast(result));
            }
            throw new EngineException("Expected %s, found \"%s\" (%s)%s".formatted(
                    token.getSimpleName(),
                    result,
                    result.getClass().getSimpleName(),
                    context
            ), null);
        } catch (final ScriptException e) {
            throw new EngineException("No error expected in " + context + ": " + e.getMessage(), e);
        }
    }

    public void set(final String variable, final Engine.Result<?> value) {
        engine.getBindings(ScriptContext.ENGINE_SCOPE).put(variable, value.value());
    }

    public class IO {
        private final ScriptEngine engine;

        public IO(final ScriptEngine engine) {
            this.engine = engine;
        }

        @HostAccess.Export
        public void print(final String message) {
            System.out.print(message);
        }

        @HostAccess.Export
        public void println(final String message) {
            System.out.println(message);
        }

        @HostAccess.Export
        public void include(final String file) throws ScriptException {
            JSEngine.this.include(file);
        }

        @HostAccess.Export
        public void declare(final String name, final Object value) {
            engine.put(name, value);
        }
    }
}
