/* eslint-disable no-underscore-dangle */
import { ASTMain, Input, Parser } from '@gobstones/gobstones-parser';
import { Linter } from '@gobstones/gobstones-parser';
import { SymbolTable } from '@gobstones/gobstones-parser';
import { Compiler } from './compiler';
import { RuntimePrimitives, RuntimeState } from './runtime';
import { VirtualMachine } from './vm';

import { UnknownPosition } from '@gobstones/gobstones-parser';
import { T_UPPERID, T_LOWERID, Token } from '@gobstones/gobstones-parser';
import {
    ASTDefProcedure,
    ASTDefFunction,
    ASTDefType,
    ASTStmtBlock,
    ASTConstructorDeclaration
} from '@gobstones/gobstones-parser';
import { Code } from './instruction';
import { Value } from './value';

/* This module is a façade for all the combined functionality of the
 * parser/compiler/vm
 */

const tok = (tag: symbol, value: string): Token =>
    new Token(tag, value, UnknownPosition, UnknownPosition);

export interface RunnerResult {
    result: Value;
    state: RuntimeState;
}

export class Runner {
    private _ast: ASTMain;
    private _primitives: RuntimePrimitives;
    private _symtable: SymbolTable;
    private _linter: Linter;
    private _code: Code;
    private _vm: VirtualMachine;
    private _result: any;

    public constructor() {
        this.initialize();
    }

    public initialize(): void {
        this._ast = undefined;
        this._primitives = new RuntimePrimitives();
        this._symtable = this._newSymtableWithPrimitives();
        this._linter = new Linter(this._symtable);
        this._code = undefined;
        this._vm = undefined;
        this._result = undefined;
    }

    /* Parse, compile, and run a program in the default global state
     * (typically an empty 9x9 board in Gobstones).
     * Return the return value of the program, ignoring the final state.
     * A GbsInterpreterException may be thrown.
     */
    public run(input: Input): Value {
        return this.runState(input, new RuntimeState()).result;
    }

    /* Parse, compile, and run a program in the given initial state.
     * Return an object of the form
     *   {'result': r, 'state': s]
     * where r is the result of the program and s is the final state.
     * A GbsInterpreterException may be thrown.
     */
    public runState(input: Input, initialState: RuntimeState): RunnerResult {
        this.parse(input);
        this.lint();
        this.compile();
        this.execute(initialState);
        return { result: this._result, state: this._vm.globalState() };
    }

    public parse(input: Input): void {
        const parser = new Parser(input);
        this._ast = parser.parse();

        for (const option of parser.getLanguageOptions()) {
            this._setLanguageOption(option);
        }
    }

    public enableLintCheck(linterCheckId: string, enabled: boolean): void {
        this._linter.enableCheck(linterCheckId, enabled);
    }

    public lint(): void {
        this._symtable = this._linter.lint(this._ast);
    }

    public compile(): void {
        this._code = new Compiler(this._symtable).compile(this._ast);
    }

    public initializeVirtualMachine(initialState: RuntimeState): void {
        this._vm = new VirtualMachine(this._code, initialState);
    }

    public execute(initialState: RuntimeState): void {
        this.executeWithTimeout(initialState, 0);
    }

    public executeWithTimeout(initialState: RuntimeState, millisecs: number): void {
        this.executeWithTimeoutTakingSnapshots(initialState, millisecs, undefined);
    }

    public executeWithTimeoutTakingSnapshots(
        initialState: RuntimeState,
        millisecs: number,
        // eslint-disable-next-line @typescript-eslint/ban-types
        snapshotCallback: Function
    ): void {
        this.initializeVirtualMachine(initialState);
        this._result = this._vm.runWithTimeoutTakingSnapshots(millisecs, snapshotCallback);
    }

    public executeEventWithTimeout(eventValue: Value, millisecs: number): void {
        this._result = this._vm.runEventWithTimeout(eventValue, millisecs);
    }

    public get abstractSyntaxTree(): ASTMain {
        return this._ast;
    }

    public get primitives(): RuntimePrimitives {
        return this._primitives;
    }

    public get symbolTable(): SymbolTable {
        return this._symtable;
    }

    public get virtualMachineCode(): Code {
        return this._code;
    }

    public get result(): Value {
        return this._result;
    }

    public get globalState(): RuntimeState {
        return this._vm.globalState();
    }

    /* Evaluate language options set by the LANGUAGE pragma */
    public _setLanguageOption(option: string): void {
        if (option === 'DestructuringForeach') {
            this.enableLintCheck('forbidden-extension-destructuring-foreach', false);
        } else if (option === 'AllowRecursion') {
            this.enableLintCheck('forbidden-extension-allow-recursion', false);
        } else {
            throw Error('Unknown language option: ' + option);
        }
    }

    /* Dynamic stack of regions */
    public regionStack(): string[] {
        return this._vm.regionStack();
    }

    /* Create a new symbol table, including definitions for all the primitive
     * types and operations (which come from RuntimePrimitives) */
    public _newSymtableWithPrimitives(): SymbolTable {
        const symtable = new SymbolTable();

        /* Populate symbol table with primitive types */
        for (const type of this._primitives.types()) {
            symtable.defType(this._astDefType(type));
        }

        /* Populate symbol table with primitive procedures */
        for (const procedureName of this._primitives.procedures()) {
            symtable.defProcedure(this._astDefProcedure(procedureName));
        }

        /* Populate symbol table with primitive functions */
        for (const functionName of this._primitives.functions()) {
            symtable.defFunction(this._astDefFunction(functionName));
        }

        return symtable;
    }

    public _astDefType(type: string): ASTDefType {
        const constructorDeclarations = [];
        for (const constructor of this._primitives.typeConstructors(type)) {
            constructorDeclarations.push(this._astConstructorDeclaration(type, constructor));
        }
        return new ASTDefType(tok(T_UPPERID, type), constructorDeclarations);
    }

    public _astDefProcedure(procedureName: string): ASTDefProcedure {
        const nargs = this._primitives.getOperation(procedureName).nargs();
        const parameters = [];
        for (let i = 1; i <= nargs; i++) {
            parameters.push(tok(T_LOWERID, 'x' + i.toString()));
        }
        return new ASTDefProcedure(tok(T_LOWERID, procedureName), parameters, new ASTStmtBlock([]));
    }

    public _astDefFunction(functionName: string): ASTDefFunction {
        const nargs = this._primitives.getOperation(functionName).nargs();
        const parameters = [];
        for (let i = 1; i <= nargs; i++) {
            parameters.push(tok(T_LOWERID, 'x' + i.toString()));
        }
        return new ASTDefFunction(tok(T_LOWERID, functionName), parameters, new ASTStmtBlock([]));
    }

    public _astConstructorDeclaration(
        type: string,
        constructor: string
    ): ASTConstructorDeclaration {
        const fields = [];
        for (const field of this._primitives.constructorFields(type, constructor)) {
            fields.push(tok(T_LOWERID, field));
        }
        return new ASTConstructorDeclaration(tok(T_UPPERID, constructor), fields);
    }
}
