package electrosphere.script;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Engine;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Source;
import org.graalvm.polyglot.Source.Builder;
import org.graalvm.polyglot.Value;

import electrosphere.engine.Globals;
import electrosphere.engine.Main;
import electrosphere.logger.LoggerInterface;
import electrosphere.menu.tutorial.TutorialMenus;
import electrosphere.script.translation.JSServerUtils;
import electrosphere.util.FileUtils;
import electrosphere.util.math.MathUtils;

/**
 * Interface for executing scripts in the game engine
 */
public class ScriptEngine {

    //the default namespaces for 
    public static String SCRIPT_NAMESPACE_ENGINE = "engine"; //namespace for the engine functions exposed to the script engine
    public static String SCRIPT_NAMESPACE_SCRIPT = "script"; //namespace for the core typescript functionsw
    public static String SCRIPT_NAMESPACE_SCENE = "scene"; //namespace for the current scene

    /**
     * The id for firing signals globally
     */
    public static final int GLOBAL_SCENE = -1;

    //the graal context
    Context context;

    //used to build source objects
    Builder builder;

    //the map of script filepaths to parsed, in-memory scripts
    Map<String,Source> sourceMap;

    //the javascript object that stores values
    Value topLevelValue;

    //the object that contains all host values accessible to javascript land
    Value hostObject;

    //the engine object
    Value engineObject;

    /**
     * The hook manager
     */
    Value hookManager;

    //The files that are loaded on init to bootstrap the script engine
    static final String[] filesToLoadOnInit = new String[]{
        //polyfills
        "Scripts/compiler/require_polyfill.js",

        //main typescript engine
        "Scripts/compiler/typescript.js",

        //compiler and utilities
        "Scripts/compiler/file_resolution.js",
        "Scripts/compiler/compiler.js",
        "Scripts/compiler/host_access.js",
    };

    /**
     * List of files that are ignored when registering new files
     */
    static final String[] registerIgnores = new String[]{
        "/Scripts/compiler/host_access.ts",
    };

    //The classes that will be provided to the scripting engine
    //https://stackoverflow.com/a/65942034
    static final Object[][] staticClasses = new Object[][]{
        {"mathUtils",MathUtils.class},
        {"simulation",Main.class},
        {"tutorialUtils",TutorialMenus.class},
        {"serverUtils",JSServerUtils.class},
    };

    //singletons from the host that are provided to the javascript context
    static final Object[][] hostSingletops = new Object[][]{
        {"timekeeper",Globals.timekeeper},
        {"currentPlayer",Globals.clientPlayer},
        {"loggerScripts",LoggerInterface.loggerScripts},
    };

    /**
     * Initializes the engine
     */
    public void init(){
        //init datastructures
        sourceMap = new HashMap<String,Source>();

        //create engine with flag to disable warning
        Engine engine = Engine.newBuilder()
        .option("engine.WarnInterpreterOnly", "false")
        .build();

        //Create the rules for guest accessing the host environment
        HostAccess accessRules = HostAccess.newBuilder(HostAccess.EXPLICIT)
        .allowArrayAccess(true)
        .build();

        //create context
        context = Context.newBuilder("js")
        .allowNativeAccess(false)
        .allowHostAccess(accessRules)
        .engine(engine)
        .build();
        //save the js bindings object
        topLevelValue = context.getBindings("js");

        //put host members into environment
        putTopLevelValue("loggerScripts",LoggerInterface.loggerScripts);

        //load all files required to start the engine
        for(String fileToLoad : filesToLoadOnInit){
            loadDependency(fileToLoad);
        }

        //register engine files
        registerFile("/Scripts/engine/engine-init.ts");

        //compile
        compile();

        //run script for engine init
        requireModule("/Scripts/engine/engine-init.ts");

        //get the engine object
        engineObject = topLevelValue.getMember("REQUIRE_CACHE").getMember("/Scripts/engine/engine-init.js").getMember("exports").getMember("engine");
        hookManager = engineObject.getMember("hookManager");

        //define host members
        defineHostMembers();

        //init on script side
        invokeModuleFunction("/Scripts/engine/engine-init.ts","ENGINE_onInit");


        //call the engine initialization function
        // invokeFunction("ENGINE_onInit");


        //read scripts into source map
        // readScriptsDirectory("/src/main/sql", FileUtils.getAssetFile("/src/main/sql"));
        //create bindings
        // try {
        //     String content = FileUtils.getAssetFileAsString("/Scripts/test.js");
        //     Source source = Source.create("js", content);
        //     context.eval(source);
        //     System.out.println("Evaluated");
        // } catch (IOException e) {
        //     // TODO Auto-generated catch block
        //     e.printStackTrace();
        // }
    }

    /**
     * Stores a variable at the top level of the js bindings
     * @param valueName The name of the variable (ie the name of the variable)
     * @param value The value that is stored at that variable
     */
    public void putTopLevelValue(String valueName, Object value){
        topLevelValue.putMember(valueName, value);
    }

    /**
     * Gets a top level value from the script engine
     * @param valueName The name of the variable
     * @return The value of the variable
     */
    public Value getTopLevelValue(String valueName){
        return topLevelValue.getMember(valueName);
    }

    /**
     * Removes a top level member from the javascript context
     * @param valueName The name of the top level member
     * @return true if successfully removed, false otherwise
     */
    public boolean removeTopLevelValue(String valueName){
        return topLevelValue.removeMember(valueName);
    }

    @Deprecated
    /**
     * Reads the scripts directory
     * @param path The 
     * @param directory
     */
    void registerScriptDirectory(String path, File directory){
        if(directory.exists() && directory.isDirectory()){
            File[] children = directory.listFiles();
            for(File childFile : children){
                String qualifiedName = path + "/" + childFile.getName();
                if(childFile.isDirectory()){
                    registerScriptDirectory(qualifiedName,childFile);
                } else {
                    //add to source map
                    registerFile(qualifiedName);
                    // String content = FileUtils.readFileToString(childFile);
                    // sourceMap.put(qualifiedName,Source.create("js",content));
                }
            }
        }
    }

    /**
     * Loads a script from disk
     * @param path The path to the script file
     */
    void loadDependency(String path){
        String content;
        Source source = null;
        try {
            content = FileUtils.getAssetFileAsString(path);
            source = Source.create("js",content);
            sourceMap.put(path,source);
            context.eval(source);
        } catch (IOException e) {
            LoggerInterface.loggerScripts.ERROR("FAILED TO LOAD SCRIPT", e);
        } catch (PolyglotException e){
            if(source != null){
                LoggerInterface.loggerScripts.WARNING("Source language: " + source.getLanguage());
            }
            LoggerInterface.loggerScripts.ERROR("Script error", e);
            e.printStackTrace();
        }
    }

    /**
     * Prints the content of a file
     * @param path The filepath of the script
     */
    public void printScriptSource(String path){
        invokeMemberFunction("COMPILER", "printSource", path);
    }

    /**
     * Gets the contents of a file in the virtual filepath
     * @param path The virtual filepath
     * @return The contents of that file if it exists, null otherwise
     */
    public String getVirtualFileContent(String path){
        String rVal = null;
        Value compiler = this.topLevelValue.getMember("COMPILER");
        Value fileMap = compiler.getMember("fileMap");
        Value virtualFile = fileMap.getMember(path);
        rVal = virtualFile.getMember("content").asString();
        return rVal;
    }

    /**
     * Registers a file with the scripting engine to be compiled into the full binary
     * @param path The path to the script file
     */
    private boolean registerFile(String path){
        String content;
        try {
            content = FileUtils.getAssetFileAsString(path);
            sourceMap.put(path,Source.create("js",content));
            Value dependentFilesValue = invokeMemberFunction("COMPILER", "registerFile",path,content);
            //
            //register dependent files if necessary
            long dependentFilesCount = dependentFilesValue.getArraySize();
            if(dependentFilesCount > 0){
                for(int i = 0; i < dependentFilesCount; i++){
                    String dependentFilePath = dependentFilesValue.getArrayElement(i).asString();
                    boolean shouldRegister = true;
                    for(String ignorePath : registerIgnores){
                        if(ignorePath.equals(dependentFilePath)){
                            shouldRegister = false;
                        }
                    }
                    if(shouldRegister){
                        LoggerInterface.loggerScripts.INFO("[HOST - Script Engine] Should register file " + dependentFilePath);
                        registerFile(dependentFilePath);
                    } else {
                        LoggerInterface.loggerScripts.DEBUG("[HOST - Script Engine] Skipping ignorepath file " + dependentFilePath);
                    }
                }
            }
        } catch (IOException e) {
            LoggerInterface.loggerScripts.ERROR("FAILED TO LOAD SCRIPT", e);
            return false;
        }
        return true;
    }

    /**
     * Compiles the project
     */
    private void compile(){
        invokeMemberFunction("COMPILER", "run");
    }

    /**
     * Initializes a scene script
     * @param scenePath The scene's init script path
     * @return The id assigned to the scene instance from the script-side
     */
    public int initScene(String scenePath){
        //add files to virtual filesystem in script engine
        registerFile(scenePath);
        //load scene from javascript side
        Value sceneLoader = this.engineObject.getMember("sceneLoader");
        Value loadFunc = sceneLoader.getMember("loadScene");
        Value result = loadFunc.execute(scenePath);
        return result.asInt();
    }


    /**
     * Calls a function defined in the global scope with the arguments provided
     * @param functionName The function name
     * @param args The arguments
     */
    public Value invokeFunction(String functionName, Object... args){
        LoggerInterface.loggerScripts.DEBUG("Host execute: " + functionName);
        Value function = topLevelValue.getMember(functionName);
        if(function != null){
            return function.execute(args);
        } else {
            LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName);
        }
        return null;
    }

    /**
     * Calls a function on a child of the top level member
     * @param memberName The name of the child
     * @param functionName The name of the function
     * @param args The arguments for the function
     * @return The value from the function call
     */
    public Value invokeMemberFunction(String memberName, String functionName, Object ... args){
        LoggerInterface.loggerScripts.DEBUG("Host execute: " + functionName);
        Value childMember = topLevelValue.getMember(memberName);
        Value function = childMember.getMember(functionName);
        if(function != null){
            return function.execute(args);
        } else {
            LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName);
        }
        return null;
    }

    /**
     * Invokes a function defined in a file
     * @param filePath The file the function is defined in
     * @param functionName The function's name
     * @param args The args to pass into the function
     */
    public void invokeModuleFunction(String filePath, String functionName, Object ... args){
        Value filePathRaw = invokeFunction("FILE_RESOLUTION_getFilePath",filePath);
        Value requireCache = topLevelValue.getMember("REQUIRE_CACHE");
        Value module = requireCache.getMember(filePathRaw.asString());
        Value exports = module.getMember("exports");
        Value function = exports.getMember(functionName);
        if(function != null && function.canExecute()){
            function.execute(args);
        } else {
            LoggerInterface.loggerScripts.WARNING("Failed to invoke function " + functionName);
        }
    }

    /**
     * Requires a module into the global space
     * @param filePath The filepath of the module
     */
    public void requireModule(String filePath){
        invokeFunction("require", filePath);
    }

    /**
     * Defines host members within javascript context
     */
    private void defineHostMembers(){
        hostObject = topLevelValue.getMember("HOST_ACCESS");
        //give guest access to static classes
        Value classes = engineObject.getMember("classes");
        for(Object[] currentClass : staticClasses){
            classes.putMember((String)currentClass[0], currentClass[1]);
        }
        //give access to script engine instance
        hostObject.putMember("scriptEngine", this);
    }

    /**
     * Fires a signal on a given scene
     * @param signal The signal name
     * @param sceneInstanceId The script-side instanceid of the scene
     * @param args The arguments to accompany the signal invocation
     */
    public void fireSignal(String signal, int sceneInstanceId, Object ... args){
        Value fireSignal = this.hookManager.getMember("fireSignal");
        fireSignal.execute(sceneInstanceId,signal,args);
    }
    
    
}
