/**
 * Holds a set of {@link Ability} instances and runs the activation +
 * preemption + queueing policy:
 *
 *   1. If an ability is active, tick it first.
 *      - If its tick returns false → it's done. Falls through to step 2
 *        in the SAME tick (no wasted frame).
 *
 *   2. If a queued ability exists (from a previous tick that was blocked),
 *      check its canActivate() again. If still wants in → activate.
 *
 *   3. If no ability is active, poll all abilities in DESCENDING priority.
 *      First that canActivate() becomes active.
 *
 *   4. If an active ability is still running but a higher-priority one
 *      wants in: check the active one's canInterrupt(). If yes → preempt
 *      now. If no → queue the higher-priority candidate; it will re-check
 *      every tick until the current ability releases (or canInterrupt
 *      flips to true).
 *
 * Returns whether base-locomotion should run this tick (true iff no
 * ability owns motion).
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class AbilitySet {
    constructor() {
        /**
         * Registered abilities. Order does not matter — sorted by priority
         * on demand when polling. Set is small (typically <10) so we don't
         * pre-sort.
         * @type {Ability[]}
         */
        this.abilities = [];

        /**
         * Currently-active ability, or null if base locomotion is running.
         * @type {Ability|null}
         */
        this.active = null;

        /**
         * Ability waiting for the current one to release. Cleared when it
         * activates OR when its canActivate() returns false on re-check
         * (the conditions that wanted it might have passed).
         * @type {Ability|null}
         */
        this.queued = null;
    }

    /**
     * Add an ability.
     * @param {Ability} ability
     */
    add(ability) {
        this.abilities.push(ability);
    }

    /**
     * Remove an ability if present. If it's currently active or queued,
     * deactivates / clears it first.
     * @param {Ability} ability
     */
    remove(ability) {
        if (this.active === ability) {
            // Best-effort: caller-side cleanup is their responsibility.
            this.active = null;
        }
        if (this.queued === ability) this.queued = null;
        const i = this.abilities.indexOf(ability);
        if (i !== -1) this.abilities.splice(i, 1);
    }

    /**
     * Execute one tick. Returns true if base locomotion should run this
     * fixed step (no ability owns motion).
     *
     * @param {*} controller
     * @param {*} runtime
     * @param {*} bodyTransform
     * @param {*} sensors
     * @param {number} dt
     * @param {*} [system] FirstPersonPlayerControllerSystem instance,
     *                     passed through to ability.tick() so abilities
     *                     can delegate to shared helpers.
     * @returns {boolean} true = run base locomotion; false = ability owned
     */
    tick(controller, runtime, bodyTransform, sensors, dt, system) {
        // Step 1: tick the active ability if any. May release control.
        if (this.active !== null) {
            const stillActive = this.active.tick(controller, runtime, bodyTransform, dt, system);
            if (!stillActive) {
                this._deactivate(controller, runtime);
            }
        }

        // Step 2: if something is queued, see if it can activate now (the
        // current ability may have released in step 1, OR may be
        // interruptible now even though it wasn't before).
        if (this.queued !== null) {
            if (this.active === null) {
                if (this.queued.canActivate(controller, runtime, sensors)) {
                    this._activate(this.queued, controller, runtime);
                }
                this.queued = null; // cleared whether or not it activated
            } else if (this.queued.priority > this.active.priority
                && this.active.canInterrupt(controller, runtime)) {
                if (this.queued.canActivate(controller, runtime, sensors)) {
                    const next = this.queued;
                    this.queued = null;
                    this._deactivate(controller, runtime);
                    this._activate(next, controller, runtime);
                } else {
                    // Queued candidate no longer wants in — drop it.
                    this.queued = null;
                }
            }
            // else: still waiting on a non-interruptible current ability
        }

        // Step 3: if nothing active, poll candidates (highest priority first).
        if (this.active === null) {
            const best = this._findHighestPriorityActivatable(controller, runtime, sensors, -Infinity);
            if (best !== null) {
                this._activate(best, controller, runtime);
            }
        } else {
            // Step 4: existing ability active — check if a higher-priority
            // candidate wants in.
            const challenger = this._findHighestPriorityActivatable(
                controller, runtime, sensors, this.active.priority,
            );
            if (challenger !== null) {
                if (this.active.canInterrupt(controller, runtime)) {
                    this._deactivate(controller, runtime);
                    this._activate(challenger, controller, runtime);
                } else {
                    // Queue it — will re-poll each tick until current releases
                    // or becomes interruptible.
                    this.queued = challenger;
                }
            }
        }

        return this.active === null;
    }

    /**
     * @private
     */
    _findHighestPriorityActivatable(controller, runtime, sensors, minPriorityExclusive) {
        let best = null;
        let bestPriority = minPriorityExclusive;
        const list = this.abilities;
        for (let i = 0; i < list.length; i++) {
            const a = list[i];
            if (a === this.active) continue;
            if (a.priority <= bestPriority) continue;
            if (a.canActivate(controller, runtime, sensors)) {
                best = a;
                bestPriority = a.priority;
            }
        }
        return best;
    }

    /**
     * @private
     */
    _activate(ability, controller, runtime) {
        this.active = ability;
        ability.onActivate(controller, runtime);
    }

    /**
     * @private
     */
    _deactivate(controller, runtime) {
        if (this.active === null) return;
        const prev = this.active;
        this.active = null;
        prev.onDeactivate(controller, runtime);
    }
}
