import { MOVE_SPEED, PlayerColorState } from '../common/Constants';

/**
 * Pure-TS motion model for the player character.
 *
 * This is the foundation used by tasks 4.1, 4.2 (jumping/parabolic) and
 * later 5.x (combo attacks). It is deliberately engine-free so that the
 * entire movement state-machine is Jest-testable (requirement 2.1, 5.1-5.2).
 *
 * Coordinate convention: landscape design resolution, **+y is up**. All
 * numbers are in design pixels / seconds.
 */

export interface IAxisAlignedBox {
    /** Centre x. */
    x: number;
    /** Centre y. */
    y: number;
    /** Full width. */
    w: number;
    /** Full height. */
    h: number;
}

/** A simple horizontal platform the player may stand on. */
export interface IPlatform {
    /** Platform top edge y (world px). */
    topY: number;
    /** Platform left edge x (world px). */
    leftX: number;
    /** Platform right edge x (world px). */
    rightX: number;
}

/** Horizontal input reported by `InputModel` / `FloatingControlLayer`. */
export type HorizontalInput = -1 | 0 | 1;

export interface IPlayerMotionOptions {
    /** World gravity (px/s²). Default derived so a 250-px jump lasts ~0.45 s. */
    gravity?: number;
    /** Starting AABB of the player. */
    aabb: IAxisAlignedBox;
    /** Platforms defining the walkable terrain. Can be swapped per-level. */
    platforms: IPlatform[];
    /** Starting color state. */
    initialColorState?: PlayerColorState;
}

export const DEFAULT_GRAVITY = 2500; // px/s² — tuned so a 250px jump peaks in ~0.45s

/**
 * Encapsulates player horizontal/vertical movement + ground detection.
 * Call `setHorizontalInput()` + `requestJump()` every frame from the view
 * layer, then invoke `update(dt)` to advance the simulation.
 */
export class PlayerMotionModel {
    // -- mutable state ------------------------------------------------------
    private _vx = 0;
    private _vy = 0;
    private _grounded = false;
    private _colorState: PlayerColorState;
    private _horizontalInput: HorizontalInput = 0;
    private _aabb: IAxisAlignedBox;
    private _platforms: IPlatform[];
    private readonly gravity: number;

    constructor(options: IPlayerMotionOptions) {
        this._aabb = { ...options.aabb };
        this._platforms = options.platforms.slice();
        this._colorState = options.initialColorState ?? PlayerColorState.Red;
        this.gravity = options.gravity ?? DEFAULT_GRAVITY;
    }

    // -- accessors ----------------------------------------------------------
    public get aabb(): IAxisAlignedBox {
        return this._aabb;
    }
    public get vx(): number {
        return this._vx;
    }
    public get vy(): number {
        return this._vy;
    }
    public get isGrounded(): boolean {
        return this._grounded;
    }
    public get colorState(): PlayerColorState {
        return this._colorState;
    }

    // -- inputs -------------------------------------------------------------

    /** -1 moves left, 1 moves right, 0 stops (req 2.1). */
    public setHorizontalInput(input: HorizontalInput): void {
        this._horizontalInput = input;
    }

    /**
     * Update the player's color state (e.g. after a crystal-jade pickup).
     * Movement speed will immediately reflect the new bucket (req 5.1-5.2).
     */
    public setColorState(state: PlayerColorState): void {
        this._colorState = state;
    }

    /** Impulse-based vertical jump. Does nothing if not grounded (req 2.4). */
    public applyJumpImpulse(verticalPxPerSec: number): boolean {
        if (!this._grounded) return false;
        this._vy = verticalPxPerSec;
        this._grounded = false;
        return true;
    }

    /** Additional horizontal impulse used by parabolic jumps (req 2.5). */
    public applyHorizontalImpulse(vx: number): void {
        this._vx = vx;
    }

    /** Swap level terrain; also clears grounded so we re-settle on next update. */
    public setPlatforms(platforms: IPlatform[]): void {
        this._platforms = platforms.slice();
        this._grounded = false;
    }

    // -- simulation step ----------------------------------------------------

    /**
     * Advance the simulation by `dt` seconds. In hardcore mode (req 13.4) the
     * horizontal velocity is **only** rewritten from input when on the
     * ground; mid-air `_vx` is preserved (起跳定型).
     */
    public update(dt: number): void {
        if (this._grounded) {
            this._vx = this._horizontalInput * MOVE_SPEED[this._colorState];
            this._vy = 0;
        } else {
            // Apply gravity (requirement 13.4: no air-control).
            this._vy -= this.gravity * dt;
        }
        // Integrate position.
        this._aabb = {
            ...this._aabb,
            x: this._aabb.x + this._vx * dt,
            y: this._aabb.y + this._vy * dt,
        };
        // Resolve against platforms (basic AABB vs. top-surface only).
        this._grounded = false;
        for (const p of this._platforms) {
            if (this.isRestingOn(p)) {
                this._grounded = true;
                this._aabb = { ...this._aabb, y: p.topY + this._aabb.h / 2 };
                if (this._vy < 0) this._vy = 0;
                break;
            }
        }
    }

    // -- helpers ------------------------------------------------------------

    private isRestingOn(p: IPlatform): boolean {
        const feetY = this._aabb.y - this._aabb.h / 2;
        const withinHorizontal = this._aabb.x >= p.leftX && this._aabb.x <= p.rightX;
        const atOrJustBelowTop = feetY <= p.topY + 0.5 && feetY >= p.topY - 6 && this._vy <= 0;
        return withinHorizontal && atOrJustBelowTop;
    }
}
