import {editor} from "./cmEditor.js"
import {transform} from "@babel/standalone"
import {
    addToHamsterArray,
    getHamsterWithId,
    Hamster,
    maxHamsterError,
    hamsterNameAlreadyInUseError,
    error,
    hamsters, getHamster, hamster, setError, setMaxHamsterError, setHamsterNameAlreadyInUseError
} from "./hamster.js";
import {cols, drawAll, rows, territoryContent} from "./canvas.js";
import {
    disablePauseButton, disablePlayButton,
    disableStopButton, enablePauseButton,
    enablePlayButton,
    enableStopButton,
    resetControlButtons, sliderSpeed
} from "./buttons.js";
import {languagesStrings, userLanguage} from "./language.js";
import {errorDialogOpen, openErrorDialog} from "./dialogs.js";
import {saveTemporaryTerritory} from "./saveAndLoad.js";

let running = false;
let stopped = true;
let paused = false;
let myInterpreter;

/**
 * Stellt die Funktionen des Hamsters für den Interpreter zur Verfügung.
 * 
 * @param interpreter, für den JS-Code.
 * @param scope, globales Objekt des Interpreters.
 */
function initFunctionsForInterpreter(interpreter, scope) {
    // Definiert den Konstruktor der Hamster-Klasse für den Interpreter.
    let wrapperHamsterConstructor = function (row, column, direction, numberOfGrains) {
        let newHamster;
        let hamsterInterpreterObj = interpreter.createObject(interpreter.OBJECT)
        
        if (arguments.length === 0) {
            newHamster = new Hamster();
        } else if (arguments.length === 1 && (arguments[0] instanceof Interpreter.Object || arguments[0] instanceof Hamster)) {
            let tempHamsterObj = arguments[0];
            let hamsterParameter = getHamsterWithId(tempHamsterObj.hamsterId);
            newHamster = new Hamster(hamsterParameter);
        } else {
            newHamster = new Hamster(row, column, direction, numberOfGrains);
        }

        // Wird nur ausgeführt, falls beim Erzeugen des Hamster-Objekts kein Fehler auftritt.
        if (!error) {
            hamsterInterpreterObj.hamsterId = newHamster.getHamsterId();
            interpreter.setProperty(hamsterInterpreterObj, newHamster.getName(), newHamster)
            defineHamsterMethods(newHamster, hamsterInterpreterObj);
            addToHamsterArray(newHamster);
            drawAll();
        }

        return hamsterInterpreterObj;
    }
    interpreter.setProperty(scope, 'newHamster', interpreter.createNativeFunction(wrapperHamsterConstructor));

    let wrapperAddHamsterToArray = function (hamster) {
        return addToHamsterArray(hamster);
    }
    interpreter.setProperty(scope, 'addToHamsterArray', interpreter.createNativeFunction(wrapperAddHamsterToArray));

    let wrapperDrawAll = function () {
        return drawAll();
    }
    interpreter.setProperty(scope, 'drawAll', interpreter.createNativeFunction(wrapperDrawAll));

    
    // Default-Hamster-Objekt
    let myDefaultHamsterInterpreterObj = interpreter.createObject(interpreter.OBJECT);
    interpreter.setProperty(scope, 'Hamster', myDefaultHamsterInterpreterObj);
    interpreter.setProperty(scope, 'hamster', myDefaultHamsterInterpreterObj);

    myDefaultHamsterInterpreterObj.hamsterId = hamster.getHamsterId();
    interpreter.setProperty(myDefaultHamsterInterpreterObj, hamster.getName(), hamster);
    defineHamsterMethods(hamster, myDefaultHamsterInterpreterObj);
    
    let wrapperGetDefaultHamster = function () {
        return myDefaultHamsterInterpreterObj;
    }
    interpreter.setProperty(myDefaultHamsterInterpreterObj, 'getDefaultHamster', interpreter.createNativeFunction(wrapperGetDefaultHamster));

    let wrapperGetNumberOfHamsters = function () {
        return Hamster.getNumberOfHamster();
    }
    interpreter.setProperty(myDefaultHamsterInterpreterObj, 'getNumberOfHamster', interpreter.createNativeFunction(wrapperGetNumberOfHamsters));

    
    // Territorium
    let myTerritory = interpreter.createNativeFunction(territoryContent);
    interpreter.setProperty(scope, "Territory", myTerritory);

    let wrapperTerritoryGetNumberOfRows = function () {
        return territoryContent.length;
    }
    interpreter.setProperty(myTerritory, 'getNumberOfRows', interpreter.createNativeFunction(wrapperTerritoryGetNumberOfRows));

    let wrapperTerritoryGetNumberOfColumns = function () {
        return territoryContent[0].length;
    }
    interpreter.setProperty(myTerritory, 'getNumberOfColumns', interpreter.createNativeFunction(wrapperTerritoryGetNumberOfColumns));

    let wrapperTerritoryWall = function (row, column) {
        return row < 0 || row > rows || column < 0 || column > cols || territoryContent[row][column].hasWall();
    }
    interpreter.setProperty(myTerritory, 'wall', interpreter.createNativeFunction(wrapperTerritoryWall));

    let wrapperTerritoryGetNumberOfGrains = function (row, column) {
        if (row === undefined || column === undefined) {
            let numberOfGrains = 0;

            for (let r = 0; r < rows; r++) {
                for (let c = 0; c < cols; c++) {
                    numberOfGrains += territoryContent[r][c].getNumberOfGrains();
                }
            }

            return numberOfGrains;
        } else {
            if (row < 0 || row > rows || column < 0 || column > cols || territoryContent[row][column].hasWall()) {
                return 0;
            } else {
                return territoryContent[row][column].getNumberOfGrains();
            }
        }
    }
    interpreter.setProperty(myTerritory, 'getNumberOfGrains', interpreter.createNativeFunction(wrapperTerritoryGetNumberOfGrains));

    let wrapperTerritoryGetNumberOfHamsters = function (row, column) {
        if (row === undefined || column === undefined) {
            return Hamster.getNumberOfHamster();
        } else {
            if (row < 0 || row > rows || column < 0 || column > cols || territoryContent[row][column].hasWall()) {
                return 0;
            } else {
                return territoryContent[row][column].getNumberOfHamster();
            }
        }
    }
    interpreter.setProperty(myTerritory, 'getNumberOfHamster', interpreter.createNativeFunction(wrapperTerritoryGetNumberOfHamsters));

    let wrapperTerritoryGetHamster = function (row, column) {
        if (row === undefined || column === undefined) {
            return getHamster();
        } else {
            if (row < 0 || row > rows || column < 0 || column > cols || territoryContent[row][column].getNumberOfHamster() === 0) {
                return [];
            } else {
                return territoryContent[row][column].getHamster();
            }
        }
    }
    interpreter.setProperty(myTerritory, 'getHamster', interpreter.createNativeFunction(wrapperTerritoryGetHamster));
    
    
    // Konsole
    let myConsole = interpreter.createNativeFunction(window.console);
    interpreter.setProperty(scope, 'console', myConsole);
    
    let wrapperLog = function (text) {
        return console.log(text)
    }
    interpreter.setProperty(myConsole, 'log', interpreter.createNativeFunction(wrapperLog));
    
    
    // Hamsters-Array
    let myHamsters = interpreter.createNativeFunction(hamsters);
    interpreter.setProperty(scope, 'hamsters', myHamsters);
    
    let wrapperHamsters = function () {
        return getHamster();
    }
    interpreter.setProperty(scope, 'getHamster', interpreter.createNativeFunction(wrapperHamsters));

    /**
     * Definiert die Methoden der Hamster-Klasse für die Interpreter-Hamster-Objekte.
     * 
     * @param realHamster, das wirkliche Hamster-Objekt.
     * @param hamsterInterpreterObj, das zugeordnete Interpreter-Hamster-Objekt.
     */
    function defineHamsterMethods(realHamster, hamsterInterpreterObj) {
        let wrapperHamsterMove = function () {
            return realHamster.move();
        }
        interpreter.setProperty(hamsterInterpreterObj, 'move', interpreter.createNativeFunction(wrapperHamsterMove));

        let wrapperHamsterTurnLeft = function () {
            return realHamster.turnLeft();
        }
        interpreter.setProperty(hamsterInterpreterObj, 'turnLeft', interpreter.createNativeFunction(wrapperHamsterTurnLeft));

        let wrapperHamsterPickGrain = function () {
            return realHamster.pickGrain();
        }
        interpreter.setProperty(hamsterInterpreterObj, 'pickGrain', interpreter.createNativeFunction(wrapperHamsterPickGrain));

        let wrapperHamsterPutGrain = function () {
            return realHamster.putGrain();
        }
        interpreter.setProperty(hamsterInterpreterObj, 'putGrain', interpreter.createNativeFunction(wrapperHamsterPutGrain));

        let wrapperHamsterReadNumber = async function (text, callback) {
            let input = await realHamster.readNumber(text);
            callback(input);
        };

        interpreter.setProperty(hamsterInterpreterObj, 'readNumber', interpreter.createAsyncFunction(wrapperHamsterReadNumber));

        let wrapperHamsterReadString = async function (text, callback) {
            let input = await realHamster.readString(text);
            callback(input);
        }
        interpreter.setProperty(hamsterInterpreterObj, 'readString', interpreter.createAsyncFunction(wrapperHamsterReadString));

        let wrapperHamsterWrite = async function (text, callback) {
            callback(await realHamster.write(text));
        }
        interpreter.setProperty(hamsterInterpreterObj, 'write', interpreter.createAsyncFunction(wrapperHamsterWrite));

        let wrapperHamsterFrontIsClear = function () {
            return realHamster.frontIsClear();
        }
        interpreter.setProperty(hamsterInterpreterObj, 'frontIsClear', interpreter.createNativeFunction(wrapperHamsterFrontIsClear));

        let wrapperHamsterGrainAvailable = function () {
            return realHamster.grainAvailable();
        }
        interpreter.setProperty(hamsterInterpreterObj, 'grainAvailable', interpreter.createNativeFunction(wrapperHamsterGrainAvailable));

        let wrapperHamsterMouthEmpty = function () {
            return realHamster.mouthEmpty();
        }
        interpreter.setProperty(hamsterInterpreterObj, 'mouthEmpty', interpreter.createNativeFunction(wrapperHamsterMouthEmpty));

        let wrapperHamsterInit = function (row, column, direction, numberOfGrains) {
            return realHamster.init(row, column, direction, numberOfGrains);
        }
        interpreter.setProperty(hamsterInterpreterObj, 'init', interpreter.createNativeFunction(wrapperHamsterInit))

        let wrapperHamsterGetRow = function () {
            return realHamster.getRow();
        }
        interpreter.setProperty(hamsterInterpreterObj, 'getRow', interpreter.createNativeFunction(wrapperHamsterGetRow))

        let wrapperHamsterGetColumn = function () {
            return realHamster.getColumn();
        }
        interpreter.setProperty(hamsterInterpreterObj, 'getColumn', interpreter.createNativeFunction(wrapperHamsterGetColumn))

        let wrapperHamsterGetDirection = function () {
            return realHamster.getDirection();
        }
        interpreter.setProperty(hamsterInterpreterObj, 'getDirection', interpreter.createNativeFunction(wrapperHamsterGetDirection))

        let wrapperHamsterGetNumberOfGrains = function () {
            return realHamster.getNumberOfGrains();
        }
        interpreter.setProperty(hamsterInterpreterObj, 'getNumberOfGrains', interpreter.createNativeFunction(wrapperHamsterGetNumberOfGrains))
    }
}

/**
 * Führt den nächsten Schritt im Programm-Code innerhalb des Interpreters aus,
 * falls die Programm-Ausführung nicht pausiert oder gestoppt wurde.
 *
 * WICHTIG!!
 * Variablendeklarationen, Funktionsaufrufe, Schleifenprozeduren usw. werden vom Interpreter in mehrere Schritte unterteilt.
 * Dies bedeutet, dass beispielsweise "move();" mit einmaligen Ausführen von "interpreter.step();" nicht zum gewünschten
 * Ergebnis führt. Der Interpreter benötigt mehrere Schritte, um die gesamte Funktion abzuarbeiten.
 * Für weitere Details siehe: https://neil.fraser.name/software/JS-Interpreter/demos/line.html
 */
function nextStep() {
    if (!maxHamsterError && !hamsterNameAlreadyInUseError && !error) {
        if (!paused && !stopped) {
            try {
                if (myInterpreter.step()) {
                    window.setTimeout(nextStep, sliderSpeed);
                } else {
                    running = false;
                    enablePlayButton();
                    disablePauseButton();
                    disableStopButton();
                    window.setTimeout(resetControlButtons, 200);
                }
            } catch (err) {
                setError(true);
                stopCode();
                if (!errorDialogOpen) {
                    openErrorDialog(languagesStrings[userLanguage]['error-code-execution'] + err + ".");
                }
            }
        }
    } else {
        stopCode();
    }
}

/**
 * Formatiert den vom Nutzer eingegeben Programm-Code und übergibt ihm dem JS-Interpreter.
 */
function initInterpreterWithProgramCode() {
    let userInput = editor.state.doc.toString();
    let es5Code;
    try {
        // Formatiert den UserInput und konvertiert ihn von ES6 zu ES5.
        es5Code = transform(userInput, {'presets': ['es2015']}).code;
    } catch (err) {
        if (!errorDialogOpen) {
            openErrorDialog(languagesStrings[userLanguage]['error-code-transform'] + err + '.');
            return;
        }
    }

    try {
        es5Code = replaceHamsterDirection(es5Code);
        es5Code = replaceInstructions(es5Code);
    } catch (err) {
        if (!errorDialogOpen) {
            openErrorDialog(languagesStrings[userLanguage]['error-replace-instructions']);
        }
    }

    if (checkForHamsterBaseFunctionNames(es5Code)) {
        setError(true);
        if (!errorDialogOpen) {
            openErrorDialog(languagesStrings[userLanguage]['error-function-names']);
            return;
        }
    }

    myInterpreter = new Interpreter(es5Code, initFunctionsForInterpreter);
}

/**
 * Startet die Ausführung des Programmcodes aus der Benutzereingabe.
 */
export function runCode() {
    if (!running) {
        if (!paused) {
            setError(false);
            setMaxHamsterError(false);
            setHamsterNameAlreadyInUseError(false);
            saveTemporaryTerritory();
            initInterpreterWithProgramCode();
            stopped = false;
            running = true;
            disablePlayButton();
            enablePauseButton();
            enableStopButton();
            nextStep();
        } else {
            running = true;
            paused = false;
            disablePlayButton();
            enablePauseButton();
            enableStopButton();
            nextStep();
        }
    }
}

/**
 * Pausiert die Ausführung des Programmcodes aus der Benutzereingabe.
 */
export function pauseCode() {
    if (running) {
        running = false;
        paused = true;
        enablePlayButton();
        disablePauseButton();
        enableStopButton();
    }
}

/**
 * Stoppt die Ausführung des Programmcodes aus der Benutzereingabe.
 */
export function stopCode() {
    stopped = true;
    running = false;
    paused = false;
    enablePlayButton();
    disablePauseButton();
    disableStopButton();
    window.setTimeout(resetControlButtons, 200);
}

/**
 * Ersetzt die Funktionsaufrufe innerhalb der Benutzereingabe mit den tatsächlichen Namen der Funktionen.
 * Wandelt außerdem einfache Funktionsaufrufe in die Methodenaufrufe des Default-Hamster-Objekts um.
 *
 * @param code, die Benutzereingabe
 * @returns {string}, Code aus der Benutzereingabe mit ersetzten Funktionsnamen.
 */
function replaceInstructions(code) {
    let newCode = code.replaceAll("new Hamster", "newHamster");
    newCode = newCode.replaceAll("vornFrei", "frontIsClear");
    newCode = newCode.replaceAll("vor", "move");
    newCode = newCode.replaceAll("nimm", "pickGrain");
    newCode = newCode.replaceAll("gib", "putGrain");
    newCode = newCode.replaceAll("linksUm", "turnLeft");
    newCode = newCode.replaceAll("kornDa", "grainAvailable");
    newCode = newCode.replaceAll("maulLeer", "mouthEmpty");
    newCode = newCode.replaceAll("liesZahl", "readNumber");
    newCode = newCode.replaceAll("liesZeichenkette", "readString");
    newCode = newCode.replaceAll("schreib", "write");
    newCode = newCode.replaceAll("getReihe", "getRow");
    newCode = newCode.replaceAll("getSpalte", "getColumn");
    newCode = newCode.replaceAll("getBlickrichtung", "getDirection");
    newCode = newCode.replaceAll("getAnzahlKoerner", "getNumberOfGrains");
    newCode = newCode.replaceAll("getStandardHamster", "getDefaultHamster");
    newCode = newCode.replaceAll("getAnzahlHamster", "getNumberOfHamster");
    newCode = newCode.replaceAll("getAnzahlReihen", "getNumberOfRows");
    newCode = newCode.replaceAll("getAnzahlSpalten", "getNumberOfColumns");
    newCode = newCode.replaceAll("mauerDa", "wall");
    newCode = newCode.replaceAll("Territorium", "Territory");

    newCode = newCode.replace(/([^.]|^)move\(/g, '$1hamster.move(');
    newCode = newCode.replace(/([^.]|^)turnLeft\(/g, '$1hamster.turnLeft(');
    newCode = newCode.replace(/([^.]|^)pickGrain\(/g, '$1hamster.pickGrain(');
    newCode = newCode.replace(/([^.]|^)putGrain\(/g, '$1hamster.putGrain(');
    newCode = newCode.replace(/([^.]|^)frontIsClear\(/g, '$1hamster.frontIsClear(');
    newCode = newCode.replace(/([^.]|^)grainAvailable\(/g, '$1hamster.grainAvailable(');
    newCode = newCode.replace(/([^.]|^)mouthEmpty\(/g, '$1hamster.mouthEmpty(');
    newCode = newCode.replace(/([^.]|^)readNumber\(/g, '$1hamster.readNumber(');
    newCode = newCode.replace(/([^.]|^)readString\(/g, '$1hamster.readString(');
    newCode = newCode.replace(/([^.]|^)write\(/g, '$1hamster.write(');
    newCode = newCode.replace(/([^.]|^)getRow\(/g, '$1hamster.getRow(');
    newCode = newCode.replace(/([^.]|^)getColumn\(/g, '$1hamster.getColumn(');
    newCode = newCode.replace(/([^.]|^)getDirection\(/g, '$1hamster.getDirection(');
    newCode = newCode.replace(/([^.]|^)getNumberOfGrains\(/g, '$1hamster.getNumberOfGrains(');
    newCode = newCode.replace(/([^.]|^)init\(/g, '$1hamster.init(');

    return newCode;
}

/**
 * Ersetzt die Hamster-Blickrichtung-Konstanten innerhalb der Benutzereingabe mit den tatsächlichen Werten.
 *
 * @param code, die Benutzereingabe
 * @returns {string}, Code aus der Benutzereingabe mit ersetzten Werten.
 */
function replaceHamsterDirection(code) {
    let newCode = code.replaceAll("Hamster.NORD", "0");
    newCode = newCode.replaceAll("Hamster.OST", "1");
    newCode = newCode.replaceAll("Hamster.SUED", "2");
    newCode = newCode.replaceAll("Hamster.WEST", "3");
    newCode = newCode.replaceAll("Hamster.NORTH", "0");
    newCode = newCode.replaceAll("Hamster.EAST", "1");
    newCode = newCode.replaceAll("Hamster.SOUTH", "2");

    return newCode;
}

/**
 * Überprüft, ob sich im übergebenen Programm-Code eine Funktionsdefinition befindet, welche den gleichen Namen
 * wie die Basis-Hamster-Funktionen hat.
 *
 * @param code, welcher überprüft werden soll.
 * @return {boolean}: true, falls es eine Übereinstimmung gibt; sonst false.
 */
function checkForHamsterBaseFunctionNames(code) {
    // Definiere die regulären Ausdrücke für die einzelnen Funktionen
    const regexes = [
        /function\s+hamster.move\s*\(/,
        /function\s+hamster.turnLeft\s*\(/,
        /function\s+hamster.pickGrain\s*\(/,
        /function\s+hamster.putGrain\s*\(/,
        /function\s+hamster.frontIsClear\s*\(/,
        /function\s+hamster.grainAvailable\s*\(/,
        /function\s+hamster.mouthEmpty\s*\(/,
        /function\s+hamster.readNumber\s*\(/,
        /function\s+hamster.readString\s*\(/,
        /function\s+hamster.write\s*\(/,
        /function\s+hamster.getRow\s*\(/,
        /function\s+hamster.getColumn\s*\(/,
        /function\s+hamster.getDirection\s*\(/,
        /function\s+hamster.getNumberOfGrains\s*\(/,
        /function\s+hamster.init\s*\(/
    ];

    for (let i = 0; i < regexes.length; i++) {
        if (regexes[i].test(code)) {
            return true;
        }
    }

    return false;
}
