import { clamp } from "../../../../core/math/clamp.js";
import { DecisionPoint } from "../mastery/DecisionPoint.js";
import { Ability } from "./Ability.js";

/**
 * Wall-jump ability — instantaneous off-wall impulse fired when the
 * player presses jump while close to a wall. Designed to chain
 * naturally from wall-run (its priority is higher, so it preempts wall-
 * run's `canInterrupt()=true`) but ALSO fires from "near-wall airborne"
 * — a window where the player is mid-jump near a wall they haven't
 * committed to running.
 *
 * Activation:
 *   - intent.jump just went true (rising edge)
 *   - airborne (`!state.grounded`)
 *   - a side wall sensor has hit within `nearWallMaxDistance`
 *
 * Behaviour:
 *   - Apply impulse: `outwardImpulse * wall.normal + upImpulse * worldUp`
 *     where `upImpulse = jumpInitialVy * upFactor`.
 *   - Clear variableJumpCut so the impulse takes effect cleanly.
 *   - Fire `onJumpStart` and `onLeaveGround{reason:"walljump"}` signals.
 *   - Releases immediately (it's instantaneous, not a sustained state).
 *
 * The impulse goes through `mastery.evaluate(DecisionPoint.WallJumpImpulse)`
 * so any registered evaluators (e.g. breath rhythm, wall-jump-angle
 * timing) can scale it.
 *
 * Priority 60 — highest among movement abilities. Wall-jump must fire
 * reliably on player press, so it preempts wall-run (50), mantle (30),
 * and slide (10).
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class WallJump extends Ability {
    constructor() {
        super();
        this.name = "WallJump";
        this.priority = 60;
    }

    canActivate(controller, runtime, sensors) {
        const cfg = controller.config.wallJump;
        if (!cfg) return false;
        if (sensors === undefined || sensors === null) return false;

        // Edge-trigger: must be a fresh jump press, not a held jump.
        if (!controller.intent.jump) return false;
        if (runtime.prevJumpHeld) return false;

        // Must be airborne — wall-jump from the ground is just a jump.
        if (controller.state.grounded) return false;

        // At least one side wall within reach. From an ACTIVE wall-run the
        // wall is right there by definition — allow the kick regardless of
        // distance (the wall-run pins the player at the sensor's edge, which
        // is beyond nearWallMaxDistance, so gating the canonical wall-run →
        // wall-jump chain on the near distance would make it misfire).
        // Otherwise (near-wall airborne, not committed) require the closer
        // distance so a stray nearby wall doesn't grant a free jump.
        const onWallRun = controller.abilities.active !== null
            && controller.abilities.active.name === "WallRun";
        const left = sensors.wallLeft;
        const right = sensors.wallRight;
        const leftClose = left.hit && (onWallRun || left.distance <= cfg.nearWallMaxDistance);
        const rightClose = right.hit && (onWallRun || right.distance <= cfg.nearWallMaxDistance);
        if (!leftClose && !rightClose) return false;

        return true;
    }

    onActivate(controller, runtime) {
        const cfg = controller.config.wallJump;
        const sensors = runtime.sensors;

        // Pick the closer wall (also: if only one side is hit, use it).
        let wallNormal;
        const lH = sensors.wallLeft.hit;
        const rH = sensors.wallRight.hit;
        if (lH && (!rH || sensors.wallLeft.distance <= sensors.wallRight.distance)) {
            wallNormal = sensors.wallLeft.normal;
        } else {
            wallNormal = sensors.wallRight.normal;
        }

        // Mastery: scale the impulse.
        const masteryMul = controller.mastery.evaluate(
            DecisionPoint.WallJumpImpulse, controller, runtime,
        );

        // Apply impulse. Outward is along the wall normal; up is along
        // world up. Clamp upFactor positive to avoid pathological configs
        // that would push the player into the ground.
        const upFactor = clamp(cfg.upFactor, 0, 2);
        runtime.velocityX += wallNormal.x * cfg.outwardImpulse * masteryMul;
        runtime.velocityZ += wallNormal.z * cfg.outwardImpulse * masteryMul;
        runtime.velocityY = runtime.jumpInitialVy * upFactor * masteryMul;

        // Mark this as a fresh jump (so the base jump FSM doesn't think
        // we're falling and the variable-cut logic doesn't try to cut
        // the upward velocity).
        runtime.midJump = true;
        runtime.apexFired = false;
        controller.state.isVariableJumpCut = false;
        controller.state.isAscending = true;
        // Consume the jump-press edge so the base jump FSM doesn't double-fire.
        runtime.prevJumpHeld = true;

        // Exertion impulse — same magnitude as a regular jump-fire.
        const cfgExt = controller.config.exertion;
        controller.state.exertion = clamp(
            controller.state.exertion + cfgExt.jumpRise * runtime.massRatios.exertionRiseScale,
            0, 1,
        );

        controller.signals.onJumpStart.send1({
            peakHeight: controller.config.jump.peakHeight,
        });
        controller.signals.onLeaveGround.send1({ reason: "walljump" });
    }

    /**
     * Wall-jump is instantaneous — it shouldn't ever be "interrupted".
     * tick() returns false on first call so the ability releases
     * immediately. Returning false here doesn't matter (no other ability
     * exceeds priority 60 in the default set).
     */
    canInterrupt() {
        return true;
    }

    tick(_controller, _runtime, _bodyTransform, _dt, _system) {
        // The impulse was applied in onActivate; the ability is purely
        // instantaneous. Release immediately so base locomotion picks up
        // the resulting airborne arc.
        return false;
    }
}
