import { assert } from "../../core/assert.js";
import { BitSet } from "../../core/binary/BitSet.js";
import { array_set_diff } from "../../core/collection/array/array_set_diff.js";
import { array_shrink_to_size } from "../../core/collection/array/array_shrink_to_size.js";
import { findSignalHandlerIndexByHandle } from "../../core/events/signal/findSignalHandlerIndexByHandle.js";
import {
    findSignalHandlerIndexByHandleAndContext
} from "../../core/events/signal/findSignalHandlerIndexByHandleAndContext.js";
import Signal from "../../core/events/signal/Signal.js";
import { SignalHandler } from "../../core/events/signal/SignalHandler.js";
import { max3 } from "../../core/math/max3.js";

import { EventType } from "./EventType.js";

/**
 *
 * @param {number} entityIndex
 * @returns {boolean}
 */
function validateEntityIndex(entityIndex) {
    return validateIndexValue(entityIndex, "entityIndex");
}

/**
 *
 * @param {number} componentIndex
 * @returns {boolean}
 */
function validateComponentIndex(componentIndex) {
    return validateIndexValue(componentIndex, "componentIndex");
}

/**
 *
 * @param {number} index
 * @param {string} name
 * @returns {boolean}
 */
function validateIndexValue(index, name) {
    if (typeof index !== "number") {
        throw new TypeError(`${name} must be a number, instead was ${typeof index}(=${index})`);
    }

    if (!Number.isInteger(index)) {
        throw new Error(`${name} must be an integer, instead was ${index}`);
    }

    if (index < 0) {
        throw new Error(`${name} must be non-negative, instead was ${index}`);
    }

    return true;
}

/**
 * Matches a supplies component mask against a larger set
 * @param {BitSet} componentOccupancy
 * @param {number} entityIndex
 * @param {number} componentTypeCount
 * @param {BitSet} mask
 * @returns {boolean} true if mask matches completely, false otherwise
 */
function matchComponentMask(
    componentOccupancy,
    entityIndex,
    componentTypeCount,
    mask
) {
    const offset = entityIndex * componentTypeCount;

    for (
        let componentIndex = mask.nextSetBit(0);
        componentIndex !== -1;
        componentIndex = mask.nextSetBit(componentIndex + 1)
    ) {
        const componentPresent = componentOccupancy.get(componentIndex + offset);

        if (!componentPresent) {
            return false;
        }
    }

    return true;
}

/**
 *
 * @param {number} entityIndex
 * @param {BitSet} mask
 * @param {number[]} componentIndexMap
 * @param {[]} components
 * @param {[]} result
 */
function buildObserverCallbackArgs(entityIndex, mask, componentIndexMap, components, result) {
    for (
        let i = mask.nextSetBit(0);
        i !== -1;
        i = mask.nextSetBit(i + 1)
    ) {
        const componentDataset = components[i];

        const componentInstance = componentDataset[entityIndex];

        const resultIndex = componentIndexMap[i];

        result[resultIndex] = componentInstance;
    }
}

/**
 *
 * @type {number[]}
 */
const scratch_indices = [];

/**
 * Used for constructing arguments prior to traversal visitor call
 * @type {*[]}
 */
const scratch_args = [];

/**
 * Represents a storage for entities and their associated components.
 * Entities are just integer IDs and components are stored in a virtual table, where each component type has a separate column and each entity is a row in that table.
 * It is valid for entities to have no components or to have every possible component.
 * The entity IDs are compacted, meaning that when an entity is removed - its ID can later be reused.
 * Typically, you would use {@link Entity} helper class instead of working directly with the dataset as it offers a higher-level API.
 *
 * Designed to handle millions of objects at the same time.
 * @implements {Iterable<number>} iterate over the entity IDs
 * @example
 *   const ecd = new EntityComponentDataset();
 *
 *   const entityId = ecd.createEntity(); // create an entity
 *
 *   ecd.addComponentToEntity(entityId, myComponentInstance); // add a component to your entity
 *
 *   // ... once no longer needed, destroy the entity
 *   ecd.removeEntity(entityId);
 *
 * @see https://en.wikipedia.org/wiki/Entity_component_system
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class EntityComponentDataset {

    /**
     * Set a bit at index of an entity if index is used, unset otherwise
     * @private
     * @type {BitSet}
     */
    entityOccupancy = new BitSet();

    /**
     * For each entity ID records generation when entity was created
     * Values are invalid for unused entity IDs
     * @private
     * @type {Uint32Array}
     */
    entityGeneration = new Uint32Array(0);

    /**
     * Bit table, if a bit is set - that means component is present.
     * The format is
     *      entity_0: [component_0, component_1, ... component_n]
     *      entity_1: [component_0, component_1, ... component_n]
     *       ...
     *      entity_n: [component_0, component_1, ... component_n]
     * @private
     * @type {BitSet}
     */
    componentOccupancy = new BitSet();

    /**
     * Do not modify directly.
     * Use {@link setComponentTypeMap} instead.
     * @private
     * @type {Class[]}
     */
    componentTypeMap = [];

    /**
     * Fast index lookup from a component class
     * @type {Map<Class, number>}
     * @private
     */
    __type_to_index_map = new Map();

    /**
     * How many component types exist for this collection. This is the same as componentMap.length
     * @private
     * @type {number}
     */
    componentTypeCount = 0;

    /**
     * 2d array of following structure: components[component_index][entity_index].
     * @private
     * @type {Object[][]}
     */
    components = [];

    /**
     * Current number of entities
     * @private
     * @type {number}
     */
    entityCount = 0;

    /**
     * A counter that is incremented every time an entity is created
     * Generation is used to provide an entity with a unique identity
     * Entity IDs are re-used, but generation is always increasing
     * Two entities with the same ID but different generation represent two distinct entities, one with "younger" generation (larger number) is the more recently created one
     * @private
     * @type {number}
     */
    generation = 0;

    /**
     * @readonly
     * @type {Signal<number>}
     */
    onEntityCreated = new Signal();

    /**
     * @readonly
     * @type {Signal<number>}
     */
    onEntityRemoved = new Signal();


    /**
     *
     * @type {Array<Object<SignalHandler[]>>}
     * @private
     */
    __entityEventListeners = [];

    /**
     *
     * @type {SignalHandler[][]}
     * @private
     */
    __entityAnyEventListeners = [];

    /**
     * @private
     * @type {Array<Array<EntityObserver>>}
     */
    observers = [];


    /**
     * returns a promise of a component instance based on a given type
     * @template T, R
     * @param {int} entity
     * @param {T} component_type
     * @returns {Promise<R>}
     */
    promiseComponent(entity, component_type) {

        assert.ok(this.entityExists(entity), `Entity ${entity} doesn't exist`);

        const self = this;
        return new Promise(function (resolve, reject) {
            const component = self.getComponent(entity, component_type);

            if (component !== undefined) {
                resolve(component);
                return;
            }

            function handler(event, entity) {
                const componentClass = event.klass;

                if (componentClass === component_type) {
                    //found the right one

                    const instance = event.instance;

                    self.removeEntityEventListener(entity, EventType.ComponentAdded, handler);

                    resolve(instance);
                }
            }

            function handleRemoval() {
                reject(`Entity ${entity} has been removed`);
            }

            self.addEntityEventListener(entity, EventType.ComponentAdded, handler);
            self.addEntityEventListener(entity, EventType.EntityRemoved, handleRemoval);
        });
    }

    /**
     *
     * @param {EntityObserver} observer
     * @param {boolean} [immediate=false] whenever pre-existing matches should be processed
     * @returns {boolean}
     */
    addObserver(observer, immediate) {
        if (observer.dataset === this) {
            // already connected
            return false;
        }

        observer.dataset = this;

        let i;

        //build the observer
        observer.build(this.componentTypeMap);

        //add to observer stores
        const componentMask = observer.componentMask;

        for (i = componentMask.nextSetBit(0); i !== -1; i = componentMask.nextSetBit(i + 1)) {
            const observerStore = this.observers[i];
            observerStore.push(observer);
        }

        if (immediate === true) {
            //process existing matches

            const componentTypeCount = this.componentTypeCount;

            const componentOccupancy = this.componentOccupancy;

            const entityOccupancy = this.entityOccupancy;

            const components = this.components;

            const componentIndexMap = observer.componentIndexMapping;

            const args = [];

            for (i = entityOccupancy.nextSetBit(0); i !== -1; i = entityOccupancy.nextSetBit(i + 1)) {
                const match = matchComponentMask(componentOccupancy, i, componentTypeCount, componentMask);

                if (match) {
                    buildObserverCallbackArgs(i, componentMask, componentIndexMap, components, args);

                    //write entityIndex
                    args[observer.componentTypeCount] = i;

                    observer.callbackComplete.apply(observer.thisArg, args);
                }
            }
        }

        return true;
    }

    /**
     *
     * @param {EntityObserver} observer
     * @param {boolean} [immediate=false] if flag set, matches will be broken after observer is removed
     * @returns {boolean}
     */
    removeObserver(observer, immediate) {
        if (observer.dataset !== this) {
            // not connected to this dataset
            return false;
        }

        let i;
        let foundFlag = false;

        const componentMask = observer.componentMask;

        //remove observer from stores
        for (i = componentMask.nextSetBit(0); i !== -1; i = componentMask.nextSetBit(i + 1)) {
            const observerStore = this.observers[i];
            const index = observerStore.indexOf(observer);

            if (index === -1) {
                continue;
            }

            foundFlag = true;

            observerStore.splice(index, 1);
        }

        if (foundFlag && immediate === true) {
            //process existing matches

            const componentTypeCount = this.componentTypeCount;

            const componentOccupancy = this.componentOccupancy;

            const entityOccupancy = this.entityOccupancy;

            const components = this.components;

            const componentIndexMap = observer.componentIndexMapping;

            const args = [];

            for (i = entityOccupancy.nextSetBit(0); i !== -1; i = entityOccupancy.nextSetBit(i + 1)) {
                const match = matchComponentMask(componentOccupancy, i, componentTypeCount, componentMask);

                if (match) {
                    buildObserverCallbackArgs(i, componentMask, componentIndexMap, components, args);

                    //write entityIndex
                    args[observer.componentTypeCount] = i;

                    //break the match
                    observer.callbackBroken.apply(observer.thisArg, args);
                }
            }
        }

        // clear out observer's state
        observer.dataset = null;

        return foundFlag;
    }

    /**
     *
     * @returns {number}
     */
    getEntityCount() {
        return this.entityCount;
    }

    /**
     *
     * @returns {number}
     */
    getComponentTypeCount() {
        return this.componentTypeCount;
    }

    /**
     * Convenience method for retrieving a collection of components for a given entity
     * @param {number} entity ID of the entity
     * @param {[]} component_classes Classes of components to extract
     * @returns {Array}
     */
    getComponents(entity, component_classes) {
        assert.ok(this.entityExists(entity), `Entity ${entity} doesn't exist`);

        assert.notNull(component_classes, "component_classes");
        assert.defined(component_classes, "component_classes");
        assert.isArray(component_classes, "component_classes");
        //assert.notOk(componentClasses.some((c, i) => componentClasses.indexOf(c) !== i), 'componentClasses contains duplicates');

        const resultLength = component_classes.length;

        const result = new Array(resultLength);

        const componentTypeCount = this.componentTypeCount;
        const occupancyStart = componentTypeCount * entity;
        const occupancyEnd = occupancyStart + componentTypeCount;

        for (let i = this.componentOccupancy.nextSetBit(occupancyStart); i < occupancyEnd && i !== -1; i = this.componentOccupancy.nextSetBit(i + 1)) {
            const componentIndex = i % componentTypeCount;

            const componentType = this.componentTypeMap[componentIndex];

            const resultIndex = component_classes.indexOf(componentType);

            if (resultIndex === -1) {
                // not requested, skip
                continue;
            }

            result[resultIndex] = this.components[componentIndex][entity];
        }

        return result;
    }

    /**
     *
     * @param {[]} output
     * @param {number} output_offset
     * @param {number} entity_id
     * @returns {number} how many components were written to the output
     * @see getAllComponents
     */
    readEntityComponents(
        output,
        output_offset,
        entity_id
    ) {

        assert.isNonNegativeInteger(entity_id, 'entity_id');
        assert.ok(this.entityExists(entity_id), `Entity ${entity_id} doesn't exist`);
        assert.isArray(output, "output");
        assert.isNonNegativeInteger(output_offset, 'output_offset');

        const componentTypeCount = this.componentTypeCount;
        const occupancy_start = componentTypeCount * entity_id;
        const occupancy_end = occupancy_start + componentTypeCount;

        const occupancy = this.componentOccupancy;

        let offset = output_offset;

        for (
            let i = occupancy.nextSetBit(occupancy_start);
            i < occupancy_end && i !== -1;
            i = occupancy.nextSetBit(i + 1)
        ) {
            const componentIndex = i % componentTypeCount;

            const component = this.components[componentIndex][entity_id];

            output[offset++] = component;

        }

        return offset - output_offset;
    }

    /**
     * Get all components associated with a given entity.
     * Note that this method allocates. If performance is important - prefer alternatives.
     * Prefer to use {@link readEntityComponents} for performance reasons.
     * @param {number} entity_id
     * @returns {[]} all components attached to the entity, array is not compacted
     * @see readEntityComponents
     */
    getAllComponents(entity_id) {
        assert.isNonNegativeInteger(entity_id, 'entity_id');
        assert.ok(this.entityExists(entity_id), `Entity ${entity_id} doesn't exist`);

        const ret = [];

        const componentTypeCount = this.componentTypeCount;
        const occupancy_start = componentTypeCount * entity_id;
        const occupancy_end = occupancy_start + componentTypeCount;

        const occupancy = this.componentOccupancy;

        for (
            let i = occupancy.nextSetBit(occupancy_start);
            i < occupancy_end && i !== -1;
            i = occupancy.nextSetBit(i + 1)
        ) {
            const componentIndex = i % componentTypeCount;

            ret[componentIndex] = this.components[componentIndex][entity_id];
        }

        return ret;
    }

    /**
     * Modify dataset component mapping. Algorithm will attempt to mutate dataset even if entities exist, however, it will not remove component classes for which instances exist in the dataset.
     * @param {Class[]} map collection of component classes
     * @returns {void}
     * @throws Error when attempting to remove component classes with live instances
     */
    setComponentTypeMap(map) {
        assert.defined(map, "map");
        assert.isArray(map, 'map');

        const newComponentTypeCount = map.length;

        const diff = array_set_diff(map, this.componentTypeMap);

        const typesToAdd = diff.uniqueA;
        const typesToRemove = diff.uniqueB;

        const typesCommon = diff.common;

        const self = this;

        function existingComponentsRemovalCheck() {
            const presentComponentTypes = [];

            for (let i = 0; i < typesToRemove.length; i++) {
                const type = typesToRemove[i];

                self.traverseComponents(type, function () {
                    presentComponentTypes.push(type);
                    //stop traversal
                    return false;
                });
            }

            if (presentComponentTypes.length > 0) {
                const sTypes = presentComponentTypes.map(t => t.typeName).join(", ");
                throw new Error(`Component types can not be unmapped due to presence of live components: ${sTypes}`);
            }
        }

        /**
         *
         * @returns {number[]}
         */
        function computeComponentIndexRemapping() {
            const indexRemapping = [];

            let i;
            let l;

            for (i = 0, l = typesCommon.length; i < l; i++) {
                const commonType = typesCommon[i];

                //get old index
                const indexOld = self.componentTypeMap.indexOf(commonType);

                const indexNew = map.indexOf(commonType);

                indexRemapping[indexOld] = indexNew;
            }

            return indexRemapping;
        }

        /**
         *
         * @param {number[]} indexRemapping
         */
        function updateComponentOccupancy(indexRemapping) {

            //build new component occupancy map
            const newComponentOccupancy = new BitSet();

            let i;

            const oldComponentTypeCount = self.componentTypeCount;

            for (i = self.componentOccupancy.nextSetBit(0); i !== -1; i = self.componentOccupancy.nextSetBit(i + 1)) {
                //determine component index
                const oldComponentIndex = i % oldComponentTypeCount;


                const newComponentIndex = indexRemapping[oldComponentIndex];

                if (newComponentIndex !== undefined) {
                    const entity = Math.floor(i / oldComponentTypeCount);

                    newComponentOccupancy.set(entity * newComponentTypeCount + newComponentIndex, true);
                }
            }

            self.componentOccupancy = newComponentOccupancy;
        }

        /**
         *
         * @param {number[]} indexRemapping
         */
        function updateComponentStores(indexRemapping) {
            let i;
            let l;

            const newStore = [];

            for (i = 0, l = typesToAdd.length; i < l; i++) {
                const type = typesToAdd[i];

                assert.defined(type, 'type');

                const newIndex = map.indexOf(type);

                //initialize component store
                newStore[newIndex] = [];
            }

            for (i = 0, l = indexRemapping.length; i < l; i++) {
                const newIndex = indexRemapping[i];

                if (newIndex === undefined) {
                    continue;
                }

                newStore[newIndex] = self.components[i];
            }

            self.components = newStore;
        }

        /**
         *
         * @param {number[]} indexRemapping
         */
        function updateObservers(indexRemapping) {
            let i;
            let l;

            const newStore = [];

            for (i = 0, l = typesToAdd.length; i < l; i++) {
                const type = typesToAdd[i];

                const newIndex = map.indexOf(type);

                //initialize component store
                newStore[newIndex] = [];
            }


            for (i = 0, l = indexRemapping.length; i < l; i++) {
                const newIndex = indexRemapping[i];

                if (newIndex === undefined) {
                    continue;
                }

                const observers = self.observers[i];

                //rebuild observers
                observers.forEach(function (observer) {
                    observer.build(map);
                });

                newStore[newIndex] = observers;
            }


            self.observers = newStore;
        }

        //make sure that no components exist of type scheduled for removal
        existingComponentsRemovalCheck();

        const indexRemapping = computeComponentIndexRemapping();

        updateComponentOccupancy(indexRemapping);

        updateComponentStores(indexRemapping);

        updateObservers(indexRemapping);

        this.componentTypeMap = map;

        // rebuild class->index lookup
        this.__type_to_index_map.clear();
        for (let i = 0; i < newComponentTypeCount; i++) {
            this.__type_to_index_map.set(map[i], i);
        }

        this.componentTypeCount = newComponentTypeCount;
    }

    /**
     *
     * @param {Class[]} types
     * @returns {boolean} true if all types are present, false otherwise
     */
    areComponentTypesRegistered(types) {
        assert.isArray(types, 'types');

        const count = types.length;
        for (let i = 0; i < count; i++) {
            const type = types[i];

            if (!this.isComponentTypeRegistered(type)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Does this dataset have a given component registered?
     * Use {@link registerComponentType}/{@link unregisterComponentType} to alter registered set
     * @param {Class|Function} type
     * @return {boolean}
     */
    isComponentTypeRegistered(type) {
        assert.defined(type, 'type');
        assert.notNull(type, 'type');

        const componentTypeMap = this.getComponentTypeMap();

        return componentTypeMap.indexOf(type) !== -1;
    }

    /**
     *
     * @returns {Class[]}
     */
    getComponentTypeMap() {
        return this.componentTypeMap;
    }

    /**
     *
     * @param {Class[]} types
     * @returns {boolean} false if no new classes were added, true if at least one new class was added
     */
    registerManyComponentTypes(types) {
        const diff = array_set_diff(types, this.componentTypeMap);

        if (diff.uniqueA.length === 0) {
            // all classes area already registered
            return false;
        }

        // new set
        const coalesced = this.componentTypeMap.concat(diff.uniqueA);

        this.setComponentTypeMap(coalesced);

        return true;
    }

    /**
     * Attempt to add a component class to dataset registry
     * @param {Class|Function} type
     * @returns {boolean} true if component successfully added, false if component is already registered
     */
    registerComponentType(type) {
        if (this.isComponentTypeRegistered(type)) {
            // already registered
            return false;
        }

        const classes = this.componentTypeMap.concat([type]);

        this.setComponentTypeMap(classes);

        return true;
    }

    /**
     * Attempt to remove a component class from the registry
     * @param {Class} type
     * @returns {boolean} true iff component is removed, false if it was not registered
     */
    unregisterComponentType(type) {

        if (!this.isComponentTypeRegistered(type)) {
            // not registered
            return false;
        }

        const classes = this.componentTypeMap.slice();

        const t = classes.indexOf(type);

        classes.splice(t, 1);

        this.setComponentTypeMap(classes);

        return true;
    }

    /**
     *
     * @param {number} min_size
     * @returns {void}
     */
    enlargeGenerationTable(min_size) {
        assert.isNonNegativeInteger(min_size, 'min_size');

        const old_generation_table_size = this.entityGeneration.length;

        const new_size = max3(
            min_size,
            Math.ceil(old_generation_table_size * 1.2),
            old_generation_table_size + 16
        );

        const new_generation_table = new Uint32Array(new_size);

        // copy over old data
        new_generation_table.set(this.entityGeneration);

        this.entityGeneration = new_generation_table
    }

    /**
     * Produces generation ID for a given entity
     * NOTE: this method doesn't check for entity's existence, make sure to check that separately if needed
     * @param {number} entity_id
     * @returns {number}
     */
    getEntityGeneration(entity_id) {
        assert.isNonNegativeInteger(entity_id, 'entity_id');
        assert.ok(this.entityExists(entity_id), `Entity ${entity_id} does not exist`);

        return this.entityGeneration[entity_id];
    }

    /**
     * @private
     * @param {number} entity_id
     * @returns {void}
     */
    createEntityUnsafe(entity_id) {
        this.entityOccupancy.set(entity_id, true);

        // record entity generation
        if (this.entityGeneration.length <= entity_id) {
            // needs to be resized
            this.enlargeGenerationTable(entity_id + 1);
        }

        const current_generation = this.generation;
        this.generation = current_generation + 1;

        this.entityGeneration[entity_id] = current_generation;

        this.entityCount++;

        this.onEntityCreated.send1(entity_id);
    }

    /**
     *
     * @returns {number} entityIndex
     */
    createEntity() {
        const entity_id = this.entityOccupancy.nextClearBit(0);

        this.createEntityUnsafe(entity_id);

        return entity_id;
    }

    /**
     *
     * @param {number} entity_id
     * @throws {Error} if entity index is already in use
     * @returns {void}
     */
    createEntitySpecific(entity_id) {
        if (this.entityExists(entity_id)) {
            throw new Error(`EntityId ${entity_id} is already in use`);
        }

        this.createEntityUnsafe(entity_id);
    }

    /**
     *
     * @param {number} entityIndex
     * @returns {boolean}
     */
    entityExists(entityIndex) {
        assert.ok(validateEntityIndex(entityIndex));

        return this.entityOccupancy.get(entityIndex);
    }

    /**
     *
     * @param {number} componentIndex
     * @returns {boolean}
     */
    componentIndexExists(componentIndex) {
        assert.ok(validateComponentIndex(componentIndex));

        return componentIndex >= 0 && componentIndex < this.componentTypeCount;
    }

    /**
     * Remove entity from the dataset, effectively destroying it from the world
     * @param {number} entity_id
     * @returns {boolean} true if entity was removed, false if it doesn't exist
     */
    removeEntity(entity_id) {
        assert.isNonNegativeInteger(entity_id, 'entity_id');

        if (!this.entityExists(entity_id)) {
            // entity doesn't exist
            return false;
        }

        const componentOccupancy = this.componentOccupancy;
        const typeCount = this.componentTypeCount;

        const occupancyStart = entity_id * typeCount;
        const occupancyEnd = occupancyStart + typeCount;

        // remove all components from the entity
        for (
            let i = componentOccupancy.nextSetBit(occupancyStart);
            i < occupancyEnd && i !== -1;
            i = componentOccupancy.nextSetBit(i + 1)
        ) {
            const componentIndex = i % typeCount;
            this.removeComponentFromEntityByIndex_Unchecked(entity_id, componentIndex, i);
        }

        //dispatch event
        this.sendEvent(entity_id, EventType.EntityRemoved, entity_id);

        //purge all event listeners
        delete this.__entityEventListeners[entity_id];
        delete this.__entityAnyEventListeners[entity_id];

        this.entityOccupancy.set(entity_id, false);

        this.entityCount--;

        this.onEntityRemoved.send1(entity_id);

        return true;
    }

    /**
     * Convenience method for removal of multiple entities
     * Works the same as {@link removeEntity} but for multiple elements
     * @param {number[]} entity_ids
     * @returns {void}
     */
    removeEntities(entity_ids) {
        const length = entity_ids.length;
        for (let i = 0; i < length; i++) {
            const entityIndex = entity_ids[i];
            this.removeEntity(entityIndex);
        }
    }

    /**
     *
     * @param {number} entity_id
     * @param {Class} klass
     * @returns {void}
     */
    removeComponentFromEntity(entity_id, klass) {
        const component_index = this.componentTypeMap.indexOf(klass);

        if (component_index === -1) {
            throw new Error(`Component class not found in this dataset`);
        }

        this.removeComponentFromEntityByIndex(entity_id, component_index);
    }

    /**
     *
     * @param {number} entity_id
     * @param {number} component_index
     * @returns {void}
     */
    removeComponentFromEntityByIndex(entity_id, component_index) {
        assert.ok(this.entityExists(entity_id), `entity ${entity_id} does not exist`);
        assert.ok(this.componentIndexExists(component_index), `componentIndex ${component_index} is out of bounds`);

        //check if component exists
        const componentOccupancyIndex = entity_id * this.componentTypeCount + component_index;
        const exists = this.componentOccupancy.get(componentOccupancyIndex);

        if (!exists) {
            //nothing to remove
            console.warn(`Entity ${entity_id} doesn't have a component with index ${component_index}`);
            return;
        }

        this.removeComponentFromEntityByIndex_Unchecked(entity_id, component_index, componentOccupancyIndex);
    }

    /**
     * This method doesn't perform any checks, make sure you understand what you are doing when using it
     * @private
     * @param {number} entity_id
     * @param {number} component_index
     * @param {number} component_occupancy_index
     * @returns {void}
     */
    removeComponentFromEntityByIndex_Unchecked(entity_id, component_index, component_occupancy_index) {
        this.processObservers_ComponentRemoved(entity_id, component_index);

        const componentInstance = this.components[component_index][entity_id];

        //remove component from record
        delete this.components[component_index][entity_id];

        //clear occupancy bit
        this.componentOccupancy.clear(component_occupancy_index);

        //dispatch events
        const componentClass = this.componentTypeMap[component_index];

        //dispatch event to components
        this.sendEvent(entity_id, EventType.ComponentRemoved, { klass: componentClass, instance: componentInstance });
    }

    /**
     * Internally every component class is mapped to an index, this method is used to retrieve such mapping.
     * @param {Function|Class} klass
     * @returns {number} integer index, -1 if not found
     */
    computeComponentTypeIndex(klass) {
        assert.defined(klass, "klass");
        assert.notNull(klass, "klass");

        const idx = this.__type_to_index_map.get(klass);

        if (idx === undefined) {
            return -1;
        }

        return idx;
    }

    /**
     * @template T
     * @param {T} klass
     * @returns {number}
     */
    computeComponentCount(klass) {
        let result = 0;

        this.traverseComponents(klass, function () {
            result++;
        });

        return result;
    }

    /**
     * Retrieves any instance of a given component.
     * If no component instances exist - component will be `null` and entity will be `-1`.
     * Useful for singleton components such as a camera or an audio listener.
     * @template T
     * @param {Class<T>} component_type
     * @returns {{entity:number, component:T}}
     */
    getAnyComponent(component_type) {
        let entity = -1;
        let component = null;

        const index = this.computeComponentTypeIndex(component_type);

        if (index !== -1) {

            const components = this.components[index];
            for (const entity_key in components) {
                const c = components[entity_key];

                if (c !== undefined) {
                    entity = Number(entity_key);
                    component = c;
                    break;
                }

            }
        }

        return {
            entity,
            component
        };
    }

    /**
     *
     * Associate a component with a particular entity.
     * NOTE: An entity can have *AT MOST* one component of a given type.
     * @template C
     * @param {number} entity_id
     * @param {C} component_instance
     * @returns {void}
     */
    addComponentToEntity(entity_id, component_instance) {

        assert.notNull(component_instance, "componentInstance");

        assert.defined(component_instance, "componentInstance");

        /**
         *
         * @type {Class<C>}
         */
        const klass = component_instance.constructor;

        const componentTypeIndex = this.__type_to_index_map.get(klass);

        if (typeof componentTypeIndex !== "number") {
            throw new Error(`Component class not found in this dataset for component_instance ${stringifyComponent(component_instance)}`);
        }

        this.addComponentToEntityByIndex(entity_id, componentTypeIndex, component_instance);
    }

    /**
     * If in doubt, prefer to use {@link addComponentToEntity} instead.
     * @template C
     * @param {number} entity_id
     * @param {number} component_index ordered index of the entity type, matching order in {@link getComponentTypeMap}
     * @param {C} component_instance
     * @returns {void}
     */
    addComponentToEntityByIndex(entity_id, component_index, component_instance) {

        assert.ok(this.entityExists(entity_id), `entity ${entity_id} does not exist`);
        assert.ok(this.componentIndexExists(component_index), `component_index ${component_index} is out of bounds`);

        assert.defined(component_instance, "component_instance");

        assert.equal(this.getComponentByIndex(entity_id, component_index), undefined, `entity ${entity_id} already has component ${component_index}`);


        const componentOccupancyIndex = entity_id * this.componentTypeCount + component_index;

        //record component occupancy
        this.componentOccupancy.set(componentOccupancyIndex, true);

        //inset component instance into component dataset
        this.components[component_index][entity_id] = component_instance;

        //process observers
        this.processObservers_ComponentAdded(entity_id, component_index);

        //dispatch events
        const componentClass = this.componentTypeMap[component_index];

        //dispatch event to components
        this.sendEvent(entity_id, EventType.ComponentAdded, { klass: componentClass, instance: component_instance });
    }

    /**
     * @template C
     * @param {number} entity_id
     * @param {number} component_index
     * @returns {C|undefined}
     */
    getComponentByIndex(entity_id, component_index) {

        assert.ok(this.entityExists(entity_id), `entity ${entity_id} does not exist`);
        assert.ok(this.componentIndexExists(component_index), `component_index ${component_index} is out of bounds`);


        return this.components[component_index][entity_id];
    }

    /**
     * Whether a given entity has a component of the specified class attached
     * @template C
     * @param {number} entity_id
     * @param {Class<C>} klass
     * @returns {boolean}
     */
    hasComponent(entity_id, klass) {
        return this.getComponent(entity_id, klass) !== undefined;
    }

    /**
     * @template T
     * @param {number} entity_id
     * @param {Class<T>} klass
     * @returns {T|undefined}
     */
    getComponent(entity_id, klass) {
        assert.isNonNegativeInteger(entity_id, 'entity_id');
        assert.ok(this.entityExists(entity_id), `entity ${entity_id} does not exist`);
        assert.defined(klass, "klass");

        const componentIndex = this.computeComponentTypeIndex(klass);

        if (componentIndex === -1) {
            // throw  new Error(`Component class ${computeComponentClassName(klass)} not registered in this dataset`);
            return undefined;
        }

        return this.getComponentByIndex(entity_id, componentIndex);
    }

    /**
     * Always returns non-null value, if the component is not found - an error is thrown instead
     * @template C
     * @param {number} entity_id
     * @param {Class<C>} klass
     * @returns {C}
     * @throws {Error} when component not found
     */
    getComponentSafe(entity_id, klass) {
        const component = this.getComponent(entity_id, klass);

        if (component === undefined) {
            throw new Error("Component not found");
        }

        return component;
    }

    /**
     * same as getComponent when component exists, if component is not associated with the entity, callback will be invoked once when it is added.
     * @param {Number} entity_id
     * @param {Class} component_class
     * @param {function} callback
     * @param {*} [thisArg]
     * @returns {void}
     */
    getComponentAsync(entity_id, component_class, callback, thisArg) {
        const component = this.getComponent(entity_id, component_class);

        const handler = (options) => {
            if (options.klass === component_class) {
                this.removeEntityEventListener(entity_id, EventType.ComponentAdded, handler);
                callback.call(thisArg, options.instance);
            }
        }

        if (component === undefined) {
            this.addEntityEventListener(entity_id, EventType.ComponentAdded, handler);
        } else {
            callback.call(thisArg, component);
        }
    };

    /**
     * Performs traversal on a subset of entities which have specified components.
     * @example
     *  ecd.traverseEntities(
     *      [Transform, Renderable, Tag], // Component classes to match
     *      (transform, renderable, tag, entity ) => {  // actual component instances along with entity ID
     *          // do something
     *      }
     *  );
     * @param {Array} classes
     * @param {function(...args):boolean} visitor Visitor can return optional "false" to terminate traversal earlier
     * @param {object} [thisArg] specifies context object on which callbacks are to be called, optional
     * @returns {void}
     */
    traverseEntities(classes, visitor, thisArg) {

        assert.isArray(classes, "classes");
        // assert.notOk(classes.some((c, i) => classes.indexOf(c) !== i), 'classes contains duplicates');
        assert.isFunction(visitor, "visitor");

        let entityIndex, i;

        //map classes to indices
        const indices = scratch_indices;

        // prepare sorted array of component indices
        const numClasses = classes.length;

        for (i = 0; i < numClasses; i++) {
            const k = classes[i];

            const componentIndex = this.computeComponentTypeIndex(k);

            if (componentIndex === -1) {
                // throw new Error(`Component (index=${i}) not found in the dataset`);

                // no chance to match any entities
                return;
            }

            indices[i] = componentIndex;
        }

        const args = scratch_args;

        // crop args array to the right length (since we're re-using a scratch array)
        const args_length_intended = numClasses + 1;

        array_shrink_to_size(scratch_args, args_length_intended);

        const entityOccupancy = this.entityOccupancy;

        const component_type_count = this.componentTypeCount;
        const component_occupancy = this.componentOccupancy;
        const components = this.components;

        entity_loop: for (
            entityIndex = entityOccupancy.nextSetBit(0);
            entityIndex !== -1;
            entityIndex = entityOccupancy.nextSetBit(entityIndex + 1)
        ) {

            const componentOccupancyAddress = entityIndex * component_type_count;

            for (i = 0; i < numClasses; i++) {
                const componentIndex = indices[i];

                const componentPresent = component_occupancy.get(componentOccupancyAddress + componentIndex);

                if (!componentPresent) {
                    continue entity_loop;
                }

                args[i] = components[componentIndex][entityIndex];
            }

            args[numClasses] = entityIndex;

            const keepGoing = visitor.apply(thisArg, args);

            if (keepGoing === false) {
                //stop traversal
                return;
            }
        }
    }

    /**
     * Performs traversal on a subset of entities which have only the specified components and no others
     * @example traverseEntitiesExact([Transform,Renderable,Tag],function(transform, renderable, tag, entity){ ... }, this);
     * @param {Array.<class>} classes
     * @param {Function} visitor
     * @param {Object} [thisArg] specifies context object on which callbacks are to be called, optional
     * @returns {void}
     */
    traverseEntitiesExact(classes, visitor, thisArg) {
        let entityIndex, i;

        //map classes to indices
        const indices = [];

        const numClasses = classes.length;
        for (i = 0; i < numClasses; i++) {
            const k = classes[i];

            const componentIndex = this.computeComponentTypeIndex(k);

            indices[i] = componentIndex;
        }

        const args = [];

        entity_loop: for (entityIndex = this.entityOccupancy.nextSetBit(0); entityIndex !== -1; entityIndex = this.entityOccupancy.nextSetBit(entityIndex + 1)) {
            const componentOccupancyAddress = entityIndex * this.componentTypeCount;
            const componentOccupancyEnd = componentOccupancyAddress + this.componentTypeCount;

            let matched = 0;

            for (
                i = this.componentOccupancy.nextSetBit(componentOccupancyAddress);
                i < componentOccupancyEnd && i !== -1;
                i = this.componentOccupancy.nextSetBit(i + 1)
            ) {
                const componentIndex = i - componentOccupancyAddress;

                const componentPosition = indices.indexOf(componentIndex);

                if (componentPosition === -1) {
                    //undesirable component present (Extra)
                    continue entity_loop;
                }

                matched++;

                args[componentPosition] = this.components[componentIndex][entityIndex];
            }

            if (matched !== numClasses) {
                //Not all components were present
                continue;
            }

            args[numClasses] = entityIndex;

            const keepGoing = visitor.apply(thisArg, args);

            if (keepGoing === false) {
                //stop traversal
                return;
            }
        }
    }

    /**
     * Iterate over all entities
     * @return {Generator<number>}
     */
    * [Symbol.iterator]() {
        const eo = this.entityOccupancy;

        for (
            let i = eo.nextSetBit(0);
            i !== -1;
            i = eo.nextSetBit(i + 1)
        ) {
            yield i;
        }
    }

    /**
     * Traverse all entities with a given component class instance.
     * @example
     *  ecd.traverseComponents(Name, n => n.value = `${n.value} the Great`); // turn every name X to "X the Great", like "John" -> "John the Great"
     *
     * @template T
     * @param {Class<T>} klass
     * @param {function(instance:T, entity:number)} visitor
     * @param {*} [thisArg=undefined] optional `this` argument for the visitor callback
     * @returns {void}
     */
    traverseComponents(klass, visitor, thisArg) {
        const componentTypeIndex = this.computeComponentTypeIndex(klass);

        if (componentTypeIndex === -1) {
            // throw new Error(`Component class is not registered in this dataset`);

            // no chance to match any components
            return;
        }

        this.traverseComponentsByIndex(componentTypeIndex, visitor, thisArg);
    }

    /**
     *
     * @param {number} component_index
     * @param {function} visitor
     * @param {*} [thisArg]
     * @returns {void}
     */
    traverseComponentsByIndex(component_index, visitor, thisArg) {

        assert.isNumber(component_index, "component_index");
        assert.isNonNegativeInteger(component_index, "component_index");
        assert.isFunction(visitor, "visitor");

        this.__traverseComponentsByIndex_via_property(component_index, visitor, thisArg);
    }

    /**
     * Alternative to {@link __traverseComponentsByIndex_via_property} as of 2020, appears to be significantly slower on Chrome with larger datasets
     * @private
     * @param {number} component_index
     * @param {function} visitor
     * @param {*} [thisArg]
     * @returns {void}
     */
    __traverseComponentsByIndex_via_bitset(component_index, visitor, thisArg) {

        const componentDataset = this.components[component_index];
        const componentTypeCount = this.componentTypeCount;

        const entityOccupancy = this.entityOccupancy;
        const componentOccupancy = this.componentOccupancy;

        for (let entityIndex = entityOccupancy.nextSetBit(0); entityIndex !== -1; entityIndex = entityOccupancy.nextSetBit(entityIndex + 1)) {

            const componentOccupancyIndex = entityIndex * componentTypeCount + component_index;

            if (componentOccupancy.get(componentOccupancyIndex)) {

                const componentInstance = componentDataset[entityIndex];

                const continueFlag = visitor.call(thisArg, componentInstance, entityIndex);

                if (continueFlag === false) {
                    //stop traversal
                    break;
                }

            }
        }
    }

    /**
     * @private
     * @param {number} component_index
     * @param {function} visitor
     * @param {*} [thisArg]
     * @returns {void}
     */
    __traverseComponentsByIndex_via_property(component_index, visitor, thisArg) {

        const componentDataset = this.components[component_index];

        for (const entity_key in componentDataset) {

            const entityIndex = Number.parseInt(entity_key);

            const componentInstance = componentDataset[entityIndex];

            if (componentInstance === undefined) {
                continue;
            }

            const continueFlag = visitor.call(thisArg, componentInstance, entityIndex);

            if (continueFlag === false) {
                //stop traversal
                break;
            }
        }

    }

    /**
     * @private
     * @param {number} entity_id
     * @param {number} component_index
     * @returns {void}
     */
    processObservers_ComponentAdded(entity_id, component_index) {
        const observers = this.observers;

        const observersStore = observers[component_index];

        const numObservers = observersStore.length;

        if (numObservers === 0) {
            // no observers
            return;
        }

        let i = 0;

        const args = [];

        for (; i < numObservers; i++) {
            const observer = observersStore[i];

            const match = matchComponentMask(this.componentOccupancy, entity_id, this.componentTypeCount, observer.componentMask);

            if (match) {
                //match completing addition
                buildObserverCallbackArgs(entity_id, observer.componentMask, observer.componentIndexMapping, this.components, args);

                //write entityId argument
                args[observer.componentTypeCount] = entity_id;

                //invoke callback
                observer.callbackComplete.apply(observer.thisArg, args);
            }
        }
    }

    /**
     * @private
     * @param {number} entity_id
     * @param {number} component_index
     * @returns {void}
     */
    processObservers_ComponentRemoved(entity_id, component_index) {
        const observers = this.observers;

        const observersStore = observers[component_index];

        const numObservers = observersStore.length;

        let i;

        const args = [];

        for (i = 0; i < numObservers; i++) {
            const observer = observersStore[i];

            const match = matchComponentMask(this.componentOccupancy, entity_id, this.componentTypeCount, observer.componentMask);

            if (match) {
                //match breaking removal
                buildObserverCallbackArgs(entity_id, observer.componentMask, observer.componentIndexMapping, this.components, args);

                //write entityId argument
                args[observer.componentTypeCount] = entity_id;

                //invoke callback
                observer.callbackBroken.apply(observer.thisArg, args);
            }
        }
    }

    /**
     * Captures all events for a given entity
     * @param {number} entity
     * @param {function} listener
     * @param {*} [thisArg]
     * @returns {void}
     *
     * @see removeEntityAnyEventListener
     * @see addEntityEventListener
     */
    addEntityAnyEventListener(entity, listener, thisArg) {

        if (!this.entityExists(entity)) {
            throw new Error(`Entity '${entity}' does not exist`);
        }

        const evl = this.__entityAnyEventListeners;

        let handlers = evl[entity];

        if (handlers === void 0) {
            handlers = [];
            evl[entity] = handlers;
        }

        const handler = new SignalHandler(listener, thisArg);

        handlers.push(handler);
    }

    /**
     * Removed listener attached via {@link #addEntityAnyEventListener}
     * @param {number} entity
     * @param {function} listener
     * @param {*} [thisArg]
     * @returns {boolean}
     *
     * @see addEntityAnyEventListener
     * @see removeEntityEventListener
     */
    removeEntityAnyEventListener(entity, listener, thisArg) {
        assert.isNonNegativeInteger(entity, "entity");
        assert.isFunction(listener, "listener");

        const evl = this.__entityAnyEventListeners;

        let handlers = evl[entity];

        if (handlers === void 0) {
            return false;
        }

        let index;

        if (thisArg === undefined) {
            index = findSignalHandlerIndexByHandle(handlers, listener);
        } else {
            index = findSignalHandlerIndexByHandleAndContext(handlers, listener, thisArg);
        }

        if (index < 0) {
            //listener was not found
            return false;
        }

        //remove the listener
        handlers.splice(index, 1);

        return true;
    }

    /**
     * Registers an event listener to a specific entity and event type, specified by event_name string.
     * @param {number} entity
     * @param {string} event_name
     * @param {function()} listener
     * @param {*} [thisArg]
     * @returns {void}
     *
     * @see sendEvent
     * @see removeEntityEventListener
     */
    addEntityEventListener(entity, event_name, listener, thisArg) {
        assert.isNonNegativeInteger(entity, "entity");
        assert.isString(event_name, "event_name");
        assert.isFunction(listener, "listener");

        if (!this.entityExists(entity)) {
            throw new Error(`Entity '${entity}' does not exist`);
        }

        const evl = this.__entityEventListeners;

        let hash = evl[entity];

        if (hash === void 0) {
            hash = {};
            evl[entity] = hash;
        }

        let listeners = hash[event_name];

        if (listeners === void 0) {
            listeners = [];
            hash[event_name] = listeners;
        }

        const handler = new SignalHandler(listener, thisArg);

        listeners.push(handler);
    }

    /**
     * Remove existing event listener associated with a specific entity. Does nothing if listener isn't registered.
     * @param {number} entity
     * @param {string} event_name
     * @param {function} listener
     * @param {*} [thisArg]
     * @returns {boolean}
     *
     * @see addEntityEventListener
     * @see sendEvent
     */
    removeEntityEventListener(entity, event_name, listener, thisArg) {
        assert.isNonNegativeInteger(entity, "entity");
        assert.isString(event_name, "event_name");
        assert.isFunction(listener, "listener");

        const evl = this.__entityEventListeners;

        const hash = evl[entity];

        if (hash === undefined) {
            //no event listeners exist for this entity
            return false;
        }

        const listeners = hash[event_name];

        if (listeners === undefined) {
            //no event listeners exist for this event
            return false;
        }

        let index;

        if (thisArg === undefined) {
            index = findSignalHandlerIndexByHandle(listeners, listener);
        } else {
            index = findSignalHandlerIndexByHandleAndContext(listeners, listener, thisArg);
        }

        if (index < 0) {
            //listener was not found
            return false;
        }

        //remove the listener
        listeners.splice(index, 1);

        return true;
    }

    /**
     * Notifies every component of specified entity with given event
     * @param {number} entity
     * @param {string} name event name
     * @param {Object} [event=undefined]
     * @returns {void}
     * @see addEntityEventListener
     */
    sendEvent(entity, name, event) {
        // console.log("sendEvent", entity, name, event);

        assert.isString(name, "name");
        assert.isNonNegativeInteger(entity, "entity");

        dispatchEntityEventListeners(this.__entityEventListeners, entity, name, event);


        // handle "any" event listeners on entities
        const anyEventListeners = this.__entityAnyEventListeners;

        const handlers = anyEventListeners[entity];
        if (handlers !== undefined) {
            dispatchAnyEventListenersByHash(handlers, entity, name, event);
        }
    }

    /**
     *
     * @param {number} entity_id
     * @returns {boolean}
     */
    entityHasComponents(entity_id) {
        const components = this.getAllComponents(entity_id);
        const n = components.length;

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

            if (component !== undefined) {
                return true;
            }
        }

        return false;
    }

    /**
     * Remove all entities, triggers all relevant removal events for each entity/component
     * @returns {void}
     * @see dropData
     */
    clear() {
        const entityOccupancy = this.entityOccupancy;

        for (let i = entityOccupancy.nextSetBit(0); i !== -1; i = entityOccupancy.nextSetBit(i + 1)) {
            this.removeEntity(i);
        }
    }

    /**
     * Drops all data, bypassing standard workflow, no events are dispatched. Data is simply ejected.
     * This method is very efficient but is intended to be used only when associated data is not needed as it may produce unintended consequences if the data is re-used
     * @returns {void}
     * @see clear
     */
    dropData() {
        //drop components
        for (let i = 0; i < this.componentTypeCount; i++) {
            this.components[i] = [];
        }

        this.componentOccupancy.reset();
        this.entityOccupancy.reset();

        this.entityCount = 0;

        this.__entityEventListeners = [];
    }

    /**
     * @template T
     * @param {string} class_name
     * @returns {null|function|Class<T>}
     */
    getComponentClassByName(class_name) {
        let i = 0;

        for (; i < this.componentTypeCount; i++) {

            const componentClass = this.componentTypeMap[i];
            const name = componentClass.typeName;

            if (name === class_name) {
                return componentClass;
            }

        }

        return null;
    }

    /**
     * Copy entities from a supplied input dataset. Only components present in the mask are copied
     * @param {Array} component_classes
     * @param {EntityComponentDataset} source
     * @returns {void}
     */
    maskedCopy(source, component_classes) {

        let i;

        const componentCount = component_classes.length;

        const sourceComponentIndices = [];
        const thisComponentIndices = [];

        for (i = 0; i < componentCount; i++) {
            const componentClass = component_classes[i];

            const sourceComponentIndex = source.computeComponentTypeIndex(componentClass);

            if (sourceComponentIndex === -1) {
                //source doesn't have this class, this just means there's nothing to copy. skip
                continue;
            }

            const thisComponentIndex = this.computeComponentTypeIndex(componentClass);

            if (thisComponentIndex === -1) {
                //this dataset doesn't have component class registered
                throw new Error(`This dataset does not have component class registered`);
            }

            sourceComponentIndices.push(sourceComponentIndex);
            thisComponentIndices.push(thisComponentIndex);
        }

        const copyingComponentClassCount = sourceComponentIndices.length;

        const sourceEntityOccupancy = source.entityOccupancy;
        const sourceComponentOccupancy = source.componentOccupancy;
        const sourceComponentTypeCount = source.componentTypeCount;

        let entityIndex;

        for (entityIndex = sourceEntityOccupancy.nextSetBit(0); entityIndex !== -1; entityIndex = sourceEntityOccupancy.nextSetBit(entityIndex + 1)) {

            this.createEntitySpecific(entityIndex);

            for (i = 0; i < copyingComponentClassCount; i++) {

                const sourceComponentIndex = sourceComponentIndices[i];

                const componentExists = sourceComponentOccupancy.get(sourceComponentTypeCount * entityIndex + sourceComponentIndex);

                if (componentExists) {

                    const thisComponentIndex = thisComponentIndices[i];

                    const componentInstance = source.getComponentByIndex(entityIndex, sourceComponentIndex);

                    this.addComponentToEntityByIndex(entityIndex, thisComponentIndex, componentInstance);

                }
            }
        }
    }

    /**
     * Main utility of this method is to facilitate serialization.
     * @param {Array} component_classes
     * @param {function(componentIndex:number,components:Array, componentCount:number)} visitor
     * @returns {void}
     */
    traverseEntitiesCompactedFiltered(component_classes, visitor) {

        const inputClassCount = component_classes.length;

        const componentIndices = new Array(inputClassCount);

        //map component classes to indices

        let i, j;

        for (i = 0; i < inputClassCount; i++) {
            const klass = component_classes[i];
            componentIndices[i] = this.computeComponentTypeIndex(klass);
        }

        //sort component indices array for faster traversal
        componentIndices.sort();

        const entityOccupancy = this.entityOccupancy;
        const componentOccupancy = this.componentOccupancy;

        const componentTypeCount = this.componentTypeCount;

        const components = [];
        let componentCount;

        for (i = entityOccupancy.nextSetBit(0); i !== -1; i = entityOccupancy.nextSetBit(i + 1)) {

            const entityOccupancyAddress = i * componentTypeCount;

            componentCount = 0;

            for (j = 0; j < inputClassCount; j++) {

                const componentIndex = componentIndices[j];
                if (componentOccupancy.get(entityOccupancyAddress + componentIndex)) {
                    //component exists
                    components[componentCount] = this.components[componentIndex][i];

                    componentCount++;
                }

            }

            visitor(i, components, componentCount);
        }
    }

    /**
     * Tells if dataset has any entities
     * @returns {boolean}
     */
    isEmpty() {
        return this.entityCount === 0;
    }

    /**
     * Note that {@link EntityComponentDataset} also implements `Iterable<number>`
     * @param {function(entityIndex:number)} visitor
     * @param {*} [thisArg]
     * @returns {void}
     */
    traverseEntityIndices(visitor, thisArg) {
        let entity_id;

        const occupancy = this.entityOccupancy;

        for (
            entity_id = occupancy.nextSetBit(0);
            entity_id !== -1;
            entity_id = occupancy.nextSetBit(entity_id + 1)
        ) {

            visitor.call(thisArg, entity_id);

        }
    }
}

/**
 * Useful for fast type checks that doesn't require an import
 * `x.isEntityComponentDataset === true` vs `x instanceof EntityComponentDataset`
 * @readonly
 * @type {boolean}
 */
EntityComponentDataset.prototype.isEntityComponentDataset = true;

// aliases

/**
 * @deprecated use [Symbol.iterator] directly instead
 */
EntityComponentDataset.prototype.createEntityIterator = EntityComponentDataset.prototype[Symbol.iterator];

/**
 * @deprecated use {@link removeEntityAnyEventListener} directly instead
 */
EntityComponentDataset.prototype.removeAnyEventListener = EntityComponentDataset.prototype.removeEntityAnyEventListener;


function stringifyComponent(c) {
    if (typeof c === "object") {
        return c.constructor.toString();
    } else {
        return c;
    }
}


/**
 *
 * @param {Array<Object<SignalHandler[]>>} evl
 * @param {number} entity
 * @param {string} name
 * @param {*} event
 */
function dispatchEntityEventListeners(evl, entity, name, event) {
    const hash = evl[entity];
    if (hash !== void 0) {
        dispatchEventListenersByHash(hash, entity, name, event);
    }


}

/**
 *
 * @type {SignalHandler[]}
 */
const entityListenersProxy = [];

/**
 * Keep track of the stack pointer to allow recursive dispatching
 * @type {number}
 */
let entityListenersProxyPointer = 0;

/**
 *
 * @param {Object<SignalHandler[]>} hash
 * @param {number} entity
 * @param {string} name
 * @param {*} event
 */
function dispatchEventListenersByHash(hash, entity, name, event) {
    const listeners = hash[name];

    if (listeners === void 0) {
        return;
    }

    const numListeners = listeners.length;

    if (numListeners === 0) {
        //no listeners, nothing to do
        return;
    }

    const pointerOffset = entityListenersProxyPointer;

    // reserve space on the stack
    entityListenersProxyPointer += numListeners;

    let i;

    //copy listeners to prevent possible modification errors
    for (i = 0; i < numListeners; i++) {
        const signalHandler = listeners[i];

        entityListenersProxy[i + pointerOffset] = signalHandler;
    }


    //invoke handlers
    for (let i = 0; i < numListeners; i++) {
        const handler = entityListenersProxy[i + pointerOffset];

        handler.handle.call(handler.context, event, entity);
    }

    //reset proxy
    entityListenersProxyPointer = pointerOffset;
}

/**
 *
 * @param {SignalHandler[]} listeners
 * @param {number} entity
 * @param {string} name
 * @param {*} event
 */
function dispatchAnyEventListenersByHash(listeners, entity, name, event) {

    const numListeners = listeners.length;

    if (numListeners === 0) {
        //no listeners, nothing to do
        return;
    }

    const pointerOffset = entityListenersProxyPointer;

    // reserve space on the stack
    entityListenersProxyPointer += numListeners;

    let i;

    //copy listeners to prevent possible modification errors
    for (i = 0; i < numListeners; i++) {
        const signalHandler = listeners[i];

        entityListenersProxy[i + pointerOffset] = signalHandler;
    }


    //invoke handlers
    for (let i = 0; i < numListeners; i++) {
        const handler = entityListenersProxy[i + pointerOffset];

        handler.handle.call(handler.context, name, event, entity);
    }

    //reset proxy
    entityListenersProxyPointer = pointerOffset;
}
