import { AnimationCurve } from "../../../animation/curve/AnimationCurve.js";
import { Keyframe } from "../../../animation/curve/Keyframe.js";
import { DecisionPoint } from "./DecisionPoint.js";
import { MasteryEvaluator } from "./MasteryEvaluator.js";

/**
 * Symmetric curve over breath phase ∈ [0, 1):
 *   0.00 → neutral (mid-inhale-to-exhale transition)
 *   0.25 → trough: inhaling (small penalty)
 *   0.50 → neutral
 *   0.75 → peak: exhaling (small bonus)
 *
 * Physically motivated. Power-output research (weightlifting, sports
 * biomechanics) consistently finds that peak force output happens during
 * controlled exhalation. Holding breath produces a brief assist but
 * inhaling during exertion is measurably worse. Bonus magnitude > penalty
 * magnitude per the project design rule (asymmetric in magnitude).
 *
 * The curve is parameterizable so the same evaluator can serve multiple
 * roles: large peak/trough for "action" decisions (jump, slide, wall-
 * jump), small peak/trough for "continuous" decisions (ground accel
 * during running in-sync with breath).
 */
export function makeBreathRhythmCurve(peakBonus, troughPenalty) {
    return AnimationCurve.from([
        Keyframe.from(0.00, 1.0),
        Keyframe.from(0.25, 1.0 - troughPenalty),
        Keyframe.from(0.50, 1.0),
        Keyframe.from(0.75, 1.0 + peakBonus),
        Keyframe.from(1.00, 1.0),
    ]);
}

/**
 * Mastery evaluator: scales the queried quantity by breath-phase timing.
 * Multi-decision-point — same instance fires on whatever decision points
 * are configured. Two canonical use cases:
 *
 *   1. **Action evaluators** (jump, slide-start, wall-jump, mantle-exit):
 *      Default magnitudes (+3% peak / −1% trough). Fires once per event;
 *      players naturally hold or release breath when committing to an
 *      action.
 *
 *   2. **Sustained-action evaluator** (ground accel during in-sync run):
 *      Smaller magnitudes (+1.5% / −0.5%) and gated by exertion (only
 *      meaningful when the player is actually breathing heavily; matches
 *      the locomotor-respiratory-coupling principle — coupling-bonus
 *      kicks in at high exertion).
 *
 * The "smallest bonus in the system" rule applies here: this evaluator's
 * defaults are tiny on purpose. Compounded across a long run with many
 * actions, a breath-aware player gets a noticeable lift; a casual player
 * who never notices the rhythm pays barely anything.
 *
 * Example usage:
 *
 *     // Action bonuses — fires on jump/slide-entry/wall-jump
 *     controller.mastery.add(new BreathRhythmEvaluator());
 *
 *     // Speed bonus during sustained heavy breathing
 *     controller.mastery.add(new BreathRhythmEvaluator({
 *         decisionPoints: [DecisionPoint.GroundAccel],
 *         peakBonus: 0.015,
 *         troughPenalty: 0.005,
 *         requireExertionAbove: 0.3,
 *     }));
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class BreathRhythmEvaluator extends MasteryEvaluator {
    /**
     * @param {object} [opts]
     * @param {number[]} [opts.decisionPoints]  Defaults to action points
     *   (jump, slide entry, wall-jump). Pass [GroundAccel] for the
     *   "running in sync" speed bonus.
     * @param {number} [opts.peakBonus]  Default 0.03 = +3% at exhale.
     * @param {number} [opts.troughPenalty]  Default 0.01 = −1% at inhale.
     * @param {number} [opts.requireExertionAbove]  Below this exertion
     *   level (default 0) the evaluator returns identity. Use ~0.3 for
     *   "only when breathing heavily" semantics.
     */
    constructor({
        decisionPoints,
        peakBonus = 0.03,
        troughPenalty = 0.01,
        requireExertionAbove = 0.0,
    } = {}) {
        super();
        this.name = "BreathRhythm";

        /**
         * Set of decision points this evaluator answers. Default: the
         * three classic "action" points.
         * @type {Set<number>}
         */
        this.decisionPoints = new Set(decisionPoints || [
            DecisionPoint.JumpImpulse,
            DecisionPoint.SlideEntryVelocity,
            DecisionPoint.WallJumpImpulse,
        ]);

        /** @type {number} */
        this.peakBonus = peakBonus;
        /** @type {number} */
        this.troughPenalty = troughPenalty;
        /** @type {number} */
        this.requireExertionAbove = requireExertionAbove;

        /** Magnitude exponent — scales the deviation around 1.0. */
        this.flatBoost = 1.0;

        /** @private */
        this._curve = makeBreathRhythmCurve(peakBonus, troughPenalty);
    }

    /**
     * Rebuild the curve from current peakBonus/troughPenalty. Call after
     * mutating those fields if you want the change to take effect.
     */
    rebuildCurve() {
        this._curve = makeBreathRhythmCurve(this.peakBonus, this.troughPenalty);
    }

    evaluate(decisionPoint, controller, _runtime) {
        if (!this.decisionPoints.has(decisionPoint)) return 1.0;
        if (controller.state.exertion < this.requireExertionAbove) return 1.0;

        const phase = controller.state.breathPhase;
        const raw = this._curve.evaluate(phase);
        return 1.0 + (raw - 1.0) * this.flatBoost;
    }
}
