/**
 * An object used to configure the Engine instance based on pandemonium export options, and to override those in custom HTML
 * templates if needed.
 *
 * @header Engine configuration
 * @summary The Engine configuration object. This is just a typedef, create it like a regular object, e.g.:
 *
 * ``const MyConfig = { executable: 'pandemonium', unloadAfterInit: false }``
 *
 * @typedef {Object} EngineConfig
 */
const EngineConfig = {}; // eslint-disable-line no-unused-vars

/**
 * @struct
 * @constructor
 * @ignore
 */
const InternalConfig = function(initConfig) { // eslint-disable-line no-unused-vars
    const cfg = /** @lends {InternalConfig.prototype} */ {
        /**
         * Whether the unload the engine automatically after the instance is initialized.
         *
         * @memberof EngineConfig
         * @default
         * @type {boolean}
         */
        unloadAfterInit: true,
        /**
         * The HTML DOM Canvas object to use.
         *
         * By default, the first canvas element in the document will be used is none is specified.
         *
         * @memberof EngineConfig
         * @default
         * @type {?HTMLCanvasElement}
         */
        canvas: null,
        /**
         * The name of the WASM file without the extension. (Set by Pandemonium Editor export process).
         *
         * @memberof EngineConfig
         * @default
         * @type {string}
         */
        executable: '',
        /**
         * An alternative name for the game pck to load. The executable name is used otherwise.
         *
         * @memberof EngineConfig
         * @default
         * @type {?string}
         */
        mainPack: null,
        /**
         * Specify a language code to select the proper localization for the game.
         *
         * The browser locale will be used if none is specified. See complete list of
         * :ref:`supported locales <doc_locales>`.
         *
         * @memberof EngineConfig
         * @type {?string}
         * @default
         */
        locale: null,
        /**
         * The canvas resize policy determines how the canvas should be resized by Pandemonium.
         *
         * ``0`` means Pandemonium won't do any resizing. This is useful if you want to control the canvas size from
         * javascript code in your template.
         *
         * ``1`` means Pandemonium will resize the canvas on start, and when changing window size via engine functions.
         *
         * ``2`` means Pandemonium will adapt the canvas size to match the whole browser window.
         *
         * @memberof EngineConfig
         * @type {number}
         * @default
         */
        canvasResizePolicy: 2,
        /**
         * The arguments to be passed as command line arguments on startup.
         *
         * See :ref:`command line tutorial <doc_command_line_tutorial>`.
         *
         * **Note**: :js:meth:`startGame <Engine.prototype.startGame>` will always add the ``--main-pack`` argument.
         *
         * @memberof EngineConfig
         * @type {Array<string>}
         * @default
         */
        args: [],
        /**
         * When enabled, the game canvas will automatically grab the focus when the engine starts.
         *
         * @memberof EngineConfig
         * @type {boolean}
         * @default
         */
        focusCanvas: true,
        /**
         * When enabled, this will turn on experimental virtual keyboard support on mobile.
         *
         * @memberof EngineConfig
         * @type {boolean}
         * @default
         */
        experimentalVK: false,
        /**
         * The progressive web app service worker to install.
         * @memberof EngineConfig
         * @default
         * @type {string}
         */
        serviceWorker: '',
        /**
         * @ignore
         * @type {Array.<string>}
         */
        persistentPaths: ['/userfs'],
        /**
         * @ignore
         * @type {boolean}
         */
        persistentDrops: false,
        /**
         * @ignore
         * @type {Array.<string>}
         */
        gdnativeLibs: [],
        /**
         * @ignore
         * @type {Array.<string>}
         */
        fileSizes: [],
        /**
         * A callback function for handling Pandemonium's ``OS.execute`` calls.
         *
         * This is for example used in the Web Editor template to switch between project manager and editor, and for running the game.
         *
         * @callback EngineConfig.onExecute
         * @param {string} path The path that Pandemonium's wants executed.
         * @param {Array.<string>} args The arguments of the "command" to execute.
         */
        /**
         * @ignore
         * @type {?function(string, Array.<string>)}
         */
        onExecute: null,
        /**
         * A callback function for being notified when the Pandemonium instance quits.
         *
         * **Note**: This function will not be called if the engine crashes or become unresponsive.
         *
         * @callback EngineConfig.onExit
         * @param {number} status_code The status code returned by Pandemonium on exit.
         */
        /**
         * @ignore
         * @type {?function(number)}
         */
        onExit: null,
        /**
         * A callback function for displaying download progress.
         *
         * The function is called once per frame while downloading files, so the usage of ``requestAnimationFrame()``
         * is not necessary.
         *
         * If the callback function receives a total amount of bytes as 0, this means that it is impossible to calculate.
         * Possible reasons include:
         *
         * -  Files are delivered with server-side chunked compression
         * -  Files are delivered with server-side compression on Chromium
         * -  Not all file downloads have started yet (usually on servers without multi-threading)
         *
         * @callback EngineConfig.onProgress
         * @param {number} current The current amount of downloaded bytes so far.
         * @param {number} total The total amount of bytes to be downloaded.
         */
        /**
         * @ignore
         * @type {?function(number, number)}
         */
        onProgress: null,
        /**
         * A callback function for handling the standard output stream. This method should usually only be used in debug pages.
         *
         * By default, ``console.log()`` is used.
         *
         * @callback EngineConfig.onPrint
         * @param {...*} [var_args] A variadic number of arguments to be printed.
         */
        /**
         * @ignore
         * @type {?function(...*)}
         */
        onPrint: function() {
            console.log.apply(console, Array.from(arguments)); // eslint-disable-line no-console
        },
        /**
         * A callback function for handling the standard error stream. This method should usually only be used in debug pages.
         *
         * By default, ``console.error()`` is used.
         *
         * @callback EngineConfig.onPrintError
         * @param {...*} [var_args] A variadic number of arguments to be printed as errors.
         */
        /**
         * @ignore
         * @type {?function(...*)}
         */
        onPrintError: function(var_args) {
            console.error.apply(console, Array.from(arguments)); // eslint-disable-line no-console
        },
    };

    /**
     * @ignore
     * @struct
     * @constructor
     * @param {EngineConfig} opts
     */
    function Config(opts) {
        this.update(opts);
    }

    Config.prototype = cfg;

    /**
     * @ignore
     * @param {EngineConfig} opts
     */
    Config.prototype.update = function(opts) {
        const config = opts || {};
        // NOTE: We must explicitly pass the default, accessing it via
        // the key will fail due to closure compiler renames.
        function parse(key, def) {
            if (typeof(config[key]) === 'undefined') {
                return def;
            }
            return config[key];
        }
        // Module config
        this.unloadAfterInit = parse('unloadAfterInit', this.unloadAfterInit);
        this.onPrintError = parse('onPrintError', this.onPrintError);
        this.onPrint = parse('onPrint', this.onPrint);
        this.onProgress = parse('onProgress', this.onProgress);

        // Pandemonium config
        this.canvas = parse('canvas', this.canvas);
        this.executable = parse('executable', this.executable);
        this.mainPack = parse('mainPack', this.mainPack);
        this.locale = parse('locale', this.locale);
        this.canvasResizePolicy = parse('canvasResizePolicy', this.canvasResizePolicy);
        this.persistentPaths = parse('persistentPaths', this.persistentPaths);
        this.persistentDrops = parse('persistentDrops', this.persistentDrops);
        this.experimentalVK = parse('experimentalVK', this.experimentalVK);
        this.focusCanvas = parse('focusCanvas', this.focusCanvas);
        this.serviceWorker = parse('serviceWorker', this.serviceWorker);
        this.gdnativeLibs = parse('gdnativeLibs', this.gdnativeLibs);
        this.fileSizes = parse('fileSizes', this.fileSizes);
        this.args = parse('args', this.args);
        this.onExecute = parse('onExecute', this.onExecute);
        this.onExit = parse('onExit', this.onExit);
    };

    /**
     * @ignore
     * @param {string} loadPath
     * @param {Response} response
     */
    Config.prototype.getModuleConfig = function(loadPath, response) {
        let r = response;
        return {
            'print': this.onPrint,
            'printErr': this.onPrintError,
            'thisProgram': this.executable,
            'noExitRuntime': true,
            'dynamicLibraries': [`${loadPath}.side.wasm`],
            'instantiateWasm': function(imports, onSuccess) {
                function done(result) {
                    onSuccess(result['instance'], result['module']);
                }
                if (typeof(WebAssembly.instantiateStreaming) !== 'undefined') {
                    WebAssembly.instantiateStreaming(Promise.resolve(r), imports).then(done);
                } else {
                    r.arrayBuffer().then(function(buffer) {
                        WebAssembly.instantiate(buffer, imports).then(done);
                    });
                }
                r = null;
                return {};
            },
            'locateFile': function(path) {
                if (path.endsWith('.worker.js')) {
                    return `${loadPath}.worker.js`;
                } else if (path.endsWith('.audio.worklet.js')) {
                    return `${loadPath}.audio.worklet.js`;
                } else if (path.endsWith('.js')) {
                    return `${loadPath}.js`;
                } else if (path.endsWith('.side.wasm')) {
                    return `${loadPath}.side.wasm`;
                } else if (path.endsWith('.wasm')) {
                    return `${loadPath}.wasm`;
                }
                return path;
            },
        };
    };

    /**
     * @ignore
     * @param {function()} cleanup
     */
    Config.prototype.getPandemoniumConfig = function(cleanup) {
        // Try to find a canvas
        if (!(this.canvas instanceof HTMLCanvasElement)) {
            const nodes = document.getElementsByTagName('canvas');
            if (nodes.length && nodes[0] instanceof HTMLCanvasElement) {
                this.canvas = nodes[0];
            }
            if (!this.canvas) {
                throw new Error('No canvas found in page');
            }
        }
        // Canvas can grab focus on click, or key events won't work.
        if (this.canvas.tabIndex < 0) {
            this.canvas.tabIndex = 0;
        }

        // Browser locale, or custom one if defined.
        let locale = this.locale;
        if (!locale) {
            locale = navigator.languages ? navigator.languages[0] : navigator.language;
            locale = locale.split('.')[0];
        }
        const onExit = this.onExit;

        // Pandemonium configuration.
        return {
            'canvas': this.canvas,
            'canvasResizePolicy': this.canvasResizePolicy,
            'locale': locale,
            'persistentDrops': this.persistentDrops,
            'virtualKeyboard': this.experimentalVK,
            'focusCanvas': this.focusCanvas,
            'onExecute': this.onExecute,
            'onExit': function(p_code) {
                cleanup(); // We always need to call the cleanup callback to free memory.
                if (typeof(onExit) === 'function') {
                    onExit(p_code);
                }
            },
        };
    };
    return new Config(initConfig);
};