import Signal from "../../../core/events/signal/Signal.js";
import Vector2 from "../../../core/geom/Vector2.js";
import Vector3 from "../../../core/geom/Vector3.js";
import { AbilitySet } from "./abilities/AbilitySet.js";
import { FirstPersonPlayerControllerConfig } from "./FirstPersonPlayerControllerConfig.js";
import { MasterySet } from "./mastery/MasterySet.js";
import { FirstPersonActionState, FirstPersonLocomotionMode, FirstPersonPose } from "./pose/FirstPersonPose.js";
import { FirstPersonPosture } from "./pose/FirstPersonPosture.js";

/**
 * Intent — what the player (or AI) wants the body to do. All fields are
 * latched ("hold-state") except `look`, which accumulates deltas between
 * fixed updates and is zeroed by the system on consume.
 *
 * The system performs its own edge detection on `jump` (and on `crouch`
 * when toggle-mode is configured). This is what guarantees a held jump
 * button does not produce repeated jumps.
 */
class FirstPersonIntent {
    /** [-1..1, -1..1] forward+/right+ in body-local. @type {Vector2} */
    move = new Vector2(0, 0);

    /**
     * Accumulated look delta (radians). x = yaw, y = pitch. Consumed and
     * zeroed by the system in fixedUpdate.
     * @type {Vector2}
     */
    look = new Vector2(0, 0);

    /** Hold state — true while jump button is held. @type {boolean} */
    jump = false;

    /** Hold state — meaning depends on config.crouch.mode. @type {boolean} */
    crouch = false;

    /** Hold state — true while sprint button is held. @type {boolean} */
    sprint = false;
}

/**
 * Derived state — read-only outputs. Useful for HUDs, sound systems, AI
 * sensors that need to know "is the player crouched / sprinting / etc."
 */
class FirstPersonState {
    grounded = true;
    timeSinceGrounded = 0;

    /** Set by the physics layer (or the prototype's flat-ground integrator). */
    groundNormal = new Vector3(0, 1, 0);
    surfaceTag = null;

    speed = 0;
    /** [0..1] vs sprint speed — drives bob amp and FOV kick. */
    speedNormalized = 0;
    /** Horizontal locomotion mode. See {@link FirstPersonLocomotionMode}. */
    locomotionMode = FirstPersonLocomotionMode.Idle;

    verticalSpeed = 0;
    airborneTime = 0;
    fallDistance = 0;

    jumpBufferRemaining = 0;
    inJumpAnticipation = false;
    isVariableJumpCut = false;
    /** Set to true while jumping ascent until apex; gates the cut behavior. */
    isAscending = false;

    exertion = 0;
    breathPhase = 0;
    breathRateHz = 0;
    breathAmplitudeM = 0;

    stridePhase = 0;
    stepCount = 0;
    /** Which foot fires next: 0 = right, 1 = left. */
    nextFoot = 0;

    eyeHeight = 1.80;

    /** Set true when crouch was released but a ceiling above prevents standing. */
    crouchBlocked = false;
    /** True while in crouch pose (independent of intent — see crouchBlocked). */
    crouchActive = false;

    /**
     * Body posture. Set each tick by whichever layer owns motion: the
     * base derives Stand/Crouch from intent; an active ability can
     * force Prone (slide) or Hang (ledge-grab). See
     * {@link FirstPersonPosture}. Drives eye-height target and gait
     * gating (stride, footsteps, bob).
     * @type {number}
     */
    posture = FirstPersonPosture.Stand;

    leanRollRad = 0;
}

/**
 * Event surface. All signals are sent from fixedUpdate so listeners see
 * consistent state across the same simulation tick.
 */
class FirstPersonSignals {
    /** {side:"L"|"R", speed:number, surfaceTag:string?} */
    onFootStep = new Signal();
    /** {peakHeight:number} fired at impulse, after anticipation finishes. */
    onJumpStart = new Signal();
    /** No payload. */
    onJumpApex = new Signal();
    /** {reason:"jump"|"fall"} */
    onLeaveGround = new Signal();
    /** {verticalSpeed:number, kind:"soft"|"hard"} */
    onLand = new Signal();
    onCrouchEnter = new Signal();
    onCrouchExit = new Signal();
    onSprintStart = new Signal();
    onSprintStop = new Signal();
    /** {amplitude:number, rateHz:number} fired at peak inhale (phase 0.25). */
    onBreathIn = new Signal();
    /** {amplitude:number, rateHz:number} fired at peak exhale (phase 0.75). */
    onBreathOut = new Signal();
    /** {ledgeHeight:number} — fired when LedgeGrab activates. */
    onLedgeGrab = new Signal();
    /** {reason:"mantle-up"|"drop"|"slip"|"shuffle-off"} */
    onLedgeRelease = new Signal();
}

/**
 * First-person camera + body controller. See DESIGN.md (sibling file) for
 * the goals, layered architecture, and tuning rationale.
 *
 * Lifecycle: only `config` is serialized. `intent`, `state`, `pose`,
 * `signals`, and `eyeEntity` are transient — rebuilt by the system on
 * link.
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class FirstPersonPlayerController {
    static typeName = "FirstPersonPlayerController";

    constructor() {
        this.config = new FirstPersonPlayerControllerConfig();
        this.intent = new FirstPersonIntent();
        this.state = new FirstPersonState();
        this.pose = new FirstPersonPose();
        this.signals = new FirstPersonSignals();

        /**
         * Mastery layer — registry of {@link MasteryEvaluator}s and the
         * telemetry score they feed. Designers register evaluators here;
         * the controller's system queries them at each decision point.
         * Empty by default (no mastery effects unless the game adds them).
         * @type {MasterySet}
         */
        this.mastery = new MasterySet();

        /**
         * Movement abilities (wall-run, mantle, slide, etc.). Empty by
         * default — base locomotion is unchanged unless the game adds
         * abilities here.
         * @type {AbilitySet}
         */
        this.abilities = new AbilitySet();

        /**
         * ID of the entity carrying the eye Transform + Camera. Created by
         * the system on link if -1. Held as a plain number rather than
         * EntityReference to keep serialization simple — the relationship
         * is rebuilt on link anyway.
         * @type {number}
         */
        this.eyeEntity = -1;
    }

    toJSON() {
        return { config: this.config.toJSON() };
    }

    fromJSON(json) {
        if (json && json.config) {
            this.config.fromJSON(json.config);
        }
    }

    /**
     * @param {FirstPersonPlayerController} other
     */
    copy(other) {
        this.config.copy(other.config);
        // intent/state/pose/signals/eyeEntity are transient
    }

    clone() {
        const c = new FirstPersonPlayerController();
        c.copy(this);
        return c;
    }

    equals(other) {
        // config-only equality (the rest is runtime state)
        return JSON.stringify(this.config.toJSON()) === JSON.stringify(other.config.toJSON());
    }

    hash() {
        // hashing transient state is undesirable; the config is large; return 0
        return 0;
    }
}

FirstPersonPlayerController.ActionState = FirstPersonActionState;
FirstPersonPlayerController.LocomotionMode = FirstPersonLocomotionMode;
