/**
 *
 */

import { PerspectiveCamera as ThreePerspectiveCamera } from 'three';
import { assert } from "../core/assert.js";
import Vector1 from "../core/geom/Vector1.js";
import { Localization } from "../core/localization/Localization.js";
import { ModuleRegistry } from "../core/model/ModuleRegistry.js";
import ObservedBoolean from "../core/model/ObservedBoolean.js";
import ConcurrentExecutor from '../core/process/executor/ConcurrentExecutor.js';
import EmptyView from "../view/elements/EmptyView.js";
import { ViewStack } from "../view/elements/navigation/ViewStack.js";

import { AssetManager } from './asset/AssetManager.js';
import { AssetPreloader } from "./asset/preloader/AssetPreloader.js";
import { MetricCollection } from "./development/performance/MetricCollection.js";
import { MetricStatistics } from "./development/performance/MetricStatistics.js";
import { PeriodicConsolePrinter } from "./development/performance/monitor/PeriodicConsolePrinter.js";
import { EntityManager } from "./ecs/EntityManager.js";
import { BinarySerializationRegistry } from "./ecs/storage/binary/BinarySerializationRegistry.js";
import { GraphicsEngine } from './graphics/GraphicsEngine.js';
import KeyboardDevice from "./input/devices/KeyboardDevice.js";
import { PointerDevice } from "./input/devices/PointerDevice.js";
import { StaticKnowledgeDatabase } from "./knowledge/database/StaticKnowledgeDatabase.js";
import { logger } from "./logging/GlobalLogger.js";
import { OptionGroup } from "./options/OptionGroup.js";
import { EnginePluginManager } from "./plugin/EnginePluginManager.js";
import SceneManager from "./scene/SceneManager.js";
import Ticker from "./simulation/Ticker.js";
import SoundEngine from './sound/SoundEngine.js';

import GUIEngine from './ui/GUIEngine.js';


class EngineSettings {
    graphics_control_viewport_size = new ObservedBoolean(true);
    simulation_speed = new Vector1(1);
    input_mouse_sensitivity = new Vector1(5);
}

const METRIC_ID_FRAME = 'frame_delay';
const METRIC_ID_RENDER = 'render_time';
const METRIC_ID_SIMULATION = 'simulation_time';

class Engine {
    #last_frame_timestamp = 0;

    viewStack = new ViewStack();

    /**
     *
     * Static database
     * Can contain tables of structured data that the application often works with.
     * Engine startup will load all tables ensuring that data is ready once startup finishes
     * Use sparingly, this is not suitable for very large amounts of data as all the data will be in memory and deserialization will stall engine startup
     * Example: inventory item table, monster table
     * @readonly
     * @type {StaticKnowledgeDatabase}
     */
    staticKnowledge = new StaticKnowledgeDatabase();

    /**
     * @readonly
     * @type {EnginePluginManager}
     */
    plugins = new EnginePluginManager();

    /**
     * Main simulation ticker
     * @readonly
     * @type {Ticker}
     */
    ticker = new Ticker();

    /**
     * Default executor, enables time-sharing concurrency
     * @readonly
     * @type {ConcurrentExecutor}
     */
    executor = new ConcurrentExecutor(1, 10);

    /**
     *
     * @param {EnginePlatform} platform
     * @param {EntityManager} [entityManager]
     * @param {boolean} [enableGraphics]
     * @param {boolean} [enableAudio]
     * @param {boolean} [debug]
     * @constructor
     */
    constructor(platform, {
        entityManager,
        enableGraphics = true,
        enableAudio = true,
        debug = true
    } = {}) {
        assert.defined(platform, 'platform');

        /**
         *
         * @type {EnginePlatform}
         */
        this.platform = platform;

        this.staticKnowledge.validation_enabled = debug;


        /**
         *
         * @type {MetricCollection}
         */
        this.performance = new MetricCollection();


        this.__using_external_entity_manager = entityManager !== undefined;

        //setup entity component system
        if (this.__using_external_entity_manager) {
            // external entity manager provided
            this.entityManager = entityManager;
        } else {
            this.entityManager = new EntityManager();
        }


        /**
         *
         * @type {AssetManager<Engine>}
         */
        this.assetManager = new AssetManager({
            context: this,
            executor: this.executor
        });

        this.__performacne_monitor = new PeriodicConsolePrinter(15, () => {
            const metrics = this.performance;
            const stats = new MetricStatistics();

            // RENDER
            const m_render = metrics.get(METRIC_ID_RENDER);

            m_render.computeStats(stats);

            const v_render = (stats.mean * 1000).toFixed(2);

            // SIMULATION
            const m_sim = metrics.get(METRIC_ID_SIMULATION);

            m_sim.computeStats(stats);

            const v_sim = (stats.mean * 1000).toFixed(2);

            // FPS

            const m_frame = metrics.get(METRIC_ID_FRAME);

            m_frame.computeStats(stats);

            const v_fps = (1 / stats.mean).toFixed(2);


            // clear stats
            m_render.clear();
            m_sim.clear();
            m_frame.clear();

            return `FPS: ${v_fps}, RENDER: ${v_render}ms, SIMULATION: ${v_sim}ms`;
        });


        if (!this.__using_external_entity_manager) {
            // subscribe simulator to common ticker
            this.ticker.onTick.add(timeDelta => {
                const t0 = performance.now();
                this.entityManager.simulate(timeDelta);

                const t1 = performance.now();
                const duration = (t1 - t0) / 1000;

                // record frame time
                this.performance.get(METRIC_ID_SIMULATION).record(duration);
            });
        }

        // initialize performance metrics
        this.performance.create({
            name: METRIC_ID_RENDER
        });
        this.performance.create({
            name: METRIC_ID_SIMULATION
        });
        this.performance.create({
            name: METRIC_ID_FRAME
        });

        /**
         *
         * @type {OptionGroup}
         */
        this.options = new OptionGroup("options");

        /**
         *
         * @type {ModuleRegistry}
         */
        this.moduleRegistry = new ModuleRegistry();

        /**
         * @readonly
         * @type {BinarySerializationRegistry}
         */
        this.binarySerializationRegistry = new BinarySerializationRegistry();

        this.settings = new EngineSettings();


        /**
         * @deprecated use plugins instead
         */
        this.services = {};

        /**
         *
         * @type {Storage}
         */
        this.storage = this.platform.getStorage();


        this.localization = new Localization();
        this.localization.setAssetManager(this.assetManager);


        const innerWidth = window.innerWidth / 3;
        const innerHeight = window.innerHeight / 3;

        this.camera = new ThreePerspectiveCamera(45, innerWidth / innerHeight, 1, 50);


        if (enableGraphics !== false) {

            const graphicsEngine = new GraphicsEngine({
                camera: this.camera,
                debug
            });

            /**
             *
             * @type {GraphicsEngine}
             */
            this.graphics = graphicsEngine;

            try {
                graphicsEngine.start();
            } catch (e) {
                logger.error(`Failed to start GraphicEngine: ${e}`);
            }

        } else {
            logger.info('enableGraphics option is not set, no graphics engine will be created');
        }

        if (enableAudio !== false) {
            //sound engine
            const soundEngine = new SoundEngine();
            soundEngine.volume = 1;

            /**
             *
             * @type {SoundEngine}
             */
            this.sound = soundEngine;
        } else {
            logger.info('enableAudio option is not set, no audio engine will be created');
        }

        /**
         * Graphical User Interface engine
         * @type {GUIEngine}
         */
        this.gui = new GUIEngine();

        /**
         * @readonly
         * @type {SceneManager}
         */
        this.sceneManager = new SceneManager(this.entityManager, this.ticker.clock);

        this.initializeViews();

        this.devices = {
            pointer: new PointerDevice(this.viewStack.el),
            keyboard: new KeyboardDevice(this.viewStack.el)
        };

        //init level engine

        this.devices.pointer.start();
        this.devices.keyboard.start();

        //process settings
        this.initializeSettings();

        logger.info("engine initialized");

        /**
         * Toggles GraphicsEngine rendering on and off
         * @type {boolean}
         */
        this.renderingEnabled = true;


        this.plugins.initialize(this);
    }

    #initialize_audio() {
        const sound = this.sound;
        if (sound === undefined || sound === null) {
            // no sound engine
            return;
        }

        // hook up context resumption to input
        if (this.devices.pointer !== undefined) {

            this.devices.pointer.on.down.addOne(sound.resumeContext, sound);
            this.devices.keyboard.on.down.addOne(sound.resumeContext, sound);

        }
    }

    initializeViews() {

        const gameView = new EmptyView();

        gameView.addClass('game-view');

        gameView.css({
            left: 0,
            top: 0,
            position: "absolute",
            pointerEvents: "none"
        });
        this.gameView = gameView;

        this.viewStack.push(gameView, 'game-view');

        if (this.graphics !== undefined) {

            const viewport = this.graphics.viewport;

            viewport.css({
                pointerEvents: "auto"
            });

            gameView.addChild(viewport);
            //bind size of renderer viewport to game view
            viewport.bindSignal(gameView.size.onChanged, viewport.size.set.bind(viewport.size));
            gameView.on.linked.add(function () {
                viewport.size.copy(gameView.size);
            });

        }
    }

    initializeSettings() {
        logger.info('Initializing engine settings...');

        const engine = this;

        function setViewportToWindowSize() {
            // get parent element
            let parentElement = engine.viewStack.el.parentElement;

            while (parentElement !== null && parentElement.innerWidth === undefined) {
                // traverse up until we find an element with defined dimensions
                parentElement = parentElement.parentElement;
            }

            if (parentElement === null || parentElement === undefined) {
                // no parent element, default to window
                parentElement = window;
            }

            engine.viewStack.size.set(parentElement.innerWidth, parentElement.innerHeight);
        }

        this.settings.graphics_control_viewport_size.process(function (value) {
            if (value) {
                setViewportToWindowSize();
                window.addEventListener("resize", setViewportToWindowSize, false);
            } else {
                window.removeEventListener("resize", setViewportToWindowSize);
            }
        });

        logger.info('Engine settings initilized.');
    }

    benchmark() {
        const duration = 2000;
        let count = 0;
        const t0 = Date.now();
        let t1;
        while (true) {
            this.entityManager.simulate(0.0000000001);
            t1 = Date.now();
            if (t1 - t0 > duration) {
                break;
            }
            count++;
        }
        //normalize
        const elapsed = (t1 - t0) / 1000;
        const rate = (count / elapsed);
        return rate;
    }

    /**
     * Returns preloader object
     * @param {String} listURL
     */
    loadAssetList(listURL) {
        const preloader = new AssetPreloader();
        const assetManager = this.assetManager;
        assetManager.get({
            path: listURL, type: "json", callback: function (asset) {
                preloader.addAll(asset.create());
                preloader.load(assetManager);
            }
        });
        return preloader;
    }

    render() {
        assert.isInstanceOf(this, Engine, 'this', 'Engine');

        const graphics = this.graphics;

        if (graphics.autoDraw) {
            // request redraw
            graphics.needDraw = true;
        }

        if (graphics && this.renderingEnabled && graphics.needDraw) {

            const metrics = this.performance;

            const t0 = performance.now();

            graphics.render();

            const t1 = performance.now();
            const frame_time = (t1 - t0) / 1000;

            // record frame time
            metrics.get(METRIC_ID_RENDER).record(frame_time);
        }
    }

    /**
     * Startup
     * @returns {Promise}
     */
    start() {
        this.assetManager.startup();

        this.#initialize_audio();

        const promiseEntityManager = () => {
            return new Promise((resolve, reject) => {
                //initialize entity manager
                this.entityManager.startup(resolve, reject);
            });
        }

        this.__performacne_monitor.start();

        return Promise.all([
            this.sound.start()
                .then(promiseEntityManager),
            this.staticKnowledge.load(this.assetManager, this.executor),
            this.gui.startup(this),
            this.plugins.startup()
        ]).then(async () => {

            this.#last_frame_timestamp = performance.now();

            /**
             * Starting the engine
             */
            requestAnimationFrame(this.#animation_frame);

            //start simulation
            this.ticker.start({ maxTimeout: 200 });
            //self.uiController.init(self);

            //load options
            await this.options.attachToStorage('lazykitty.komrade.options', this.storage);

            logger.info('engine started');
        }, function (e) {
            logger.error(`Engine Failed to start: ${e}`);
        });


    }

    /**
     * Fired every animation frame via {@link requestAnimationFrame}
     */
    #animation_frame = () => {

        const t0 = this.#last_frame_timestamp;
        const t1 = performance.now();
        requestAnimationFrame(this.#animation_frame);
        this.render();

        const frame_delay = (t1 - t0) / 1000;

        // swap variables
        this.#last_frame_timestamp = t1;

        // record metric
        this.performance.get(METRIC_ID_FRAME).record(frame_delay);
    }

    /**
     * Shutdown the engine, disposing all relevant resources
     */
    async stop() {
        this.__performacne_monitor.stop();

        // stop the ticker
        this.ticker.pause();

        // shutdown entity manager
        if (!this.__using_external_entity_manager) {
            await new Promise((resolve, reject) => this.entityManager.shutdown(resolve, reject));
        }

        // shutdown plugins
        await this.plugins.shutdown();

        // stop gui
        await this.gui.shutdown();


        // TODO shutdown executor

        await this.assetManager.shutdown();
    }

    exit() {
        window.close();
    }

    /**
     * @returns {Promise}
     */
    requestExit() {
        return this.gui.confirmTextDialog({
            title: this.localization.getString('system_confirm_exit_to_system.title'),
            text: this.localization.getString('system_confirm_exit_to_system.text')
        }).then(() => {
            this.exit();
        });
    }
}


/**
 * @readonly
 * @type {boolean}
 */
Engine.prototype.isEngine = true;

export default Engine;
