import LabelView from "../../common/LabelView.js";
import EmptyView from "../../elements/EmptyView.js";
import { TooltipParser } from "./TooltipParser.js";
import { TooltipTokenType } from "./parser/TooltipTokenType.js";


class GMLContextFrame {
    constructor() {
        this.enableTooltips = true;
    }
}

const RECURSION_LIMIT = 50;

/**
 * Game Markup Language
 */
export class GMLEngine {

    constructor() {

        /**
         *
         * @type {StaticKnowledgeDatabase}
         */
        this.database = null;
        /**
         *
         * @type {Localization}
         */
        this.localization = null;

        /**
         *
         * @type {DomTooltipManager}
         */
        this.tooltips = null;

        /**
         * Used to prevent infinite recursion
         * @type {number}
         * @private
         */
        this.__recursionCount = 0;

        /**
         *
         * @type {Array}
         * @private
         */
        this.__recursionReferencePath = [];

        /**
         *
         * @type {GMLContextFrame[]}
         */
        this.__contextStack = [];

        /**
         *
         * @type {boolean}
         * @private
         */
        this.__tooltipsEnabled = true;

        /**
         *
         * @type {Map<string, GMLReferenceCompiler>}
         * @private
         */
        this.__reference_compilers_visual = new Map();

        /**
         *
         * @type {Map<string, GMLReferenceCompiler>}
         * @private
         */
        this.__reference_compilers_text = new Map();


        /**
         *
         * @type {TooltipParser}
         * @private
         */
        this.__parser = new TooltipParser();
    }


    /**
     *
     * @param {string} key
     * @param {GMLReferenceCompiler} compiler
     * @returns {boolean}
     */
    addReferenceCompilerVisual(key, compiler) {
        if (this.__reference_compilers_visual.has(key)) {
            return false;
        }

        this.__reference_compilers_visual.set(key, compiler);

        return true;
    }

    /**
     *
     * @param {string} key
     * @returns {boolean}
     */
    removeReferenceCompilerVisual(key) {
        if (!this.__reference_compilers_visual.has(key)) {
            return false;
        }

        this.__reference_compilers_visual.delete(key);

        return true;
    }

    /**
     *
     * @param {string} key
     * @param {GMLReferenceCompiler} compiler
     * @returns {boolean}
     */
    addReferenceCompilerText(key, compiler) {
        if (this.__reference_compilers_text.has(key)) {
            return false;
        }

        this.__reference_compilers_text.set(key, compiler);

        return true;
    }

    /**
     *
     * @param {string} key
     * @returns {boolean}
     */
    removeReferenceCompilerText(key) {
        if (!this.__reference_compilers_text.has(key)) {
            return false;
        }

        this.__reference_compilers_text.delete(key);

        return true;
    }

    /**
     *
     * @returns {boolean}
     */
    getTooltipsEnabled() {
        return this.__tooltipsEnabled;
    }

    /**
     *
     * @param {boolean} v
     */
    setTooltipsEnabled(v) {
        this.__tooltipsEnabled = v;
    }

    /**
     * @param {StaticKnowledgeDatabase} database
     * @param {Localization} localization
     */
    initialize(database, localization) {

        /**
         *
         * @type {StaticKnowledgeDatabase}
         */
        this.database = database;
        this.localization = localization;
    }

    /**
     *
     * @returns {Promise<any>}
     */
    startup() {
        const self = this;

        return new Promise(function (resolve, reject) {
            if (self.database === null) {
                throw new Error('Database not set; probably not initialized');
            }

            self.database.promise().then(resolve, reject);
        });
    }

    /**
     *
     * @param {String} type
     * @returns {number}
     */
    getReferenceDepth(type) {
        let result = 0;

        const path = this.__recursionReferencePath;

        const pathLength = path.length;

        for (let i = 0; i < pathLength; i++) {
            const r = path[i];
            if (r.type === type) {
                result++;
            }
        }

        return result;
    }

    /**
     * @private
     * @param {Token[]} tokens
     * @returns {string}
     */
    compileTokensToText(tokens) {
        const database = this.database;
        const localization = this.localization;
        const tooltips = this.tooltips;

        const gml = this;

        /**
         *
         * @param {Token} token
         */
        function compileTextToken(token) {
            return token.value;
        }

        function compileReferenceToken(token) {
            /**
             * @type {TooltipReferenceValue}
             */
            const reference = token.value;

            const refType = reference.type.toLocaleLowerCase();

            const referenceCompiler = gml.__reference_compilers_text.get(refType);

            if (referenceCompiler === undefined) {
                //unknown reference type
                console.error(`unknown reference type '${refType}'`);
                return;
            }

            let result;

            gml.__recursionReferencePath.push(reference);

            try {
                result = referenceCompiler.compile(reference.values, database, localization, gml, tooltips);
            } catch (e) {
                console.error(`Failed to compile reference token`, token, 'ERROR:', e, 'token stream:', tokens);
                result = 'ERROR';
            }

            gml.__recursionReferencePath.pop();

            return result;
        }

        const result = tokens.map(t => {
            let v;
            const tokenType = t.type;

            if (tokenType === TooltipTokenType.Reference) {
                v = compileReferenceToken(t);
            } else if (tokenType === TooltipTokenType.Text) {
                v = compileTextToken(t);
            } else if (tokenType === TooltipTokenType.StyleStart) {
                //do nothing
            } else if (tokenType === TooltipTokenType.StyleEnd) {
                //do nothing
            } else {
                throw new TypeError(`Unsupported token type '${tokenType}'`);
            }

            return v;
        })
            .filter(v => v !== undefined)
            .join('');

        return result;
    }

    /**
     *
     * @param {Token[]} tokens
     * @param {View} [target]
     * @returns {View}
     * @private
     */
    compileTokensToVisual(tokens, target = new EmptyView({ tag: 'span' })) {
        const database = this.database;
        const localization = this.localization;
        const tooltips = this.tooltips;

        const gml = this;

        /**
         *
         * @param {Token} token
         * @param {View[]} result
         */
        function compileTextToken(token, result) {

            /**
             * @type {string}
             */
            const tokenValue = token.value;

            const view = new LabelView(tokenValue, { tag: 'span' });

            result.push(view);

        }

        /**
         *
         * @param {Token} token
         * @param {View[]} result
         */
        function compileReferenceToken(token, result) {
            /**
             * @type {TooltipReferenceValue}
             */
            const reference = token.value;

            const refType = reference.type.toLocaleLowerCase();

            const referenceCompiler = gml.__reference_compilers_visual.get(refType);

            if (referenceCompiler === undefined) {
                //unknown reference type
                console.error(`No compiler registered for reference type '${refType}'`);
                return;
            }

            let view;

            gml.__recursionReferencePath.push(reference);

            try {
                view = referenceCompiler.compile(reference.values, database, localization, gml, tooltips);
            } catch (e) {
                console.error(`Failed to compile reference token`, token, 'ERROR:', e, 'token stream:', tokens);
                view = new LabelView('ERROR');
            }

            gml.__recursionReferencePath.pop();

            view.addClass('reference-type-' + refType);

            result.push(view);
        }


        //style stack
        const styleSet = [];


        let containerElement = target;

        function makeStyleContainer() {
            const view = new EmptyView({ tag: 'span' });

            styleSet.forEach(n => view.addClass(n));

            return view;
        }

        function pushStyle(name) {
            styleSet.push(name);

            const el = makeStyleContainer();

            target.addChild(el);

            containerElement = el;
        }

        function popStyle(name) {
            const i = styleSet.indexOf(name);

            if (i === -1) {
                console.error(`encountered closing token for a style(name=${name}) that is not open. Current style set: [${styleSet}]`);
                //bail
                return;
            }

            styleSet.splice(i, 1);

            if (styleSet.length === 0) {
                containerElement = target;
            } else {
                //close current container and start a new one
                const el = makeStyleContainer();

                target.addChild(el);

                containerElement = el;
            }
        }

        const tokenCount = tokens.length;

        for (let i = 0; i < tokenCount; i++) {
            let t = tokens[i];

            /**
             * @type {View[]}
             */
            const childViews = [];

            const tokenType = t.type;

            if (tokenType === TooltipTokenType.Reference) {
                compileReferenceToken(t, childViews);
            } else if (tokenType === TooltipTokenType.Text) {
                compileTextToken(t, childViews);
            } else if (tokenType === TooltipTokenType.StyleStart) {
                pushStyle(t.value);
            } else if (tokenType === TooltipTokenType.StyleEnd) {
                popStyle(t.value);
            } else {
                throw new TypeError(`Unsupported token type '${tokenType}'`);
            }

            const nChildren = childViews.length;

            for (let j = 0; j < nChildren; j++) {
                const childView = childViews[j];

                containerElement.addChild(childView);
            }

        }

        return target;
    }

    pushState() {
        const frame = new GMLContextFrame();

        frame.enableTooltips = this.__tooltipsEnabled;

        this.__contextStack.push(frame);
    }

    popState() {
        const frame = this.__contextStack.pop();

        this.__tooltipsEnabled = frame.enableTooltips;
    }

    /**
     *
     * @param {string} code
     * @return {string}
     */
    compileAsText(code) {
        if (this.__recursionCount >= RECURSION_LIMIT) {
            console.error(`Hit recursion limit(=${RECURSION_LIMIT}), returning empty view`);
            return "";
        }

        this.__recursionCount++;

        this.pushState();

        try {
            const tokens = this.__parser.parse(code);

            const result = this.compileTokensToText(tokens);

            return result;
        } finally {

            //restore frame
            this.popState();

            this.__recursionCount--;
        }
    }

    /**
     *
     * @param {string} code
     * @param {View} [target]
     * @returns {View}
     */
    compile(code, target) {
        if (this.__recursionCount >= RECURSION_LIMIT) {
            console.error(`Hit recursion limit(=${RECURSION_LIMIT}), returning empty view`);
            return new EmptyView();
        } else {

            this.__recursionCount++;

            this.pushState();

            try {
                const tokens = this.__parser.parse(code);

                const view = this.compileTokensToVisual(tokens, target);

                return view;
            } finally {

                //restore frame
                this.popState();

                this.__recursionCount--;

            }

        }
    }

    /**
     *
     * @param {string} key
     * @param {View} [target]
     * @param {Object} [seed]
     * @returns {View}
     */
    compile_localized(key, target, seed) {
        const text = this.localization.getString(key, seed);

        return this.compile(text, target);
    }

}
