import { assert } from "../../core/assert.js";
import { array_copy_unique } from "../../core/collection/array/array_copy_unique.js";
import Signal from "../../core/events/signal/Signal.js";
import { IllegalStateException } from "../../core/fsm/exceptions/IllegalStateException.js";

import { noop } from "../../core/function/noop.js";
import { ResourceAccessKind } from "../../core/model/ResourceAccessKind.js";
import { computeSystemName } from "./computeSystemName.js";
import { EntityComponentDataset } from "./EntityComponentDataset.js";
import { EntityObserver } from "./EntityObserver.js";
import { System, SystemState } from "./System.js";
import { computeSystemComponentDependencyGraph } from "./system/computeSystemComponentDependencyGraph.js";
import { system_validate_class } from "./system_validate_class.js";

/**
 * @readonly
 * @enum {number}
 */
export const EntityManagerState = {
    Initial: 0,
    Starting: 1,
    Running: 2,
    Failed: 3,
    Stopping: 4,
    Stopped: 5
};

/**
 * In seconds
 * @type {number}
 */
const DEFAULT_SYSTEM_STARTUP_TIMEOUT_CHECK = 5;

/**
 * Brings together {@link System}s and an {@link EntityComponentDataset}
 * Main entry point into the simulation process.
 *
 * @example
 *      const em = new EntityManager()
 *
 *      em.addSystem(new MySystem())
 *      em.addSystem(new MyOtherSystem())
 *
 *      em.attachDataset(new EntityComponentDataset())
 *
 *      em.startup();
 *
 *      // ..
 *
 *      em.simulate(0.016); // advance simulation forward by 16ms
 *
 *      // ..
 *
 *      em.shutdown();
 *
 *      // ..
 *
 *      em.detachDataset();
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class EntityManager {

    /**
     * Registered systems
     * @readonly
     * @type {System[]}
     */
    systems = [];

    /**
     * Current order of execution.
     * Note: this can be smaller that the number of registered systems, as systems are only added to the execution order once their startup finishes
     * @readonly
     * @private
     * @type {System[]}
     */
    systemsExecutionOrder = [];

    /**
     * Observers associated with individual systems, responsible for {@link System.link}/{@link System.unlink}
     * @readonly
     * @private
     * @type {Map<System,EntityObserver>}
     */
    systemObservers = new Map();

    /**
     * @readonly
     */
    on = {
        systemStarted: new Signal(),
        systemStopped: new Signal(),
        /**
         * @type {Signal<System>}
         */
        systemAdded: new Signal(),
        systemRemoved: new Signal(),
    };

    /**
     *
     * @type {EntityManagerState}
     */
    state = EntityManagerState.Initial;

    /**
     * Value used to execute {@link System.fixedUpdate}
     * In seconds.
     * The default is ~60 Hz, which should be sufficient for most use-cases.
     * Setting this value higher will reduce the total number of steps per second, and typically lead to better performance.
     * Setting this value lower will increase number of steps per second, increasing simulation accuracy at the cost of performance.
     * Make sure you understand the implications when modifying this.
     * A good safe range is [0.001 - 0.2]
     * @type {number}
     */
    fixedUpdateStepSize = 0.016666666666;

    /**
     * Wall-clock budget, in milliseconds, for the whole fixed-step catch-up loop
     * within a single {@link simulate} call. Once a cycle's accumulated
     * fixedUpdate work exceeds this, the loop stops early and the unconsumed time
     * stays in the accumulator to be caught up later — this avoids a runaway
     * freeze when {@link System.fixedUpdate} work outpaces its time step. (Now a
     * per-cycle budget across all systems' lock-step steps; previously per-system.)
     * @type {number}
     */
    fixedUpdatePerSystemExecutionTimeLimit = 15;

    /**
     * Monotonic **simulation tick id** — the count of fixed steps the simulation
     * has executed. Advances once per {@link fixedUpdateStepSize} step inside
     * {@link simulate}. This is the hub-owned identity that producers (physics,
     * network) stamp per-tick state with, and that render-rate consumers
     * (interpolation) read as "the latest completed step". Pairs with
     * {@link getFixedStepAlpha}.
     * @type {number}
     */
    fixedStepTick = 0;

    /**
     * Canonical fixed-step time accumulator backing {@link fixedStepTick} and
     * {@link getFixedStepAlpha}. Holds the sub-step remainder in `[0, step)`.
     * @private
     * @type {number}
     */
    __fixedStepAccumulator = 0;

    /**
     * Currently attached dataset.
     * Do not modify directly, instead use {@link attachDataset} and {@link detachDataset} respectively.
     * @type {EntityComponentDataset}
     */
    dataset = null;

    /**
     * Whenever a system is added or removed, optimal execution plan changes, this flag tells us to rebuild the current plan
     * see {@link #systemsExecutionOrder}
     * @type {boolean}
     * @private
     */
    __execution_order_needs_update = true;

    /**
     * Rebuild execution order
     * @private
     */
    updateExecutionOrder() {
        // console.time('updateExecutionOrder')

        const order = this.systemsExecutionOrder;

        order.splice(0, order.length);

        const systems = this.systems;
        const system_count = systems.length;

        let executable_system_count = 0;

        for (let i = 0; i < system_count; i++) {
            const system = systems[i];

            if (system.state.get() !== SystemState.RUNNING) {
                // exclude systems that are not running
                continue;
            }

            if (system.update === noop && system.fixedUpdate === noop) {
                // not a simulation system
                continue;
            }

            order[executable_system_count++] = system;
        }

        // build dependency graphs amongst components
        const dependency_graph = computeSystemComponentDependencyGraph(executable_system_count, order);


        /**
         *
         * @param {System} system
         * @returns {number} higher values means it should be scheduled before others
         */
        function scoreSystem(system) {
            let result = 0;

            const components = system.referenced_components;
            const component_count = components.length;

            for (let i = 0; i < component_count; i++) {
                const component = components[i];

                result += 0.0001;


                // count number of incoming edges
                const attached_edge_count = dependency_graph.getNodeContainer(component).getIncomingEdgeCount();

                const access = system.getAccessForComponent(component);

                if ((access & ResourceAccessKind.Create) !== 0) {
                    result += attached_edge_count * 4
                } else if ((access & ResourceAccessKind.Write) !== 0) {
                    result += attached_edge_count * 2
                } else {
                    result += attached_edge_count;
                }

            }

            return result;
        }

        // pre-score systems for faster sorting
        const scores = new Map();

        for (let i = 0; i < executable_system_count; i++) {
            const system = order[i];

            scores.set(system, scoreSystem(system));
        }

        order.sort((a, b) => {
            return scores.get(b) - scores.get(a);
        });

        // clear update flag
        this.__execution_order_needs_update = false;
        // console.timeEnd('updateExecutionOrder')
    }

    /**
     * Get list of all components referenced by active systems
     * @returns {Class[]}
     */
    getComponentTypeMap() {
        const componentTypes = [];

        const systems = this.systems;

        const systemCount = systems.length;

        for (let i = 0; i < systemCount; i++) {
            /**
             *
             * @type {System}
             */
            const system = systems[i];


            const referencedComponents = system.referenced_components;

            array_copy_unique(referencedComponents, 0, componentTypes, componentTypes.length, referencedComponents.length);
        }

        return componentTypes;
    }

    /**
     * Link a given dataset, will cause associated systems to work with the new dataset as well
     * @param {EntityComponentDataset} dataset
     * @throws {Error} if another dataset is attached
     * @throws {Error} if dataset is incompatible with current system set
     */
    attachDataset(dataset) {
        assert.defined(dataset, "dataset");
        assert.notNull(dataset, "dataset");
        assert.equal(dataset.isEntityComponentDataset, true, "dataset must be an instance of EntityComponentDataset");

        //check if another dataset is attached
        if (this.dataset !== null) {

            if (this.dataset === dataset) {
                // special case, we already have this dataset attached. Nothing to more to do
                return;
            }

            throw new Error("Illegal status, another dataset is currently attached, you must detach it first");
        }

        const localComponentTypeMap = this.getComponentTypeMap();

        // ensure compatibility with system-used component map
        dataset.registerManyComponentTypes(localComponentTypeMap);

        this.dataset = dataset;

        for (const system of this.systems) {
            if (system.state.get() !== SystemState.RUNNING) {
                // not ready
                continue;
            }

            system.handleDatasetAttached(dataset);
        }

        for (const [s, observer] of this.systemObservers) {
            if (s.state.get() !== SystemState.RUNNING) {
                continue;
            }

            dataset.addObserver(observer, true);
        }

    }


    /**
     * Dissociate currently bound dataset, will cause attached systems to unlink entities held in the dataset.
     * Idempotent, if no dataset is attached - nothing will happen.
     */
    detachDataset() {
        const dataset = this.dataset;

        if (dataset === null) {
            //no dataset attached
            return;
        }

        //remove system observers
        for (const [_, observer] of this.systemObservers) {
            if (!observer.isConnected) {
                continue;
            }

            dataset.removeObserver(observer, true);
        }

        for (const system of this.systems) {
            if (system.state.get() !== SystemState.RUNNING) {
                // not running
                continue;
            }

            system.handleDatasetAttached(dataset);
        }

        this.dataset = null;
    }

    /**
     * @template T
     * @param {Class<T>} systemClass
     * @returns {boolean}
     */
    hasSystem(systemClass) {
        return this.getSystem(systemClass) !== null;
    }

    /**
     * @template T
     * @param {Class<T>} systemClass
     * @returns {T|null}
     */
    getSystem(systemClass) {
        assert.isFunction(systemClass, 'systemClass');

        const systems = this.systems;
        const numSystems = systems.length;

        for (let i = 0; i < numSystems; i++) {
            const system = systems[i];

            if (system instanceof systemClass) {
                return system;
            }
        }

        //not found
        return null;
    }

    /**
     * @deprecated use {@link EntityComponentDataset.getComponentClassByName} instead
     * @template T
     * @param {string} className
     * @returns {null|Class<T>}
     */
    getComponentClassByName(className) {
        assert.isString(className, 'className');

        const componentTypes = this.getComponentTypeMap();

        let i = 0;
        const l = componentTypes.length;
        for (; i < l; i++) {
            const componentClass = componentTypes[i];
            const name = componentClass.typeName;
            if (name === className) {
                return componentClass;
            }
        }
        return null;
    }


    /**
     * Advance simulation forward by a specified amount of time
     * @param {number} time_delta_seconds in seconds
     */
    simulate(time_delta_seconds) {
        assert.isNumber(time_delta_seconds, 'timeDelta');
        assert.notNaN(time_delta_seconds, 'timeDelta');
        assert.greaterThanOrEqual(time_delta_seconds, 0, 'time_delta_seconds must be >= 0');
        assert.isFinite(time_delta_seconds, 'timeDelta');

        if (this.dataset === null) {
            // dataset is not attached
            return;
        }

        if (this.__execution_order_needs_update) {
            this.updateExecutionOrder();
        }

        /**
         *
         * @type {System[]}
         */
        const systems = this.systemsExecutionOrder;

        const system_count = systems.length;

        const fixed_step = this.fixedUpdateStepSize;

        assert.notNaN(fixed_step, 'fixed_step');
        assert.greaterThan(fixed_step, 0, 'fixed_step must be greater than 0');

        // Global fixed-step loop. Every system advances its fixedUpdate in
        // LOCK-STEP on a single shared accumulator, and the simulation tick id is
        // incremented once per step inside the loop — so all systems run the same
        // step under the same `fixedStepTick`, and a producer can read
        // `fixedStepTick` during its own fixedUpdate as the true current step.
        // (All fixedUpdates for a step run before any update() — a clean
        // sim-then-render phase split that also keeps every system on the same
        // tick boundary for deterministic rewind/resim.)
        this.__fixedStepAccumulator += time_delta_seconds;

        const t0 = performance.now();
        while (this.__fixedStepAccumulator >= fixed_step) {
            this.fixedStepTick++;

            for (let i = 0; i < system_count; i++) {
                const system = systems[i];
                if (system.fixedUpdate !== noop) {
                    try {
                        system.fixedUpdate(fixed_step);
                    } catch (e) {
                        console.error(`Failed during fixedUpdate of system '${computeSystemName(system)}': `, e);
                    }
                }
            }

            this.__fixedStepAccumulator -= fixed_step;

            // Runaway guard: cap total fixed-step catch-up per simulate so a long
            // stall (e.g. a backgrounded tab resuming) doesn't freeze the frame.
            // The unconsumed remainder stays in the accumulator and is caught up
            // over subsequent calls.
            if (performance.now() - t0 > this.fixedUpdatePerSystemExecutionTimeLimit) {
                console.warn('EntityManager.simulate: fixedUpdate is falling behind the clock; capping catch-up this cycle to avoid a freeze.');
                break;
            }
        }

        // Variable-rate update pass — after all fixed steps for this cycle.
        for (let i = 0; i < system_count; i++) {
            const system = systems[i];
            if (system.update !== noop) {
                try {
                    system.update(time_delta_seconds);
                } catch (e) {
                    console.error(`Failed during update of system '${computeSystemName(system)}': `, e);
                }
            }
        }
    }

    /**
     * Fractional progress through the current fixed step, in `[0, 1)` — the
     * leftover sim time that hasn't yet formed a whole {@link fixedUpdateStepSize}
     * step, normalized by the step size. This is the blend parameter the
     * render-time interpolation system pairs with {@link fixedStepTick} to
     * interpolate between the last two completed steps: `alpha = 0` sits on the
     * previous tick, `alpha → 1` approaches the latest.
     *
     * @returns {number} alpha in `[0, 1)`
     */
    getFixedStepAlpha() {
        const alpha = this.__fixedStepAccumulator / this.fixedUpdateStepSize;
        return alpha < 1 ? alpha : 1;
    }

    /**
     * If the {@link EntityManager} is already started, the system will be started automatically before being added
     * @param {System} system
     * @returns {Promise} resolution depends on {@link EntityManager}'s state, if running - promise resolves after system startup. Otherwise, the promise will be resolved immediately.
     * @throws {IllegalStateException}
     */
    addSystem(system) {
        assert.defined(system, "system");
        assert.isInstanceOf(system, System, 'system', 'System');

        const existing = this.getSystem(Object.getPrototypeOf(system).constructor);

        if (existing !== null) {
            if (existing === system) {
                //system already added, do nothing
                return Promise.resolve();
            } else {
                throw new IllegalStateException(`Another instance of system '${computeSystemName(system)}' is already registered`);
            }
        }


        try {
            system_validate_class(system);
        } catch (e) {
            console.error(`Validation of '${computeSystemName(system)}' failed : `, e, system);
        }

        //check system state
        const systemState = system.state.getValue();

        const valid_states = [SystemState.INITIAL, SystemState.STOPPED];

        if (valid_states.indexOf(systemState) === -1) {
            //illegal state
            throw new IllegalStateException(`System must be in one of these states: [${valid_states.join(",")}], instead was ${systemState}`);
        }

        const systems = this.systems;
        const systemIndex = systems.length;

        systems[systemIndex] = system;


        //build observer
        const entityObserver = new EntityObserver(system.dependencies, system.link, system.unlink, system);
        this.systemObservers.set(system, entityObserver);

        let result_promise;
        if (this.state === EntityManagerState.Running) {
            //initialize the system
            result_promise = new Promise((resolve, reject) => {
                this.startSystem(system, resolve, reject);
            });
        } else {
            result_promise = Promise.resolve();
        }


        // link dependency components
        this.dataset?.registerManyComponentTypes(system.referenced_components);

        this.on.systemAdded.send1(system);

        return result_promise;
    }

    /**
     *
     * @param {System} system
     * @returns {Promise<boolean>}
     */
    async removeSystem(system) {
        assert.defined(system, "system");
        assert.isInstanceOf(system, System, 'system', 'System');

        const systemIndex = this.systems.indexOf(system);

        if (systemIndex === -1) {
            //system not found
            return false;
        }

        //unlink system observer
        const systemObserver = this.systemObservers.get(system);
        this.systemObservers.delete(system);

        assert.notEqual(systemObserver, undefined, "System observer is undefined, it was possibly removed illegally or was not created");

        //shutdown system
        this.systems.splice(systemIndex, 1);

        // request exec order update
        this.__execution_order_needs_update = true;

        if (this.dataset !== null) {
            // remove observed and unlink entities
            this.dataset.removeObserver(systemObserver, true);
            system.handleDatasetDetached(this.dataset);
        }

        await new Promise((resolve, reject) => {
            this.stopSystem(system, resolve, (reason) => {

                reject(`system ${computeSystemName(system)} shutdown failed: ${reason}`);
            });
        });

        this.on.systemRemoved.send1(system);

        return true;
    }

    /**
     * @private
     * @param {System} system
     * @param {function(system: System)} successCallback
     * @param {function(reason:*)} errorCallback
     */
    stopSystem(system, successCallback, errorCallback) {
        const self = this;

        try {
            system.state.set(SystemState.STOPPING);
        } catch (e) {
            console.error(`Failed to set system state to STOPPING`, e);
            errorCallback(e);
            return;
        }

        function systemReady() {
            system.state.set(SystemState.STOPPED);

            self.on.systemStopped.send1(system);

            successCallback(system);
        }

        function systemFailed(reason) {
            errorCallback(reason);
        }

        try {
            const promise = system.shutdown(self);

            assert.defined(promise, "promise");
            assert.notNull(promise, "promise");
            assert.isFunction(promise.then, "promise.then");

            promise.then(systemReady, systemFailed);
        } catch (e) {
            console.error(`Failed to execute system shutdown`, e);
            errorCallback(e);
        }
    }

    /**
     * @private
     * @param {System} system
     * @param {function(system: System)} successCallback
     * @param {function(reason:*)} errorCallback
     */
    startSystem(system, successCallback, errorCallback) {
        assert.defined(system, "system");
        assert.isFunction(successCallback, "successCallback");
        assert.isFunction(errorCallback, "errorCallback");

        if (system.state.getValue() === SystemState.RUNNING) {
            //system is already running
            console.warn(`System '${computeSystemName(system)}' is already running, nothing to do`);
            successCallback(system);
            return;
        }

        const self = this;

        try {
            system.state.set(SystemState.STARTING);
        } catch (e) {
            console.error(`Failed to set system state to STARTING`, e);
            errorCallback(e);
            return;
        }

        function systemReady() {
            system.state.set(SystemState.RUNNING);

            self.on.systemStarted.dispatch(system);

            const i = self.systems.indexOf(system);

            assert.notEqual(i, -1, "System was not found in the system list");

            if (self.dataset !== null) {
                system.handleDatasetAttached(self.dataset);
            }

            const observer = self.systemObservers.get(system);

            if (observer !== undefined) {
                // only link the observer once startup has succeeded
                self.dataset?.addObserver(observer, true);
            }

            // request exec order update
            self.__execution_order_needs_update = true;

            successCallback(system);
        }

        try {

            const deprecation_check = () => {
                throw new Error("'success' callback was deprecated in meep 2.128.0, please use `async` instead.");
            };

            // Link EntityManager
            if (system.entityManager === null || system.entityManager === undefined) {
                system.entityManager = this;
            } else if (system.entityManager !== this) {
                throw new Error(`System is bound to another EntityManager`);
            }

            const promise = system.startup(self, deprecation_check);


            assert.defined(promise, "promise");
            assert.notNull(promise, "promise");
            assert.isFunction(promise.then, "promise.then");

            const startup_check_timeout = setTimeout(
                () => {
                    console.warn(`System '${computeSystemName(system)}' failed to complete startup process in ${DEFAULT_SYSTEM_STARTUP_TIMEOUT_CHECK}s, check your code for potential deadlocks.`);
                },
                DEFAULT_SYSTEM_STARTUP_TIMEOUT_CHECK * 1000,
            );

            promise.finally(() => {
                clearTimeout(startup_check_timeout);
            });

            promise.then(systemReady, errorCallback);

        } catch (e) {
            console.error(`Failed to execute system startup`, e);
            errorCallback(e);
        }
    }

    /**
     * This method is asynchronous by nature, it has to wait for each system to finish its own startup.
     * Make sure to register callback to be notified when the startup has finished
     * @param {function} [readyCallback] executed once entity manager successfully completes startup
     * @param {function} [errorCallback] executed if entity manager encounters an error during startup
     */
    startup(readyCallback = noop, errorCallback = console.error) {

        if (this.state === EntityManagerState.Starting) {
            throw new IllegalStateException(`System is currently in starting state`);
        }

        if (this.state === EntityManagerState.Running) {
            // already running, do nothing
            readyCallback();
            return;
        }

        assert.isFunction(readyCallback, 'readyCallback');
        assert.isFunction(errorCallback, 'errorCallback');

        this.state = EntityManagerState.Starting;

        const self = this;
        const systems = this.systems;
        let readyCount = 0;
        const expectedReadyCount = systems.length;

        console.log(`EntityManager startup initialized, starting ${expectedReadyCount} systems.`);

        function finalizeStartup() {
            self.state = EntityManagerState.Running;

            console.log(`EntityManager startup finished, all systems started`);
            try {
                readyCallback();
            } catch (e) {
                console.error("All systems were started OK, but readyCallback failed.", e);
            }
        }

        function systemReady() {
            //startup all systems
            readyCount++;

            if (readyCount === expectedReadyCount) {
                finalizeStartup();
            }
        }

        let firstFailure = true;

        function systemError(system, error) {
            console.error("Failed to start system", system, 'Error:', error);

            if (firstFailure) {
                firstFailure = false;

                self.state = EntityManagerState.Failed;

                errorCallback(error);
            }
        }

        if (expectedReadyCount === 0) {
            //no systems registered, we're done
            finalizeStartup();
            return;
        }

        for (const system of systems) {
            //ensure eventManager are there
            //start the system
            const cbOK = systemReady.bind(null, system);
            const cbError = systemError.bind(null, system);

            this.startSystem(system, cbOK, cbError);

        }
    }

    /**
     *
     * @param {Class} systemClass
     * @returns {Promise.<System>}
     */
    promiseSystem(systemClass) {
        /**
         *
         * @type {EntityManager}
         */
        const em = this;

        return new Promise(function (resolve, reject) {

            function systemAdded(s) {
                if (s instanceof systemClass) {
                    //unregister listener
                    em.on.systemAdded.remove(systemAdded);
                    resolve(s);
                }
            }

            const system = em.getSystem(systemClass);
            if (system !== null) {
                resolve(system);
            } else {
                em.on.systemAdded.add(systemAdded);
            }
        });
    }

    /**
     * @param {Class} SystemClass
     * @param {SystemState} state
     * @returns {Promise.<System>}
     */
    promiseSystemInState(SystemClass, state) {

        assert.defined(SystemClass, 'SystemClass');
        assert.enum(state, SystemState, 'state');

        const em = this;

        return new Promise(function (resolve, reject) {

            const pSystem = em.promiseSystem(SystemClass);

            pSystem.then(function (system) {
                function tryProcessSystem() {
                    if (system.state.get() !== state) {
                        system.state.onChanged.addOne(tryProcessSystem);
                    } else {
                        resolve(system);
                    }
                }

                tryProcessSystem();
            }, reject);

        });
    }

    /**
     * This method is asynchronous by nature, it will not be done until each system has finished its shutdown
     * Make sure to use callback to be notified when the shutdown has completed
     * @param {function} [readyCallback] Called when shutdown finishes successfully. defaults to no-operation
     * @param {function} [errorCallback] Called when an error occurs during the shutdown process. defaults to console error output
     */
    shutdown(readyCallback = noop, errorCallback = console.error) {
        if (this.state !== EntityManagerState.Running) {
            throw new IllegalStateException(`System is wrong state, expected '${EntityManagerState.Running}'`);
        }

        assert.isFunction(readyCallback, 'readyCallback');
        assert.isFunction(errorCallback, 'errorCallback');

        this.state = EntityManagerState.Stopping;

        if (this.dataset !== null) {
            // a dataset is still attached, to properly dispose of all the component-bound resources, we first detach the dataset
            this.detachDataset();
        }

        const self = this;
        const systems = this.systems;
        let readyCount = 0;
        const expectedReadyCount = systems.length;

        function finalizeShutdown() {
            self.state = EntityManagerState.Stopped;

            try {
                readyCallback();
            } catch (e) {
                console.error("All systems were shutdown OK, but readyCallback failed.", e);
            }
        }

        function systemReady(system) {
            //startup all systems
            readyCount++;

            system.state.set(SystemState.STOPPED);

            self.on.systemStopped.send1(system);
            if (readyCount === expectedReadyCount) {
                finalizeShutdown();
            }
        }

        let firstFailure = true;

        function systemError(system) {
            console.error("Failed to shutdown system", system);

            if (firstFailure) {
                firstFailure = false;

                self.state = EntityManagerState.Failed;

                errorCallback();
            }
        }

        if (expectedReadyCount === 0) {
            //no systems registered, we're done
            finalizeShutdown();
        }

        for (const system of systems) {

            system.state.set(SystemState.STOPPING);

            const cbOK = systemReady.bind(null, system);
            const cbError = systemError.bind(null, system);

            try {
                const promise = system.shutdown(this);

                assert.defined(promise, 'promise');
                assert.notNull(promise, 'promise');
                assert.isFunction(promise.then, 'promise.then');

                // TODO add timeout

                promise.then(cbOK, cbError);
            } catch (e) {
                //failure in shutdown function
                cbError(e);
            }

        }
    }
}

/**
 * @deprecated use lowercase spelling `attachDataset` instead
 */
EntityManager.prototype.attachDataSet = EntityManager.prototype.attachDataset;
/**
 * @deprecated use lowercase spelling `detachDataset` instead
 */
EntityManager.prototype.detachDataSet = EntityManager.prototype.detachDataset;