import { isComponentCtor } from './Component';
import { Observable } from '../Util/Observable';
import { InitializeEvent, PreUpdateEvent, PostUpdateEvent } from '../Events';
import { KillEvent } from '../Events';
import { EventEmitter } from '../EventEmitter';
import { removeItemFromArray } from '../Util/Util';
/**
 * AddedComponent message
 */
export class AddedComponent {
    constructor(data) {
        this.data = data;
        this.type = 'Component Added';
    }
}
/**
 * Type guard to know if message is f an Added Component
 */
export function isAddedComponent(x) {
    return !!x && x.type === 'Component Added';
}
/**
 * RemovedComponent message
 */
export class RemovedComponent {
    constructor(data) {
        this.data = data;
        this.type = 'Component Removed';
    }
}
/**
 * Type guard to know if message is for a Removed Component
 */
export function isRemovedComponent(x) {
    return !!x && x.type === 'Component Removed';
}
export const EntityEvents = {
    Initialize: 'initialize',
    PreUpdate: 'preupdate',
    PostUpdate: 'postupdate',
    Kill: 'kill'
};
/**
 * An Entity is the base type of anything that can have behavior in Excalibur, they are part of the built in entity component system
 *
 * Entities can be strongly typed with the components they contain
 *
 * ```typescript
 * const entity = new Entity<ComponentA | ComponentB>();
 * entity.components.a; // Type ComponentA
 * entity.components.b; // Type ComponentB
 * ```
 */
export class Entity {
    constructor(componentsOrOptions, name) {
        /**
         * The unique identifier for the entity
         */
        this.id = Entity._ID++;
        this.name = `Entity#${this.id}`;
        /**
         * Listen to or emit events for an entity
         */
        this.events = new EventEmitter();
        this._tags = new Set();
        this.componentAdded$ = new Observable();
        this.componentRemoved$ = new Observable();
        this.tagAdded$ = new Observable();
        this.tagRemoved$ = new Observable();
        /**
         * Current components on the entity
         *
         * **Do not modify**
         *
         * Use addComponent/removeComponent otherwise the ECS will not be notified of changes.
         */
        this.components = new Map();
        this._componentsToRemove = [];
        this._instanceOfComponentCacheDirty = true;
        this._instanceOfComponentCache = new Map();
        /**
         * The current scene that the entity is in, if any
         */
        this.scene = null;
        /**
         * Whether this entity is active, if set to false it will be reclaimed
         */
        this.active = true;
        this._parent = null;
        this.childrenAdded$ = new Observable();
        this.childrenRemoved$ = new Observable();
        this._children = [];
        this._isInitialized = false;
        let componentsToAdd;
        let nameToAdd;
        if (Array.isArray(componentsOrOptions)) {
            componentsToAdd = componentsOrOptions;
            nameToAdd = name;
        }
        else if (componentsOrOptions && typeof componentsOrOptions === 'object') {
            const { components, name } = componentsOrOptions;
            componentsToAdd = components;
            nameToAdd = name;
        }
        if (nameToAdd) {
            this.name = nameToAdd;
        }
        if (componentsToAdd) {
            for (const component of componentsToAdd) {
                this.addComponent(component);
            }
        }
        // this.addComponent(this.tagsComponent);
    }
    /**
     * Kill the entity, means it will no longer be updated. Kills are deferred to the end of the update.
     * If parented it will be removed from the parent when killed.
     */
    kill() {
        if (this.active) {
            this.active = false;
            this.unparent();
        }
        this.emit('kill', new KillEvent(this));
    }
    isKilled() {
        return !this.active;
    }
    /**
     * Specifically get the tags on the entity from [[TagsComponent]]
     */
    get tags() {
        return this._tags;
    }
    /**
     * Check if a tag exists on the entity
     * @param tag name to check for
     */
    hasTag(tag) {
        return this._tags.has(tag);
    }
    /**
     * Adds a tag to an entity
     * @param tag
     */
    addTag(tag) {
        this._tags.add(tag);
        this.tagAdded$.notifyAll(tag);
        return this;
    }
    /**
     * Removes a tag on the entity
     *
     * Removals are deferred until the end of update
     * @param tag
     */
    removeTag(tag) {
        this._tags.delete(tag);
        this.tagRemoved$.notifyAll(tag);
        return this;
    }
    /**
     * The types of the components on the Entity
     */
    get types() {
        return Array.from(this.components.keys());
    }
    /**
     * Returns all component instances on entity
     */
    getComponents() {
        return Array.from(this.components.values());
    }
    /**
     * Verifies that an entity has all the required types
     * @param requiredTypes
     */
    hasAll(requiredTypes) {
        for (let i = 0; i < requiredTypes.length; i++) {
            if (!this.components.has(requiredTypes[i])) {
                return false;
            }
        }
        return true;
    }
    /**
     * Verifies that an entity has all the required tags
     * @param requiredTags
     */
    hasAllTags(requiredTags) {
        for (let i = 0; i < requiredTags.length; i++) {
            if (!this.tags.has(requiredTags[i])) {
                return false;
            }
        }
        return true;
    }
    _getCachedInstanceOfType(type) {
        if (this._instanceOfComponentCacheDirty) {
            this._instanceOfComponentCacheDirty = false;
            this._instanceOfComponentCache.clear();
        }
        if (this._instanceOfComponentCache.has(type)) {
            return this._instanceOfComponentCache.get(type);
        }
        for (const instance of this.components.values()) {
            if (instance instanceof type) {
                this._instanceOfComponentCache.set(type, instance);
                return instance;
            }
        }
        return undefined;
    }
    get(type) {
        const maybeComponent = this._getCachedInstanceOfType(type);
        return maybeComponent !== null && maybeComponent !== void 0 ? maybeComponent : this.components.get(type);
    }
    get parent() {
        return this._parent;
    }
    /**
     * Get the direct children of this entity
     */
    get children() {
        return this._children;
    }
    /**
     * Unparents this entity, if there is a parent. Otherwise it does nothing.
     */
    unparent() {
        if (this._parent) {
            this._parent.removeChild(this);
            this._parent = null;
        }
    }
    /**
     * Adds an entity to be a child of this entity
     * @param entity
     */
    addChild(entity) {
        if (entity.parent === null) {
            if (this.getAncestors().includes(entity)) {
                throw new Error('Cycle detected, cannot add entity');
            }
            this._children.push(entity);
            entity._parent = this;
            this.childrenAdded$.notifyAll(entity);
        }
        else {
            throw new Error('Entity already has a parent, cannot add without unparenting');
        }
        return this;
    }
    /**
     * Remove an entity from children if it exists
     * @param entity
     */
    removeChild(entity) {
        if (entity.parent === this) {
            removeItemFromArray(entity, this._children);
            entity._parent = null;
            this.childrenRemoved$.notifyAll(entity);
        }
        return this;
    }
    /**
     * Removes all children from this entity
     */
    removeAllChildren() {
        // Avoid modifying the array issue by walking backwards
        for (let i = this.children.length - 1; i >= 0; i--) {
            this.removeChild(this.children[i]);
        }
        return this;
    }
    /**
     * Returns a list of parent entities starting with the topmost parent. Includes the current entity.
     */
    getAncestors() {
        const result = [this];
        let current = this.parent;
        while (current) {
            result.push(current);
            current = current.parent;
        }
        return result.reverse();
    }
    /**
     * Returns a list of all the entities that descend from this entity. Includes the current entity.
     */
    getDescendants() {
        let result = [this];
        let queue = [this];
        while (queue.length > 0) {
            const curr = queue.pop();
            if (curr) {
                queue = queue.concat(curr.children);
                result = result.concat(curr.children);
            }
        }
        return result;
    }
    /**
     * Creates a deep copy of the entity and a copy of all its components
     */
    clone() {
        const newEntity = new Entity();
        for (const c of this.types) {
            const componentInstance = this.get(c);
            if (componentInstance) {
                newEntity.addComponent(componentInstance.clone());
            }
        }
        for (const child of this.children) {
            newEntity.addChild(child.clone());
        }
        return newEntity;
    }
    /**
     * Adds a copy of all the components from another template entity as a "prefab"
     * @param templateEntity Entity to use as a template
     * @param force Force component replacement if it already exists on the target entity
     */
    addTemplate(templateEntity, force = false) {
        for (const c of templateEntity.getComponents()) {
            this.addComponent(c.clone(), force);
        }
        for (const child of templateEntity.children) {
            this.addChild(child.clone().addTemplate(child));
        }
        return this;
    }
    /**
     * Adds a component to the entity
     * @param component Component or Entity to add copy of components from
     * @param force Optionally overwrite any existing components of the same type
     */
    addComponent(component, force = false) {
        this._instanceOfComponentCacheDirty = true;
        // if component already exists, skip if not forced
        if (this.has(component.constructor)) {
            if (force) {
                // Remove existing component type if exists when forced
                this.removeComponent(component.constructor, true);
            }
            else {
                // early exit component exits
                return this;
            }
        }
        // TODO circular dependencies will be a problem
        if (component.dependencies && component.dependencies.length) {
            for (const ctor of component.dependencies) {
                this.addComponent(new ctor());
            }
        }
        component.owner = this;
        this.components.set(component.constructor, component);
        if (component.onAdd) {
            component.onAdd(this);
        }
        this.componentAdded$.notifyAll(component);
        return this;
    }
    /**
     * Removes a component from the entity, by default removals are deferred to the end of entity update to avoid consistency issues
     *
     * Components can be force removed with the `force` flag, the removal is not deferred and happens immediately
     * @param typeOrInstance
     * @param force
     */
    removeComponent(typeOrInstance, force = false) {
        let type;
        if (isComponentCtor(typeOrInstance)) {
            type = typeOrInstance;
        }
        else {
            type = typeOrInstance.constructor;
        }
        if (force) {
            const componentToRemove = this.components.get(type);
            if (componentToRemove) {
                this.componentRemoved$.notifyAll(componentToRemove);
                componentToRemove.owner = undefined;
                if (componentToRemove.onRemove) {
                    componentToRemove.onRemove(this);
                }
            }
            this.components.delete(type); // remove after the notify to preserve typing
            this._instanceOfComponentCacheDirty = true;
        }
        else {
            this._componentsToRemove.push(type);
        }
        return this;
    }
    clearComponents() {
        const components = this.types;
        for (const c of components) {
            this.removeComponent(c);
        }
    }
    /**
     * @hidden
     * @internal
     */
    processComponentRemoval() {
        for (const type of this._componentsToRemove) {
            this.removeComponent(type, true);
        }
        this._componentsToRemove.length = 0;
    }
    /**
     * Check if a component type exists
     * @param type
     */
    has(type) {
        return this.components.has(type);
    }
    /**
     * Gets whether the actor is Initialized
     */
    get isInitialized() {
        return this._isInitialized;
    }
    /**
     * Initializes this entity, meant to be called by the Scene before first update not by users of Excalibur.
     *
     * It is not recommended that internal excalibur methods be overridden, do so at your own risk.
     * @internal
     */
    _initialize(engine) {
        if (!this.isInitialized) {
            this.onInitialize(engine);
            this.events.emit('initialize', new InitializeEvent(engine, this));
            this._isInitialized = true;
        }
    }
    /**
     * It is not recommended that internal excalibur methods be overridden, do so at your own risk.
     *
     * Internal _preupdate handler for [[onPreUpdate]] lifecycle event
     * @internal
     */
    _preupdate(engine, delta) {
        this.events.emit('preupdate', new PreUpdateEvent(engine, delta, this));
        this.onPreUpdate(engine, delta);
    }
    /**
     * It is not recommended that internal excalibur methods be overridden, do so at your own risk.
     *
     * Internal _preupdate handler for [[onPostUpdate]] lifecycle event
     * @internal
     */
    _postupdate(engine, delta) {
        this.events.emit('postupdate', new PostUpdateEvent(engine, delta, this));
        this.onPostUpdate(engine, delta);
    }
    /**
     * `onInitialize` is called before the first update of the entity. This method is meant to be
     * overridden.
     *
     * Synonymous with the event handler `.on('initialize', (evt) => {...})`
     */
    onInitialize(engine) {
        // Override me
    }
    /**
     * Safe to override onPreUpdate lifecycle event handler. Synonymous with `.on('preupdate', (evt) =>{...})`
     *
     * `onPreUpdate` is called directly before an entity is updated.
     */
    onPreUpdate(engine, delta) {
        // Override me
    }
    /**
     * Safe to override onPostUpdate lifecycle event handler. Synonymous with `.on('postupdate', (evt) =>{...})`
     *
     * `onPostUpdate` is called directly after an entity is updated.
     */
    onPostUpdate(engine, delta) {
        // Override me
    }
    /**
     *
     * Entity update lifecycle, called internally
     * @internal
     * @param engine
     * @param delta
     */
    update(engine, delta) {
        this._initialize(engine);
        this._preupdate(engine, delta);
        for (const child of this.children) {
            child.update(engine, delta);
        }
        this._postupdate(engine, delta);
    }
    emit(eventName, event) {
        this.events.emit(eventName, event);
    }
    on(eventName, handler) {
        return this.events.on(eventName, handler);
    }
    once(eventName, handler) {
        return this.events.once(eventName, handler);
    }
    off(eventName, handler) {
        if (handler) {
            this.events.off(eventName, handler);
        }
        else {
            this.events.off(eventName);
        }
    }
}
Entity._ID = 0;
//# sourceMappingURL=Entity.js.map