import { assert } from "../../../../core/assert.js";
import { resolvePath } from "../../../../core/json/resolvePath.js";
import ObservedValue from "../../../../core/model/ObservedValue.js";
import { System } from "../../../ecs/System.js";
import InputController from "../components/InputController.js";

/**
 *
 * @param {Object} object
 * @param {String} propertyName
 * @param {function} constructor
 * @returns {*}
 */
function getOrCreateProperty(object, propertyName, constructor) {
    assert.notEqual(object, undefined, `object must not be undefined, can't resolve property '${propertyName}`);
    assert.equal(typeof propertyName, "string", `propertyName must be of type 'string', instead was '${typeof propertyName}'`);
    assert.equal(typeof constructor, "function", `constructor must be of type 'function', instead was '${typeof constructor}'`);

    if (object.hasOwnProperty(propertyName)) {
        return object[propertyName];
    } else {
        let value = constructor();
        object[propertyName] = value;
        return value;
    }
}

/**
 *
 * @param {Object} proxies
 * @param {String} path
 * @param {Signal} signal
 * @returns {Proxy}
 */
function getOrCreateProxy(proxies, path, signal) {
    class Proxy {
        /**
         *
         * @param {Signal} signal
         * @constructor
         */
        constructor(signal) {

            /**
             * @type {Signal}
             */
            this.signal = signal;

            this.running = false;

            this.deferred = [];

            /**
             *
             * @type {InputControllerBinding[]}
             */
            this.bindings = [];


        }

        handler() {
            const bindings = this.bindings;
            const l = bindings.length;

            for (let i = 0; i < l; i++) {
                /**
                 *
                 * @type {InputControllerBinding}
                 */
                const binding = bindings[i];
                const listener = binding.listener;
                listener.apply(undefined, arguments);
                if (binding.exclusive) {
                    break;
                }
            }

            this.__processDeferred();

        }

        __processDeferred() {
            const deferred = this.deferred;
            const dl = deferred.length;
            for (let i = 0; i < dl; i++) {
                const binding = deferred[i];
                this.registerBinding(binding);
            }

            this.deferred = [];
        }

        /**
         *
         * @param {InputControllerBinding} binding
         */
        registerBinding(binding) {
            this.bindings.push(binding);
            this.bindings.sort((a, b) => {
                if (a.exclusive && !b.exclusive) {
                    return -1;
                } else if (!a.exclusive && b.exclusive) {
                    return 1;
                } else {
                    return b.priotity - a.priority;
                }
            });
        }

        start() {
            this.signal.add(this.handler, this);
        }

        stop() {
            this.signal.remove(this.handler, this);
        }

        /**
         *
         * @param {InputControllerBinding} binding
         */
        add(binding) {
            //TODO handle case where binding is added mid-dispatch
            this.registerBinding(binding);
        }

        /**
         *
         * @param {InputControllerBinding} binding
         */
        remove(binding) {
            const i_b = this.bindings.indexOf(binding);

            if (i_b !== -1) {
                this.bindings.splice(i_b, 1);
            } else {
                const i_d = this.deferred.indexOf(binding);

                this.deferred.splice(i_d, 1);
            }

        }
    }

    return getOrCreateProperty(proxies, path, function () {
        const proxy = new Proxy(signal);
        proxy.start();
        return proxy;
    });
}

/**
 *
 * @param {List.<InputControllerBinding>} mapping
 * @param devices
 * @param proxies
 */
function applyBindings(mapping, devices, proxies) {
    mapping.forEach(function (binding) {
        const path = binding.path;
        const signal = resolvePath(devices, path);
        const proxy = getOrCreateProxy(proxies, path, signal);
        proxy.add(binding);
    });
}

/**
 *
 * @param {List.<InputControllerBinding>} mapping
 * @param devices
 * @param proxies
 */
function removeBindings(mapping, devices, proxies) {
    mapping.forEach(function (binding) {
        const path = binding.path;
        const signal = resolvePath(devices, path);
        const proxy = getOrCreateProxy(proxies, path, signal);
        proxy.remove(binding);
    });
}

/**
 * Prefer to use {@link InputSystem} instead
 */
class InputControllerSystem extends System {
    constructor(devices) {
        super();
        this.enabled = new ObservedValue(true);

        this.dependencies = [InputController];

        this.devices = devices;

        // console.log("Input Controller System started. Devices: ", devices);

        const self = this;

        this.enabled.onChanged.add(function (v) {
            const entityManager = self.entityManager;
            const dataset = entityManager.dataset;

            if (dataset === null) {
                // do nothing
                return;
            }

            if (v) {
                dataset.traverseComponents(InputController, function (c) {
                    applyBindings(c.mapping, self.devices, self.proxies);
                });
            } else {
                dataset.traverseComponents(InputController, function (c) {
                    removeBindings(c.mapping, self.devices, self.proxies);
                });
            }
        });
    }

    async startup(entityManager) {
        this.entityManager = entityManager;
        this.proxies = {};
    }

    /**
     *
     * @param {InputController} component
     * @param entity
     */
    link(component, entity) {
        const mapping = component.mapping;
        const devices = this.devices;
        applyBindings(mapping, devices, this.proxies);
    }

    /**
     *
     * @param {InputController} component
     * @param entity
     */
    unlink(component, entity) {
        const mapping = component.mapping;
        const devices = this.devices;
        removeBindings(mapping, devices, this.proxies);

        component.on.unlinked.send1(entity);
    }
}


export default InputControllerSystem;
