import { EventType } from "../EventType.js";
import { AbstractContextSystem } from "../system/AbstractContextSystem.js";
import { SystemEntityContext } from "../system/SystemEntityContext.js";
import { ParentEntity } from "./ParentEntity.js";

/**
 *
 * @type {number[]|Uint32Array}
 */
const loop_checker_array = new Uint32Array(1024);

/**
 *
 * @param {number} value
 * @param {number} index_limit
 * @returns {boolean}
 */
function check_loop(value, index_limit) {
    for (let i = 0; i < index_limit; i++) {
        if (loop_checker_array[i] === value) {
            return true;
        }
    }

    return false;
}

/**
 *
 * @param {number} limit
 * @returns {string}
 */
function genLoopString(limit) {
    let r = "";

    for (let i = 0; i < limit; i++) {
        if (i > 0) {
            r += " -> ";
        }

        r += loop_checker_array[i];
    }

    return r;
}

class Context extends SystemEntityContext {
    link() {
        super.link();

        const ecd = this.getDataset();

        /**
         *
         * @type {ParentEntity}
         */
        const cParent = this.components[0];

        ecd.addEntityEventListener(cParent.entity, EventType.EntityRemoved, this.handleParentEntityRemoval, this);
    }

    handleParentEntityRemoval() {
        // delete this entity as well
        this.getDataset().removeEntity(this.entity);
    }

    unlink() {
        super.unlink();

        const ecd = this.getDataset();

        /**
         *
         * @type {ParentEntity}
         */
        const cParent = this.components[0];

        ecd.removeEntityEventListener(cParent.entity, EventType.EntityRemoved, this.handleParentEntityRemoval, this);
    }
}

export class ParentEntitySystem extends AbstractContextSystem {
    constructor() {
        super(Context);

        this.dependencies = [ParentEntity];
    }

    /**
     * Find root entity given some entity with a {@link ParentEntity} component attached
     * @param {EntityComponentDataset} ecd
     * @param {number} entity
     * @returns {number}
     */
    static findRoot(ecd, entity) {
        let count = 0;

        let p = entity;
        while (true) {
            loop_checker_array[count++] = p;

            const parent = ParentEntitySystem.findParentEntity(ecd, p);

            if (parent === -1) {
                break;
            }

            if (check_loop(parent, count)) {
                throw new Error(`Cyclic reference encountered: ${genLoopString(count)}`);
            }

            p = parent;
        }

        return p;
    }

    /**
     * @param {EntityComponentDataset} ecd
     * @param {number} child_entity
     * @param {number} ancestor_entity
     * @returns {boolean}
     */
    static isAncestorOf(ecd, child_entity, ancestor_entity) {
        let loop_count = 0;
        let p = child_entity;

        while (true) {
            if (p === ancestor_entity) {
                return true;
            }

            loop_checker_array[loop_count++] = p;

            p = ParentEntitySystem.findParentEntity(ecd, p);

            if (p === -1) {
                break;
            }

            if (check_loop(p, loop_count)) {
                throw new Error(`Cyclic reference encountered: ${genLoopString(loop_count)}`);
            }
        }

        // reached the root, ancestor not encountered
        return false;
    }

    /**
     * @param {EntityComponentDataset} ecd
     * @param {number} entity
     * @returns {number}
     */
    static findParentEntity(ecd, entity) {

        const parent = ecd.getComponent(entity, ParentEntity);

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

        return parent.entity;
    }

    /**
     *
     * @param {number[]} result
     * @param {number} result_offset
     * @param {EntityComponentDataset} ecd
     * @param {number} entity
     * @returns {number}
     */
    static findChildrenOf(result, result_offset, ecd, entity) {
        let result_cursor = result_offset;
        ecd.traverseComponents(ParentEntity, (p, e) => {
            if (p.entity === entity) {
                result[result_cursor++] = e;
            }
        });

        return result_cursor - result_offset;
    }

    /**
     *
     * @param {EntityComponentDataset} ecd
     * @param {number} root_entity
     * @param {function(number, EntityComponentDataset)} callback
     * @param {*} [thisArg]
     */
    static traverse(ecd, root_entity, callback, thisArg) {
        traverse(ecd, root_entity, callback, thisArg);
    }
}

/**
 *
 * @param {EntityComponentDataset} ecd
 * @param {number} root_entity
 * @param {function(number, EntityComponentDataset)} callback
 * @param {*} [thisArg]
 * @param {number[]} path keep track of where we came from, this is used to detect cycles
 */
function traverse(ecd, root_entity, callback, thisArg, path = []) {

    callback.call(thisArg, root_entity, ecd);

    const children = [];
    const child_count = ParentEntitySystem.findChildrenOf(children, 0, ecd, root_entity);

    for (let i = 0; i < child_count; i++) {
        const entity = children[i];

        if (path.includes(entity)) {
            console.warn(`Loop in entity hierarchy detected: ${path.join('->')}`);
            continue;
        }

        traverse(ecd, entity, callback, thisArg, path.concat(entity));
    }
}
