import { array_copy_unique } from "../../core/collection/array/array_copy_unique.js";
import { array_push_if_unique } from "../../core/collection/array/array_push_if_unique.js";
import { noop } from "../../core/function/noop.js";
import ObservedValue from "../../core/model/ObservedValue.js";
import { ResourceAccessKind } from "../../core/model/ResourceAccessKind.js";

/**
 * Base class to extend ECS systems from.
 * A System defines some behavior over a tuple of components.
 * Override base methods as needed to achieve desired behavior. The system already offers {@link update} and {@link fixedUpdate} methods for time-based simulation.
 * If you have event-driven requirements - make use of {@link link} and {@link unlink} methods, as well as messaging facilities of {@link EntityComponentDataset}.
 *
 * @template C
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 *
 * @example
 *   class FallDamageSystem{
 *       dependencies = [Health, Physics]
 *
 *       listeners = []; // internal storage
 *
 *       link(health: Health, physics: Physics, entity: number){
 *           const listener = () => health.value -= 10; // receive damage on fall
 *           physics.onContact.add(listener);
 *           this.listeners[entity] = listener; // remember for later
 *       }
 *
 *       unlink(health: Health, physics: Physics, entity: number){
 *           const listener = this.listeners[entity];
 *           physics.onContact.remove(listener);
 *           delete this.listeners[entity]; // cleanup
 *       }
 *   }
 */
export class System {

    /**
     * Reference to the attached {@link EntityManager}, usually set during {@link startup}
     * Useful for gaining access to other Systems, via {@link EntityManager.getSystem} as well as the associated dataset via {@link EntityManager.dataset}
     * @protected
     * @type {EntityManager}
     */
    entityManager = null;

    /**
     * Managed internally by {@link EntityManager}, do not modify manually
     * @readonly
     * @type {ObservedValue.<SystemState>}
     */
    state = new ObservedValue(SystemState.INITIAL);

    /**
     * Component classes which have to be present before the system links an entity.
     * NOTE: do not modify this while the system is running.
     * @see link
     * @see unlink
     * @readonly
     * @type {Array}
     */
    dependencies = [];

    /**
     * Component types that are used internally by the system and how they are used
     * Main benefit of doing so is twofold:
     *      - Helps the engine figure out the best execution order for system to make sure that updates propagate as quickly as possible
     *      - Declaring this helps EntityManager to ensure that all relevant component types are properly registered for the system
     *
     * NOTE: specifying this is optional. The engine will still work.
     * NOTE: do not modify this while the system is running
     * @readonly
     * @type {ResourceAccessSpecification[]}
     */
    components_used = [];


    /**
     * @returns {Array} Component classes
     */
    get referenced_components() {
        const result = [];

        array_copy_unique(this.dependencies, 0, result, result.length, this.dependencies.length);

        const used = this.components_used;
        const use_count = used.length;
        for (let i = 0; i < use_count; i++) {
            const ref = used[i];

            array_push_if_unique(result, ref.resource);
        }

        return result;
    }

    /**
     * Does not check if the component is present, will return 0 if not present.
     * @template T
     * @param {T} Klass
     * @return {number} bitmask of {@link ResourceAccessKind}
     */
    getAccessForComponent(Klass) {
        let result = 0;

        const used = this.components_used;
        const use_count = used.length;
        for (let i = 0; i < use_count; i++) {
            const ref = used[i];

            if (ref.resource === Klass) {
                result |= ref.access;
                break;
            }
        }

        const dependencies = this.dependencies;
        const dependency_count = dependencies.length;

        for (let i = 0; i < dependency_count; i++) {
            const dependency = dependencies[i];

            if (dependency === Klass) {
                // at the very least it's going to read
                result |= ResourceAccessKind.Read;
                break;
            }
        }

        return result;
    }

    /**
     * Invoked before {@link System} is used for the first time.
     * Managed by {@link EntityManager}'s lifecycle. Do not call this manually.
     * @param {EntityManager} entityManager
     * @returns {Promise<void>}
     * @see shutdown
     */
    async startup(entityManager) {
        // override as necessary
    }

    /**
     * Invoked when {@link System} is no longer needed.
     * Good place to clean up any held resourced and terminate owned processes.
     * Managed by {@link EntityManager}'s lifecycle. Do not call this manually.
     * @param {EntityManager} entityManager
     * @returns {Promise<void>}
     * @see startup
     */
    async shutdown(entityManager) {
        // override as necessary
    }


    /**
     * Called automatically when an entity gets a complete component tuple {@link this.dependencies}.
     * Inputs are the same as dependencies, only instances instead of classes, plus there is an extra argument at the end, which is the entity ID.
     * see {@link EntityObserver} for more implementation detail
     */
    link(component, entity) {
        // override as necessary
    }

    /**
     * Called automatically when an entity breaks component tuple {@link this.dependencies}.
     * Inputs are the same as dependencies, only instances instead of classes, plus there is an extra argument at the end, which is the entity ID.
     * see {@link EntityObserver} for more implementation detail
     */
    unlink(component, entity) {
        // override as necessary
    }

    /**
     * Invoked when a dataset is attached to the system, this happens before any entities are linked.
     * Happens as a result of {@link EntityManager.attachDataset}.
     * It is generally advised *NOT* to modify the dataset here.
     * Managed by {@link EntityManager}'s lifecycle. Do not call this manually.
     *
     * This method is entirely optional, it provides advanced functionality for situations where datasets are being switched on the fly.
     *
     * see {@link handleDatasetDetached} for the reverse.
     * @param {EntityComponentDataset} dataset
     */
    handleDatasetAttached(dataset) {
        // override as necessary
    }

    /**
     * Invoked when dataset is about to be detached from the system, this happens after all entities have been unlinked.
     * Happens as a result of {@link EntityManager.detachDataset}.
     * It is generally advised *NOT* to modify the dataset here.
     * Managed by {@link EntityManager}'s lifecycle. Do not call this manually.
     *
     * This method is entirely optional, it provides advanced functionality for situations where datasets are being switched on the fly.
     *
     * see {@link handleDatasetAttached} for the reverse.
     * @param {EntityComponentDataset} dataset
     */
    handleDatasetDetached(dataset) {
        // override as necessary
    }

}

/**
 * @readonly
 * @type {boolean}
 */
System.prototype.isSystem = true;

/**
 * Fixed update function, every step happens with the same exact time increment
 * useful for systems that must have a fixed time step to be predictable and stable, such as physics
 * @type {function(time_delta_seconds:number)} time delta will always be the same value, controlled via {@link EntityManager.fixedUpdateStepSize}
 */
System.prototype.fixedUpdate = noop; // by assigning NO-OP we enable a simple check, whether running the update would be useful


/**
 * This update is generally synchronized with the render loop.
 * Note that this time step can vary depending on system conditions and hardware we are running on.
 * It is generally safe to assume that this update will happen once per frame, but it is not guaranteed.
 * Also, note that when the application window/tab is suspended, the next update step can have a very large value.
 * @type {function(time_delta_seconds:number)}
 */
System.prototype.update = noop; // by assigning NO-OP we enable a simple check, whether running the update would be useful

/**
 * @readonly
 * @enum {number}
 */
export const SystemState = {
    INITIAL: 0,
    /**
     * System is currently in the process of starting up.
     * @see System.startup
     */
    STARTING: 1,
    /**
     * System is in the main running state, after startup has successfully finished.
     */
    RUNNING: 2,
    /**
     * System is currently in the process of shutting down.
     * @see System.shutdown
     */
    STOPPING: 3,
    /**
     * System has finished shutting down.
     * @see System.shutdown
     */
    STOPPED: 4
}

