import { assert } from "../../core/assert.js";
import { BitSet } from "../../core/binary/BitSet.js";

/**
 * Signaling component query.
 * Enables definition of a component class tuple, which will then be observed; for any entity that completes the tuple - a notification will be dispatched, same for any broken tuples.
 * see {@link EntityComponentDataset.addObserver}/{@link EntityComponentDataset.removeObserver}.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class EntityObserver {
    /**
     *
     * @type {BitSet}
     */
    componentMask = new BitSet();

    /**
     * Mapping from component index to position in array of observed component types,
     * this is used to build arguments for callbacks
     * @type {number[]}
     */
    componentIndexMapping = [];

    /**
     * Currently attached dataset. `null` when not attached.
     * @type {EntityComponentDataset|null}
     */
    dataset = null;

    /**
     *
     * @param {Array} componentTypes
     * @param {function(components:Array)} completedCallback
     * @param {function(components:Array)} brokenCallback
     * @param {*} [thisArg=undefined] will assume {@link this} value inside callbacks
     * @constructor
     */
    constructor(
        componentTypes,
        completedCallback,
        brokenCallback,
        thisArg
    ) {
        assert.isArray(componentTypes, 'componentTypes must be an array, instead was something else');
        assert.isFunction(completedCallback, 'completedCallback');
        assert.isFunction(brokenCallback, 'brokenCallback');

        const numComponentTypes = componentTypes.length;

        if (numComponentTypes < 1) {
            throw new Error(`Observer has to have at least 1 component types to watch, instead was given ${numComponentTypes}`);
        }

        /**
         * @type {number}
         */
        this.componentTypeCount = numComponentTypes;

        /**
         *
         * @type {function(Array)}
         */
        this.callbackComplete = completedCallback;

        /**
         *
         * @type {function(Array)}
         */
        this.callbackBroken = brokenCallback;

        /**
         *
         * @type {Array}
         */
        this.componentTypes = componentTypes;

        /**
         *
         * @type {*}
         */
        this.thisArg = thisArg;
    }

    /**
     *
     * @param {Array} componentTypeMap
     */
    build(componentTypeMap) {
        let i;

        this.componentIndexMapping = [];
        this.componentMask.reset();


        for (i = 0; i < this.componentTypeCount; i++) {
            const componentType = this.componentTypes[i];

            const componentTypeIndex = componentTypeMap.indexOf(componentType);

            if (componentTypeIndex === -1) {
                throw new Error(`Component type[${i}] was not found in the supplied map. Observer is not compatible.`);
            }

            this.componentMask.set(componentTypeIndex, true);

            this.componentIndexMapping[componentTypeIndex] = i;
        }

    }

    /**
     * A shortcut for {@link EntityComponentDataset.addObserver}
     * @param {EntityComponentDataset} dataset
     */
    connect(dataset) {
        if (this.dataset !== null) {
            throw new Error("Already attached to a dataset, disconnect first");
        }

        dataset.addObserver(this, true);
    }

    /**
     *
     * @return {boolean}
     */
    get isConnected() {
        return this.dataset === null;
    }

    /**
     * A shortcut for {@link EntityComponentDataset.removeObserver}
     */
    disconnect() {
        if (this.dataset === null) {
            // not attached
            return;
        }
        //de-register updates
        this.dataset.removeObserver(this);
        this.dataset = null;
    }
}
