import { DEG_TO_RAD } from "../../../../core/math/DEG_TO_RAD.js";
import { Ability } from "./Ability.js";

/**
 * Wall-run ability. Activates when the player is airborne with momentum
 * and a wall is detected to their left or right. While active:
 *
 *   - Gravity is reduced to `cfg.wallRun.gravityFactor × base` (typically
 *     25%), letting the player hang on the wall for longer than a normal
 *     jump arc.
 *   - Horizontal velocity is projected onto the wall's tangent each tick,
 *     so the body sticks to the wall and slides along it. Forward
 *     momentum is preserved; into-wall and out-of-wall velocity
 *     components are zeroed.
 *   - Camera tilts into the wall via the shared lean spring (writes the
 *     wall-tilt to `runtime.leanTargetRad` each tick — same channel
 *     base writes its lat-accel-derived lean to, so transitions in and
 *     out share the spring's smoothing).
 *
 * Exits on:
 *   - Timer (`cfg.wallRun.maxDuration`)
 *   - Lost wall contact (sensor's hit flag becomes false)
 *   - Re-grounded (we walked onto a floor at the wall's end)
 *   - Wall-jump preempts (higher priority — WallJump declares priority 60)
 *
 * Priority 50 — above Mantle (30), Slide (10), below Wall-jump (60).
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class WallRun extends Ability {
    constructor() {
        super();
        this.name = "WallRun";
        this.priority = 50;

        /** @private Seconds since this ability activated. */
        this._elapsed = 0;
        /** @private "L" or "R" — which wall we're running on. */
        this._side = "L";
    }

    canActivate(controller, runtime, sensors) {
        const cfg = controller.config.wallRun;
        if (!cfg) return false;
        if (sensors === undefined || sensors === null) return false;
        if (controller.state.grounded) return false;
        if (controller.state.airborneTime < cfg.minAirborneTime) return false;

        const speed = Math.hypot(runtime.velocityX, runtime.velocityZ);
        if (speed < cfg.minSpeed) return false;

        // Need a wall on left OR right (not front — that's a wall to bump into).
        if (!sensors.wallLeft.hit && !sensors.wallRight.hit) return false;

        // Don't (re)grab the wall when a grabbable ledge is right ahead: you've
        // reached its top, so ledge-grab/mantle should take over (hang or
        // vault) instead of the run re-attaching and riding you back DOWN the
        // wall you just climbed. Only bites when facing INTO the wall (the
        // forward probe has to hit for a ledge ahead), so a normal run ALONG
        // the wall — facing down its length — is unaffected.
        if (sensors.ledgeAhead.hit) return false;

        // Forward intent required — wall-run is a committed action.
        if (controller.intent.move.y < 0.1) return false;

        // Don't (re)grab a wall you're moving AWAY from. The wall normal
        // points off the wall toward the player, so velocity·normal > 0 means
        // leaving it — most importantly right after a wall-JUMP pushed you off
        // (else the run would re-attach next tick and the tangent projection
        // would cancel the kick). Approaching (dot < 0) or running along it
        // (dot ≈ 0) is fine.
        const hit = sensors.wallLeft.hit
            && (!sensors.wallRight.hit || sensors.wallLeft.distance <= sensors.wallRight.distance)
            ? sensors.wallLeft : sensors.wallRight;
        const awaySpeed = runtime.velocityX * hit.normal.x + runtime.velocityZ * hit.normal.z;
        if (awaySpeed > cfg.minSpeed * 0.5) return false;

        return true;
    }

    onActivate(controller, runtime) {
        const sensors = runtime.sensors;
        this._elapsed = 0;

        // Pick the closer wall if both detected.
        if (sensors.wallLeft.hit
            && (!sensors.wallRight.hit
                || sensors.wallLeft.distance <= sensors.wallRight.distance)) {
            this._side = "L";
        } else {
            this._side = "R";
        }

        // Clear any in-progress jump state — the wall-run takes over the
        // vertical model.
        runtime.midJump = false;
        runtime.apexFired = false;
        controller.state.isVariableJumpCut = false;
        controller.state.isAscending = false;

        controller.signals.onLeaveGround.send1({ reason: "wallrun" });
    }

    /**
     * Wall-jump (priority 60) should be allowed to preempt freely; that's
     * the canonical chain (wall-run → wall-jump). Any other higher-priority
     * ability is also fine — wall-run is a transient state.
     */
    canInterrupt() {
        return true;
    }

    tick(controller, runtime, bodyTransform, dt, system) {
        const cfg = controller.config.wallRun;
        this._elapsed += dt;

        // -- Exit: timer.
        if (this._elapsed >= cfg.maxDuration) {
            return false;
        }
        // -- Exit: re-grounded (we ran onto a floor).
        if (controller.state.grounded) {
            return false;
        }

        const sensors = runtime.sensors;
        const sensorHit = this._side === "L" ? sensors.wallLeft : sensors.wallRight;

        // -- Exit: lost the wall.
        if (!sensorHit.hit) {
            return false;
        }

        // -- Project velocity onto the wall's tangent.
        // The wall's normal is in the XZ plane (vertical wall). Zero the
        // component of velocity along that normal. Forward momentum
        // (along the wall's tangent direction) is preserved.
        const nx = sensorHit.normal.x;
        const nz = sensorHit.normal.z;
        const dotN = runtime.velocityX * nx + runtime.velocityZ * nz;
        runtime.velocityX -= dotN * nx;
        runtime.velocityZ -= dotN * nz;

        // -- Camera roll: write directly to the shared lean target.
        // Sign convention: positive engine roll = head tilts RIGHT. Wall
        // on LEFT → tilt LEFT → negative roll. Wall on RIGHT → tilt
        // RIGHT → positive roll. L2.f spring-steps from this value each
        // tick; base will overwrite once the ability releases, and the
        // spring smooths the transition.
        const rollSign = this._side === "L" ? -1 : 1;
        runtime.leanTargetRad = rollSign * cfg.cameraRollDeg * DEG_TO_RAD;

        // -- Reduced gravity. This is wall-run's OWN vertical model (a
        // fraction of base gravity, no fall multiplier), applied here rather
        // than via the system's standard gravity step.
        runtime.velocityY -= runtime.gravity * cfg.gravityFactor * dt;

        // -- Resolve the move through the shared motor. With the reduced-
        // gravity velocity set above, route it through the same sweep-and-
        // slide + ground-categorize the base locomotion uses (the mover when
        // physics is present, else the flat-ground fallback). _resolveMotion
        // does NOT re-apply gravity. This replaces a raw `position._add` plus
        // a hand-rolled groundResolver catch: the wall-runner now (a) can't
        // drift INTO geometry (the sweep clips it) nor sail OFF a ledge the
        // mover would have caught, and (b) gets the motor's anti-tunnel
        // ground catch for free — the same reason base no longer free-falls
        // past a floor. The motor sets `state.grounded` and fires land/leave.
        system._resolveMotion(controller, runtime, bodyTransform, dt);

        // -- Exit: the motor caught a floor at the wall's foot (we ran out
        // onto the ground). Release; base resumes next tick.
        if (controller.state.grounded) {
            return false;
        }

        return true;
    }

    // No onDeactivate cleanup needed: on the tick we release, base runs
    // and overwrites runtime.leanTargetRad with the natural value. The
    // lean spring smooths the transition.
}
