import { PlayerMotionModel, DEFAULT_GRAVITY } from './PlayerMotionModel';
import {
    JUMP_HEIGHT_STANDARD,
    JUMP_HEIGHT_CHARGED,
    JUMP_HEIGHT_YELLOW,
    JUMP_PREPARE_DELAY_MS,
    JUMP_CHARGE_THRESHOLD_MS,
    PlayerColorState,
} from '../common/Constants';
import { JoystickAngleClass } from '../ui/InputModel';

/**
 * Jump controller — orchestrates the jump lifecycle on top of
 * `PlayerMotionModel` (task 4.2).
 *
 * Lifecycle (ms timestamps supplied by caller so Jest can stay deterministic):
 *
 *   pressJump(ts)
 *     ├─ not grounded? ignore (req 2.4)
 *     ├─ enter `charging` state, start timer
 *     └─ emit `jump_prepare_start`
 *
 *   releaseJump(ts, direction)
 *     ├─ ts - pressTs >= JUMP_CHARGE_THRESHOLD_MS → charged high-jump (req 2.3)
 *     ├─ else                                     → standard jump (req 2.2)
 *     ├─ +150ms crouch delay before launch         (req 2.8)
 *     └─ parabolic_right / parabolic_left         → horizontal impulse too (req 2.5)
 *
 * Yellow-state uses a taller vertical impulse (req 2.2).
 */

export type JumpPhase = 'idle' | 'charging' | 'crouching' | 'launched';

export interface IJumpDispatchResult {
    phase: JumpPhase;
    height: number;
    horizontalImpulse: number;
    reason?: string;
}

/** How much horizontal velocity a parabolic jump imparts (px/s). */
export const PARABOLIC_HORIZONTAL_SPEED = 180;

/**
 * Converts `verticalTravel (px)` to the initial velocity needed to reach it.
 * Using `v = sqrt(2 * g * h)` under constant gravity.
 */
export function heightToImpulse(heightPx: number, gravity: number = DEFAULT_GRAVITY): number {
    return Math.sqrt(2 * gravity * heightPx);
}

export class JumpController {
    private phase: JumpPhase = 'idle';
    private pressTs = 0;
    private crouchEndsAt = 0;
    private pendingImpulse: { vy: number; vx: number } | null = null;

    constructor(
        private readonly motion: PlayerMotionModel,
        private readonly prepareDelayMs: number = JUMP_PREPARE_DELAY_MS,
        private readonly chargeThresholdMs: number = JUMP_CHARGE_THRESHOLD_MS
    ) {}

    /** Called each frame with `now` from `TimeMgr.realTime * 1000`. */
    public tick(nowMs: number): void {
        if (this.phase === 'crouching' && nowMs >= this.crouchEndsAt) {
            if (this.pendingImpulse) {
                this.motion.applyJumpImpulse(this.pendingImpulse.vy);
                this.motion.applyHorizontalImpulse(this.pendingImpulse.vx);
                this.pendingImpulse = null;
            }
            this.phase = 'launched';
        }
        // Once the motion model reports grounded again, reset to idle.
        if (this.phase === 'launched' && this.motion.isGrounded) {
            this.phase = 'idle';
        }
    }

    /** Called on `jumpPressed` UI event. */
    public pressJump(nowMs: number): IJumpDispatchResult {
        if (!this.motion.isGrounded) {
            return { phase: this.phase, height: 0, horizontalImpulse: 0, reason: 'airborne' };
        }
        if (this.phase !== 'idle') {
            return { phase: this.phase, height: 0, horizontalImpulse: 0, reason: 'already_in_jump' };
        }
        this.phase = 'charging';
        this.pressTs = nowMs;
        return { phase: this.phase, height: 0, horizontalImpulse: 0 };
    }

    /** Called on `jumpReleased` UI event. */
    public releaseJump(
        nowMs: number,
        joystickClass: JoystickAngleClass,
        colorState: PlayerColorState = PlayerColorState.Red
    ): IJumpDispatchResult {
        if (this.phase !== 'charging') {
            return { phase: this.phase, height: 0, horizontalImpulse: 0, reason: 'not_charging' };
        }
        const heldMs = nowMs - this.pressTs;
        const charged = heldMs >= this.chargeThresholdMs;

        let height = charged
            ? JUMP_HEIGHT_CHARGED
            : colorState === PlayerColorState.Yellow
              ? JUMP_HEIGHT_YELLOW
              : JUMP_HEIGHT_STANDARD;

        let vx = 0;
        if (joystickClass === 'parabolic_right') {
            vx = PARABOLIC_HORIZONTAL_SPEED;
        } else if (joystickClass === 'parabolic_left') {
            vx = -PARABOLIC_HORIZONTAL_SPEED;
        }

        this.phase = 'crouching';
        this.crouchEndsAt = nowMs + this.prepareDelayMs;
        const vy = heightToImpulse(height);
        this.pendingImpulse = { vy, vx };
        return { phase: this.phase, height, horizontalImpulse: vx };
    }

    /** Cancel any pending jump (used on pause / scene unload). */
    public cancel(): void {
        this.phase = 'idle';
        this.pendingImpulse = null;
    }

    /** Expose the current jump phase for HUD feedback (disabled button, etc.). */
    public getPhase(): JumpPhase {
        return this.phase;
    }

    /**
     * Whether the UI should render the jump button as enabled. Disabled when
     * airborne or mid-cycle (req 2.4).
     */
    public isButtonEnabled(): boolean {
        return this.motion.isGrounded && this.phase === 'idle';
    }
}
