import { FirstPersonPosture } from "../pose/FirstPersonPosture.js";
import { Ability } from "./Ability.js";

/**
 * Smoothstep-style horizontal interpolation: slow-fast-slow.
 * @param {number} t  [0..1]
 */
function easeInOutCubic(t) {
    return t < 0.5
        ? 4 * t * t * t
        : 1 - Math.pow(-2 * t + 2, 3) / 2;
}

/**
 * Front-loaded ease — fast rise then gentle settle. Used for the vertical
 * curve so the body lifts off the ground quickly (matching the hand-grip
 * pull-up dynamic) and decelerates as it approaches the surface top.
 * @param {number} t  [0..1]
 */
function easeOutCubic(t) {
    return 1 - Math.pow(1 - t, 3);
}

/**
 * Mantle ability — auto-triggers when the player walks/runs into a
 * reachable obstacle with forward intent. The body follows a scripted
 * parametric path (horizontal lerp on an ease-in-out, vertical lerp on
 * an ease-out) for {@link cfg.mantle.duration} seconds, landing on top
 * of the surface.
 *
 * Activation requires both `sensors.obstacleAhead.hit` (something in
 * front to climb) AND `sensors.ledgeAhead.hit` (there's a top surface
 * to land on). Heights between `cfg.mantle.minHeight` and `maxHeight`
 * are accepted; below is "just step over", above is "out of reach".
 *
 * The ability is FULLY non-interruptible once committed — `canInterrupt`
 * returns false throughout. This prevents the body from teleporting
 * mid-mantle if a higher-priority ability fires; the trade-off is the
 * player has limited control during the short window. Player input
 * for THIS frame is effectively suspended.
 *
 * Priority 30: above Slide (10) so an obstacle vault-into preempts a
 * slide; below Wall-run / Wall-jump so those high-air abilities still
 * win if their conditions are simultaneously met (which would be
 * unusual — typically you mantle from the ground).
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class Mantle extends Ability {
    constructor() {
        super();
        this.name = "Mantle";
        this.priority = 30;

        // -- per-instance scripted-path state --
        /** @private Elapsed fraction of duration. */
        this._t = 0;
        /** @private Captured at activation. */
        this._duration = 0.5;
        this._startX = 0;
        this._startY = 0;
        this._startZ = 0;
        this._endX = 0;
        this._endY = 0;
        this._endZ = 0;
    }

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

        // Must have BOTH the face probe (obstacleAhead) AND the top probe
        // (ledgeAhead) — otherwise we don't know if there's anywhere to
        // land. obstacleAhead alone could be a wall too tall to climb.
        if (!sensors.obstacleAhead.hit) return false;
        if (!sensors.ledgeAhead.hit) return false;

        // Only vault onto a top we can actually STAND on. A too-thin wall /
        // knife-edge isn't a mantle target — the body would land overhanging
        // and fall right back off (on a wall-run wall this is the reported
        // violent snap-then-drop). Ledge-grab, which can hang from a thin
        // edge, takes those instead.
        if (!sensors.ledgeStandable) return false;

        // Forward intent is required — the player must be committing
        // into the obstacle, otherwise we don't want to grab them.
        if (controller.intent.move.y < 0.1) return false;

        // Height check: the target surface must be within the reachable
        // band.
        const surfaceTopY = sensors.ledgeAhead.point.y;
        const playerFootY = controller.pose.rootPosition.y;
        const heightDiff = surfaceTopY - playerFootY;
        if (heightDiff < cfg.minHeight) return false;
        if (heightDiff > cfg.maxHeight) return false;

        return true;
    }

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

        this._t = 0;
        this._duration = cfg.duration;

        // Capture start position (body's current world position).
        const startPos = controller.pose.rootPosition;
        this._startX = startPos.x;
        this._startY = startPos.y;
        this._startZ = startPos.z;

        // Compute end position: above the ledge surface, displaced
        // forward by `forwardOffsetOnLand` so the player ends standing
        // on the surface (not balancing on the edge).
        const yaw = runtime.bodyYaw;
        const fx = Math.sin(yaw);
        const fz = Math.cos(yaw);
        this._endX = sensors.ledgeAhead.point.x + fx * cfg.forwardOffsetOnLand;
        this._endY = sensors.ledgeAhead.point.y;
        this._endZ = sensors.ledgeAhead.point.z + fz * cfg.forwardOffsetOnLand;

        // Zero velocity — the body is on a path, not under integration.
        runtime.velocityX = 0;
        runtime.velocityY = 0;
        runtime.velocityZ = 0;

        // Clear airborne/jump state — the scripted path supersedes them.
        runtime.midJump = false;
        runtime.apexFired = false;
        controller.state.isVariableJumpCut = false;
        controller.state.isAscending = false;

        // Body is climbing — upright posture (not Hang, even if we got
        // here from ledge-grab). Animators read posture for the rig
        // blend; this signals "switch from the hang clip to the climb /
        // upright clip".
        controller.state.posture = FirstPersonPosture.Stand;

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

    canInterrupt() {
        // Non-interruptible. Mantle commits.
        return false;
    }

    tick(controller, runtime, bodyTransform, dt, _system) {
        // Body is on a scripted path — no lat-accel-driven lean. Hold
        // the camera level for the duration of the mantle.
        runtime.leanTargetRad = 0;
        // Re-assert posture in case anything reset it. Mantle is a
        // committed climb; we want the rig in the climb/stand blend
        // until release.
        controller.state.posture = FirstPersonPosture.Stand;

        this._t += dt / this._duration;

        if (this._t >= 1.0) {
            // Completed: snap to end, mark grounded, release.
            bodyTransform.position.set(this._endX, this._endY, this._endZ);
            runtime.velocityX = 0;
            runtime.velocityY = 0;
            runtime.velocityZ = 0;
            controller.state.grounded = true;
            controller.state.verticalSpeed = 0;
            controller.state.airborneTime = 0;
            controller.state.timeSinceGrounded = 0;
            controller.signals.onLand.send1({ verticalSpeed: 0, kind: "soft" });
            return false;
        }

        // Sample the parametric path.
        const horizT = easeInOutCubic(this._t);
        const vertT = easeOutCubic(this._t);
        bodyTransform.position.set(
            this._startX + (this._endX - this._startX) * horizT,
            this._startY + (this._endY - this._startY) * vertT,
            this._startZ + (this._endZ - this._startZ) * horizT,
        );

        // Body is on a path — no integration this tick. Velocity stays 0.
        runtime.velocityX = 0;
        runtime.velocityY = 0;
        runtime.velocityZ = 0;

        return true;
    }

    onDeactivate(_controller, _runtime) {
        this._t = 0;
    }
}
