/*
 * Copyright © 2012-2021 by Data Virtuality GmbH
 */
package org.teiid.query.scripts.python;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Value;

import javax.script.*;
import java.io.IOException;
import java.io.Reader;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;

import static org.teiid.query.processor.relational.ObjectTableNode.*;

/**
 * @author Nicolas Golovan
 * @date 2021.09.05
 */
public class PythonScriptEngine extends AbstractScriptEngine
        implements ScriptEngine, Invocable, AutoCloseable {

    private final PythonScriptEngineFactory factory;

    private final Context polyglot;
    public static final String PYTHON_CORE_HOME_PATH = System.getProperty("jboss.server.base.dir") + "/lib/python/lib-graalpython";
    public static final String PYTHON_STD_LIB_PATH = System.getProperty("jboss.server.base.dir") + "/lib/python/lib-python/3";

    PythonScriptEngine(PythonScriptEngineFactory factory) {
        this.factory = factory;
        polyglot = Context.newBuilder("python")
                .allowAllAccess(true)
                .allowHostClassLoading(true)
                .allowNativeAccess(true)
                .allowHostAccess(true)
                .option("python.CoreHome", PYTHON_CORE_HOME_PATH)
                .option("python.StdLibHome", PYTHON_STD_LIB_PATH)
                .option("engine.WarnInterpreterOnly", "false")
                .build();
    }

    @Override
    public ScriptEngineFactory getFactory() {
        return factory;
    }

    @Override
    public Bindings createBindings() {
        return new PythonBindings();
    }

    @Override
    public Object eval(String script, ScriptContext context) throws ScriptException {
        try {
            String parameterisedScript = parameteriseScript(script, context);

            if (parameterisedScript.trim().isEmpty())
                return null;

            Object value = polyglot.eval("python", parameterisedScript).as(Object.class);
            return (value == null) ? "" : value;
        } catch (PolyglotException e) {
            throw new ScriptException(e);
        }
    }

    @Override
    public Object eval(Reader reader, ScriptContext context) throws ScriptException {
        return eval(readAllOfReader(reader), context);
    }

    @Override
    public Object eval(String script, Bindings bindings) throws ScriptException {
        return eval(script, context);
    }

    @Override
    public Object eval(Reader reader, Bindings bindings) throws ScriptException {
        return eval(readAllOfReader(reader), bindings);
    }

    @Override
    public Object invokeMethod(Object receiver, String name, Object... args) {
        return polyglot.asValue(receiver).getMember(name).execute(args).as(Object.class);
    }

    @Override
    public Object invokeFunction(String name, Object... args) {
        throw new UnsupportedOperationException();
    }

    @Override
    public <T> T getInterface(Class<T> interfaceClass) {
        throw new UnsupportedOperationException();
    }

    @Override
    public <T> T getInterface(Object receiver, Class<T> interfaceClass) {
        return polyglot.asValue(receiver).as(interfaceClass);
    }

    @Override
    public void close() {
        polyglot.close();
    }

    private String readAllOfReader(Reader reader) throws ScriptException {
        final StringBuilder stringBuilder = new StringBuilder(4096);

        final char[] buffer = new char[4096];

        while (true) {
            final int read;

            try {
                read = reader.read(buffer);
            } catch (IOException e) {
                throw new ScriptException(e);
            }

            if (read == -1) {
                break;
            }

            stringBuilder.append(buffer, 0, read);
        }

        return stringBuilder.toString();
    }

    private String parameteriseScript(String script, ScriptContext context) {
        setBindings(context.getBindings(ScriptContext.ENGINE_SCOPE), ScriptContext.ENGINE_SCOPE);

        Map<String, Object> bindings = new TreeMap<>(Collections.reverseOrder());
        bindings.putAll(getBindings(ScriptContext.ENGINE_SCOPE));

        for (Map.Entry<String, Object> binding : bindings.entrySet()) {
            String key = binding.getKey();
            Object value = binding.getValue();
            if (DV_ROW.equals(key)) {
                script = parameteriseDvRow(script, value);
                if (script.contains(DV_ROW)) {
                    return "";
                }
            } else if (DV_ROW_NUMBER.equals(key) ||
                    !"classloader".equals(key) && !key.startsWith("dv_") && !key.startsWith("javax")) {
                value = toPythonValue(value);
                script = script.replaceAll(key, String.valueOf(value));
            }
        }
        return script;
    }

    private String parameteriseDvRow(String script, Object value) {
        if (value instanceof Map) {
            Map<?, ?> bindings = (Map<?, ?>) value;
            for (Map.Entry<?, ?> binding : bindings.entrySet()) {
                String key = DV_ROW + "." + binding.getKey();
                script = replaceDvRowValue(script, key, binding.getValue());
            }
        } else if ("".equals(value) && script.contains(DV_ROW + ".")) {
            script = "";
        } else {
            script = replaceDvRowValue(script, DV_ROW, value);
        }
        return script;
    }

    private String replaceDvRowValue(String script, String key, Object value) {
        if ("".equals(value)) {
            script = script.replaceAll(key, "");
        } else {
            value = toPythonValue(value);
            script = script.replaceAll(key, String.valueOf(value));
        }
        return script;
    }

    private Object toPythonValue(Object value) {
        Value polyglotValue = Value.asValue(value);
        if (polyglotValue.isString()) {
            value = "'" + value + "'";
        } else if (polyglotValue.isBoolean()) {
            String strVal = String.valueOf(value);
            value = strVal.substring(0, 1).toUpperCase() + strVal.substring(1);
        }
        return value;
    }
}
