import { MasteryScore } from "./MasteryScore.js";

/**
 * Aggregates {@link MasteryEvaluator} instances and exposes a single
 * `evaluate(decisionPoint, ...)` entry point that composes all evaluators
 * for that point multiplicatively.
 *
 * Identity behaviour: with zero evaluators registered, every query returns
 * `1.0`, leaving the base controller behaviour untouched. This is the
 * default state — mastery is opt-in.
 *
 * Composition: contributions multiply, NOT sum. So a +10% and a +5% give
 * +15.5% (1.10 × 1.05 = 1.155). And a +10% with a −5% gives +4.5% (1.10
 * × 0.95 = 1.045). Mistiming partially offsets bonuses from other
 * evaluators — exactly what we want for honest "your timing matters
 * everywhere" feel.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class MasterySet {
    constructor() {
        /**
         * Registered evaluators, in insertion order.
         * @type {MasteryEvaluator[]}
         */
        this.evaluators = [];

        /**
         * Telemetry EMA. The controller doesn't consume it; downstream
         * systems read it for player-facing mastery feedback.
         * @type {MasteryScore}
         */
        this.score = new MasteryScore();
    }

    /**
     * Add an evaluator. Order does not affect the final result
     * (multiplication is commutative).
     * @param {MasteryEvaluator} evaluator
     */
    add(evaluator) {
        this.evaluators.push(evaluator);
    }

    /**
     * Remove an evaluator if present.
     * @param {MasteryEvaluator} evaluator
     */
    remove(evaluator) {
        const i = this.evaluators.indexOf(evaluator);
        if (i !== -1) this.evaluators.splice(i, 1);
    }

    /**
     * Compose all registered evaluators against `decisionPoint`. Each
     * evaluator's contribution is recorded into the score for telemetry,
     * then multiplied into the running product.
     *
     * @param {number} decisionPoint
     * @param {*} controller
     * @param {*} runtime
     * @returns {number} composed multiplier; 1.0 if no evaluators care
     */
    evaluate(decisionPoint, controller, runtime) {
        let product = 1.0;
        const n = this.evaluators.length;
        for (let i = 0; i < n; i++) {
            const v = this.evaluators[i].evaluate(decisionPoint, controller, runtime);
            // Defensive: ignore evaluators returning non-finite or non-positive
            // values (would corrupt downstream math). Treat as identity.
            if (!Number.isFinite(v) || v <= 0) continue;
            product *= v;
            this.score.record(decisionPoint, v);
        }
        return product;
    }

    /**
     * Advance the score's time-based decay. Call once per fixed step.
     * @param {number} dt
     */
    tick(dt) {
        this.score.decay(dt);
    }
}
