import { SortedLoopArray } from '../../../core/sorted-loop-array.js';

import { ComponentSystem } from '../system.js';

import { ScriptComponent } from './component.js';
import { ScriptComponentData } from './data.js';

/** @typedef {import('../../app-base.js').Application} Application */

const METHOD_INITIALIZE_ATTRIBUTES = '_onInitializeAttributes';
const METHOD_INITIALIZE = '_onInitialize';
const METHOD_POST_INITIALIZE = '_onPostInitialize';
const METHOD_UPDATE = '_onUpdate';
const METHOD_POST_UPDATE = '_onPostUpdate';

// Ever-increasing integer used as the execution order of new script components. We are using an
// ever-increasing number and not the order of the script component in the components array because
// if we ever remove components from the array, we would have to re-calculate the execution order
// for all subsequent script components in the array every time, which would be slow.
let executionOrderCounter = 0;

/**
 * Allows scripts to be attached to an Entity and executed.
 *
 * @augments ComponentSystem
 */
class ScriptComponentSystem extends ComponentSystem {
    /**
     * Create a new ScriptComponentSystem.
     *
     * @param {Application} app - The application.
     */
    constructor(app) {
        super(app);

        this.id = 'script';

        this.ComponentType = ScriptComponent;
        this.DataType = ScriptComponentData;

        // list of all entities script components
        // we are using pc.SortedLoopArray because it is
        // safe to modify while looping through it
        this._components = new SortedLoopArray({
            sortBy: '_executionOrder'
        });

        // holds all the enabled script components
        // (whose entities are also enabled). We are using pc.SortedLoopArray
        // because it is safe to modify while looping through it. This array often
        // change during update and postUpdate loops as entities and components get
        // enabled or disabled
        this._enabledComponents = new SortedLoopArray({
            sortBy: '_executionOrder'
        });


        // if true then we are currently preloading scripts
        this.preloading = true;

        this.on('beforeremove', this._onBeforeRemove, this);
        this.app.systems.on('initialize', this._onInitialize, this);
        this.app.systems.on('postInitialize', this._onPostInitialize, this);
        this.app.systems.on('update', this._onUpdate, this);
        this.app.systems.on('postUpdate', this._onPostUpdate, this);
    }

    initializeComponentData(component, data) {
        // Set execution order to an ever-increasing number
        // and add to the end of the components array.
        component._executionOrder = executionOrderCounter++;
        this._components.append(component);

        // check we don't overflow executionOrderCounter
        if (executionOrderCounter > Number.MAX_SAFE_INTEGER) {
            this._resetExecutionOrder();
        }

        component.enabled = data.hasOwnProperty('enabled') ? !!data.enabled : true;
        // if enabled then add this component to the end of the enabledComponents array
        // Note, we should be OK to just append this to the end instead of using insert()
        // which will search for the right slot to insert the component based on execution order,
        // because the execution order of this script should be larger than all the others in the
        // enabledComponents array since it was just added.
        if (component.enabled && component.entity.enabled) {
            this._enabledComponents.append(component);
        }

        if (data.hasOwnProperty('order') && data.hasOwnProperty('scripts')) {
            component._scriptsData = data.scripts;

            for (let i = 0; i < data.order.length; i++) {
                component.create(data.order[i], {
                    enabled: data.scripts[data.order[i]].enabled,
                    attributes: data.scripts[data.order[i]].attributes,
                    preloading: this.preloading
                });
            }
        }
    }

    cloneComponent(entity, clone) {
        const order = [];
        const scripts = { };

        for (let i = 0; i < entity.script._scripts.length; i++) {
            const scriptInstance = entity.script._scripts[i];
            const scriptName = scriptInstance.__scriptType.__name;
            order.push(scriptName);

            const attributes = { };
            for (const key in scriptInstance.__attributes)
                attributes[key] = scriptInstance.__attributes[key];

            scripts[scriptName] = {
                enabled: scriptInstance._enabled,
                attributes: attributes
            };
        }

        for (const key in entity.script._scriptsIndex) {
            if (key.awaiting) {
                order.splice(key.ind, 0, key);
            }
        }

        const data = {
            enabled: entity.script.enabled,
            order: order,
            scripts: scripts
        };

        return this.addComponent(clone, data);
    }

    _resetExecutionOrder() {
        executionOrderCounter = 0;
        for (let i = 0, len = this._components.length; i < len; i++) {
            this._components.items[i]._executionOrder = executionOrderCounter++;
        }
    }

    _callComponentMethod(components, name, dt) {
        for (components.loopIndex = 0; components.loopIndex < components.length; components.loopIndex++) {
            components.items[components.loopIndex][name](dt);
        }
    }

    _onInitialize() {
        this.preloading = false;

        // initialize attributes on all components
        this._callComponentMethod(this._components, METHOD_INITIALIZE_ATTRIBUTES);

        // call onInitialize on enabled components
        this._callComponentMethod(this._enabledComponents, METHOD_INITIALIZE);
    }

    _onPostInitialize() {
        // call onPostInitialize on enabled components
        this._callComponentMethod(this._enabledComponents, METHOD_POST_INITIALIZE);
    }

    _onUpdate(dt) {
        // call onUpdate on enabled components
        this._callComponentMethod(this._enabledComponents, METHOD_UPDATE, dt);
    }

    _onPostUpdate(dt) {
        // call onPostUpdate on enabled components
        this._callComponentMethod(this._enabledComponents, METHOD_POST_UPDATE, dt);
    }

    // inserts the component into the enabledComponents array
    // which finds the right slot based on component._executionOrder
    _addComponentToEnabled(component) {
        this._enabledComponents.insert(component);
    }

    // removes the component from the enabledComponents array
    _removeComponentFromEnabled(component) {
        this._enabledComponents.remove(component);
    }

    _onBeforeRemove(entity, component) {
        const ind = this._components.items.indexOf(component);
        if (ind >= 0) {
            component._onBeforeRemove();
        }

        this._removeComponentFromEnabled(component);

        // remove from components array
        this._components.remove(component);
    }

    destroy() {
        super.destroy();

        this.app.systems.off('initialize', this._onInitialize, this);
        this.app.systems.off('postInitialize', this._onPostInitialize, this);
        this.app.systems.off('update', this._onUpdate, this);
        this.app.systems.off('postUpdate', this._onPostUpdate, this);
    }
}

export { ScriptComponentSystem };
