import { assert } from "../../core/assert.js";
import Signal from "../../core/events/signal/Signal.js";
import { EntityFlags } from "./EntityFlags.js";
import { EntityReference } from "./EntityReference.js";
import { EventType } from "./EventType.js";

/**
 * Set of default flags
 * @type {number}
 */
const DEFAULT_FLAGS =
    EntityFlags.RegisterComponents
    | EntityFlags.WatchDestruction
;

/**
 * Representation of an entity, helps build entities and keep track of them without having to access {@link EntityComponentDataset} directly
 *
 *
 * @example
 * const entity = new Entity();
 *
 * // Add components to the entity.
 * entity.add(new Position(10, 20));
 * entity.add(new Velocity(1, 2));
 *
 * // Get a component (would return null if the entity doesn't have it).
 * const position = entity.getComponent(Position); // { x:10, y:20 }
 *
 * // Remove a component.
 * entity.removeComponent(Velocity);
 *
 * // To actually use the entity in a game, you need to build it using an EntityComponentDataset.
 * const dataset = new EntityComponentDataset(); // You would typically have one already
 *
 * entity.build(dataset); // add entity with the components to the scene
 *
 * // ... Once entity is no longer needed, we destroy it
 * entity.destroy();
 *
 * @see {@link EntityComponentDataset}
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class Entity {

    /**
     * Reference to the entity in a dataset. Do not modify directly.
     * Only valid when the entity is built, check with {@link isBuilt}.
     * @readonly
     * @type {EntityReference}
     */
    reference = new EntityReference();


    /**
     * ID of the entity. Shortcut to {@link reference.id}
     * @returns {number}
     */
    get id() {
        return this.reference.id;
    }

    /**
     * Entity's generation tag. Shortcut to {@link reference.generation}
     * @returns {number}
     */
    get generation() {
        return this.reference.generation;
    }

    /**
     * Components associated with the entity. Do not modify directly
     * @readonly
     * @type {Array}
     */
    components = [];

    /**
     * Listeners added before the entity is build live here
     * @private
     * @type {{name:string,listener:(function|Function), context:*}[]}
     */
    #deferredListeners = [];

    /**
     * Dataset in which this entity exists, if built. If not built - null.
     * Do not modify directly.
     * @type {EntityComponentDataset}
     * @see {@link build}
     * @see {@link destroy}
     */
    dataset = null;

    /**
     * Do not modify directly unless you understand the consequences of doing so.
     * @type {EntityFlags|number}
     */
    flags = DEFAULT_FLAGS;

    /**
     * Arbitrary user data, add anything you want here.
     * @readonly
     * @type {Object}
     */
    properties = {};

    /**
     * @readonly
     */
    on = {
        /**
         * Fired when {@link build} is called
         */
        built: new Signal()
    };

    /**
     * Handles event when entity is removed without invoking {@link #destroy} method
     * @private
     */
    #handleEntityDestroyed() {
        this.clearFlag(EntityFlags.Built);
    }

    /**
     *
     * @param {number|EntityFlags} value
     */
    setFlag(value) {
        this.flags |= value;
    }

    /**
     *
     * @param {number|EntityFlags} flag
     * @returns {boolean}
     */
    getFlag(flag) {
        assert.isNumber(flag, "flag");

        return (this.flags & flag) !== 0;
    }

    /**
     *
     * @param {number|EntityFlags} flag
     */
    clearFlag(flag) {
        this.flags &= ~flag;
    }

    /**
     * True is the entity is currently attached to a dataset. When this is `true`, {@link dataset} property will be set as well.
     * @readonly
     * @returns {boolean}
     * @see {@link build}
     * @see {@link destroy}
     *
     */
    get isBuilt() {
        return this.getFlag(EntityFlags.Built);
    }

    /**
     * Remove all components from the entity
     */
    removeAllComponents() {
        const elements = this.components;
        const n = elements.length;
        for (let i = n - 1; i >= 0; i--) {
            const component = elements[i];

            const ComponentClass = Object.getPrototypeOf(component).constructor;

            this.removeComponent(ComponentClass);
        }
    }

    /**
     * Number of attached components
     * @return {number}
     */
    get count() {
        return this.components.length;
    }

    /**
     * Note that this is live-edit, if the {@link Entity} is built - component will be added to the dataset as well.
     *
     * @template T class of the component
     * @param {T} component_instance
     * @returns {Entity}
     */
    add(component_instance) {
        assert.defined(component_instance, 'component_instance');
        assert.notNull(component_instance, 'component_instance');

        assert.notOk(this.hasComponent(Object.getPrototypeOf(component_instance).constructor), 'Component of this type already exists');

        this.components.push(component_instance);

        if (this.getFlag(EntityFlags.Built)) {

            //already built, add component to entity

            if (this.getFlag(EntityFlags.RegisterComponents)) {
                this.dataset.registerComponentType(component_instance.constructor)
            }

            this.dataset.addComponentToEntity(this.reference.id, component_instance);
        }

        return this;
    }

    /**
     * Check if a component of a given type is present on this entity.
     * @template T
     * @param {T} klass type of the component
     * @returns {boolean}
     */
    hasComponent(klass) {
        return this.getComponent(klass) !== null;
    }

    /**
     * @template T
     * @param {Class<T>} klass type of the component
     * @returns {T|null} component of specified class
     */
    getComponent(klass) {
        const elements = this.components;
        const element_count = elements.length;

        for (let i = 0; i < element_count; i++) {
            const component = elements[i];
            if (component instanceof klass) {
                return component;
            }
        }

        return null;
    }

    /**
     * Similar to {@link #getComponent}, instead of returning null - throws an exception
     * @template T
     * @param {Class<T>} klass
     * @returns {T}
     */
    getComponentSafe(klass) {
        const component = this.getComponent(klass);

        if (component === null) {
            throw new Error(`Component of given class '${computeComponentClassName(klass)}' not found`);
        }

        return component;
    }

    /**
     * Note that this is live-edit, if the {@link Entity} is built - component will be removed from the dataset as well.
     * @param {function} klass
     * @returns {*|null}
     */
    removeComponent(klass) {
        assert.defined(klass, 'klass');

        const elements = this.components;
        const n = elements.length;

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

            if (component instanceof klass) {
                elements.splice(i, 1);

                //see if entity is built
                if (this.getFlag(EntityFlags.Built)) {

                    this.dataset.removeComponentFromEntity(this.reference.id, klass);

                }

                return component;
            }

        }
        return null;
    }

    /**
     *
     * @param {string} eventName
     * @param {*} [event]
     */
    sendEvent(eventName, event) {
        assert.isString(eventName, "eventName");

        if (this.getFlag(EntityFlags.Built)) {
            this.dataset.sendEvent(this.reference.id, eventName, event);
        } else {
            console.warn("Entity doesn't exist. Event " + eventName + ":" + event + " was not sent.")
        }
    }

    /**
     *
     * @param {string} eventName
     */
    promiseEvent(eventName) {
        assert.isString(eventName, "eventName");

        return new Promise((resolve, reject) => {

            const handle_event = () => {

                this.removeEventListener(eventName, handle_event);
                this.removeEventListener(EventType.EntityRemoved, reject);

                resolve();
            }


            this.addEventListener(eventName, handle_event);
            this.removeEventListener(EventType.EntityRemoved, reject);
        });
    }

    /**
     *
     * @param {string} eventName
     * @param {function} listener
     * @param {*} [context]
     * @returns {Entity}
     */
    addEventListener(eventName, listener, context) {
        assert.isString(eventName, "eventName");
        assert.isFunction(listener, "listener");

        if (this.getFlag(EntityFlags.Built)) {
            this.dataset.addEntityEventListener(this.reference.id, eventName, listener, context);
        } else {
            this.#deferredListeners.push({
                name: eventName,
                listener: listener,
                context
            });
        }
        return this;
    }

    /**
     *
     * @param {string} eventName
     * @param {function} listener
     * @param {*} [context]
     * @returns {Entity}
     */
    removeEventListener(eventName, listener, context) {
        assert.isString(eventName, "eventName");
        assert.isFunction(listener, "listener");

        if (this.getFlag(EntityFlags.Built)) {
            this.dataset.removeEntityEventListener(this.reference.id, eventName, listener, context);
        } else {
            const listeners = this.#deferredListeners;

            for (let i = 0, numListeners = listeners.length; i < numListeners; i++) {
                const deferredDescriptor = listeners[i];

                if (
                    deferredDescriptor.name === eventName
                    && deferredDescriptor.listener === listener
                    && deferredDescriptor.context === context
                ) {

                    listeners.splice(i, 1);

                    i--;
                    numListeners--;
                }
            }
        }
        return this;
    }

    /**
     * Removes built entity from the {@link EntityManager}.
     * Note that the destroyed {@link Entity} can be later re-built.
     * @returns {boolean} true if entity was destroyed, false if entity was not built
     * @see {@link build}
     * @see {@link isBuilt}
     */
    destroy() {
        if (!this.getFlag(EntityFlags.Built)) {
            // not built, do nothing
            return false;
        }

        const dataset = this.dataset;
        const entity = this.reference.id;

        //check that the entity is the same as what we have built
        //assert.ok(checkExistingComponents(entity, this.element, dataset), `Signature of Entity does not match existing entity(id=${entity})`);

        dataset.removeEntityEventListener(entity, EventType.EntityRemoved, this.#handleEntityDestroyed, this);

        dataset.removeEntity(entity);

        // clear reference
        this.reference.copy(EntityReference.NULL);

        this.clearFlag(EntityFlags.Built);

        return true;
    }

    /**
     * Builds entity in the given dataset.
     *
     * @example
     *  const ecd:EntityComponentDataset = ...;
     *  const entity:Entity = new Entity();
     *  entity.add(new Position(10, 20));
     *  entity.add(new Velocity(1, 2));
     *  const entity_id = entity.build(ecd);
     *
     * @returns {number} entity ID
     * @param {EntityComponentDataset} dataset
     * @see {@link destroy}
     * @see {@link isBuilt}
     *
     */
    build(dataset) {
        assert.defined(dataset, "dataset");
        assert.notNull(dataset, "dataset");
        assert.isObject(dataset, 'dataset');
        assert.equal(dataset.isEntityComponentDataset, true, 'dataset.isEntityComponentDataset !== true');

        if (
            this.getFlag(EntityFlags.Built)
            && checkExistingComponents(this.reference.id, this.components, dataset)
        ) {
            // TODO use 'generation' to avoid checking components
            //already built
            return this.reference.id;
        }

        const entity = dataset.createEntity();
        this.reference.bind(dataset, entity);

        this.dataset = dataset;

        let i;

        const listeners = this.#deferredListeners;
        const listeners_count = listeners.length;

        for (i = 0; i < listeners_count; i++) {
            const subscription = listeners[i];
            dataset.addEntityEventListener(entity, subscription.name, subscription.listener, subscription.context);
        }
        // reset listeners
        this.#deferredListeners.splice(0, listeners_count);

        const element = this.components;
        const element_count = element.length;

        if (this.getFlag(EntityFlags.RegisterComponents)) {
            for (i = 0; i < element_count; i++) {
                const component = element[i];

                dataset.registerComponentType(component.constructor);
            }
        }

        for (i = 0; i < element_count; i++) {
            const component = element[i];

            dataset.addComponentToEntity(entity, component);
        }

        this.setFlag(EntityFlags.Built);

        if (this.getFlag(EntityFlags.WatchDestruction)) {
            dataset.addEntityEventListener(entity, EventType.EntityRemoved, this.#handleEntityDestroyed, this);
        }

        this.on.built.send2(entity, dataset);
        return entity;
    }

    /**
     * Extract data about an entity and its components from a dataset.
     * Note that this behaves the same way as if you first created the {@link Entity} and then called {@link build} on it, just the other way around.
     *
     * @example
     *  const ecd:EntityComponentDataset = ...;
     *  const entity_id = 7;
     *  const entity:Entity = Entity.readFromDataset(entity_id, ecd);
     *
     * @param {number} entity
     * @param {EntityComponentDataset} dataset
     * @returns {Entity}
     */
    static readFromDataset(entity, dataset) {
        assert.isNonNegativeInteger(entity, "entity");
        assert.defined(dataset, "dataset");
        assert.notNull(dataset, "dataset");
        assert.equal(dataset.isEntityComponentDataset, true, "dataset.isEntityComponentDataset !== true");

        const r = new Entity();

        dataset.readEntityComponents(r.components, 0, entity);

        r.setFlag(EntityFlags.Built);

        r.dataset = dataset;

        r.reference.bind(dataset, entity);

        return r;
    }
}

/**
 * Useful for a faster alternative to `instanceof` checks
 * @readonly
 * @type {boolean}
 */
Entity.prototype.isEntity = true;


/**
 *
 * @param {int} entity
 * @param {Array} components
 * @param {EntityComponentDataset} dataset
 */
function checkExistingComponents(entity, components, dataset) {
    if (!dataset.entityExists(entity)) {
        return false;
    }

    const numComponents = components.length;

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

        const actual = dataset.getComponent(entity, component.constructor);

        if (actual !== component) {
            return false;
        }
    }

    return true;
}

export default Entity;
