import {
    SHURIKEN_INTERVAL_BASE,
    SHURIKEN_INTERVAL_UPGRADED,
    SWORD_INTERVAL,
    SHURIKEN_BURST_MAX,
    COMBO_INPUT_WINDOW_MS,
    PlayerColorState,
} from '../common/Constants';
import { WeaponType, AttackType } from '../data/Interfaces';

/**
 * Attack controller — models the two mutually-exclusive weapon buttons
 * (手里剑 / 忍者刀) plus the combo recognition window (req 3.1-3.9, 4.1-4.5).
 *
 * Outputs a single `IAttackDispatchEvent` per "fire" that gameplay code
 * applies through `DamageSystem` (task 6.2). Nothing here talks to `cc`;
 * it is deterministic on `now` timestamps provided by the caller.
 */

export type ActiveWeapon = 'none' | WeaponType;

export interface IAttackDispatchEvent {
    weapon: WeaponType;
    attackType: AttackType;
    /** true when this attack was chained with a jump within the combo window (req 4.1). */
    comboWithJump: boolean;
    /** 1-based index within a shuriken burst. Always 1 for the sword. */
    burstIndex: number;
    /** realtime timestamp of the swing / throw (ms). */
    ts: number;
}

/** Interface used by AttackController to know whether combo window applies. */
export interface IJumpStateProvider {
    /** Timestamp of the latest jump press (ms). `undefined` means no pending jump. */
    lastJumpPressTs(): number | undefined;
    /** Current grounded flag. */
    isGrounded(): boolean;
}

/**
 * Null jump-state provider — used when attack is fired on the ground and no
 * combo is expected (e.g. unit tests).
 */
export const NullJumpState: IJumpStateProvider = {
    lastJumpPressTs: () => undefined,
    isGrounded: () => true,
};

export class AttackController {
    private active: ActiveWeapon = 'none';
    /** Earliest press timestamp among currently-held attack buttons. */
    private pressedAt = new Map<WeaponType, number>();
    /** Next-eligible-fire timestamp per weapon. */
    private readyAt = new Map<WeaponType, number>();
    /** Current shuriken burst index (1..SHURIKEN_BURST_MAX). */
    private shurikenBurstIndex = 0;

    constructor(
        private readonly jumpState: IJumpStateProvider = NullJumpState,
        private readonly comboWindowMs: number = COMBO_INPUT_WINDOW_MS
    ) {}

    /** Returns the currently active weapon (or `'none'`). */
    public getActive(): ActiveWeapon {
        return this.active;
    }

    public isPressed(weapon: WeaponType): boolean {
        return this.pressedAt.has(weapon);
    }

    /**
     * Press handler. Implements mutual exclusion (req 3.1-3.3): the first
     * button pressed wins until it is released.
     */
    public press(weapon: WeaponType, nowMs: number): void {
        if (!this.pressedAt.has(weapon)) {
            this.pressedAt.set(weapon, nowMs);
        }
        if (this.active === 'none') {
            this.active = weapon;
            if (weapon === WeaponType.Shuriken) {
                this.shurikenBurstIndex = 0;
            }
        }
    }

    public release(weapon: WeaponType): void {
        this.pressedAt.delete(weapon);
        if (this.active === weapon) {
            // If the other button is still pressed, transfer activation.
            const remaining = Array.from(this.pressedAt.keys())[0];
            this.active = remaining ?? 'none';
            if (weapon === WeaponType.Shuriken) {
                this.shurikenBurstIndex = 0;
            }
        }
    }

    /**
     * Called every frame. Returns the list of attacks to dispatch **this
     * frame** (usually 0 or 1; can be >1 only if dt is huge in tests).
     */
    public tick(nowMs: number, colorState: PlayerColorState): IAttackDispatchEvent[] {
        if (this.active === 'none') return [];
        const weapon = this.active;
        const ready = this.readyAt.get(weapon) ?? 0;
        if (nowMs < ready) return [];
        return [this.fire(weapon, nowMs, colorState)];
    }

    /** Cancel everything (scene unload, pause, death). */
    public reset(): void {
        this.active = 'none';
        this.pressedAt.clear();
        this.readyAt.clear();
        this.shurikenBurstIndex = 0;
    }

    // ------------------------------------------------------------------

    private fire(weapon: WeaponType, nowMs: number, colorState: PlayerColorState): IAttackDispatchEvent {
        const interval = this.intervalFor(weapon, colorState);
        const comboWithJump = this.isComboWithJump(nowMs);

        let burstIndex = 1;
        if (weapon === WeaponType.Shuriken) {
            this.shurikenBurstIndex = Math.min(SHURIKEN_BURST_MAX, this.shurikenBurstIndex + 1);
            burstIndex = this.shurikenBurstIndex;
        }

        this.readyAt.set(weapon, nowMs + interval * 1000);
        const attackType: AttackType = weapon === WeaponType.Shuriken ? 'shuriken' : 'sword';
        return { weapon, attackType, comboWithJump, burstIndex, ts: nowMs };
    }

    private intervalFor(weapon: WeaponType, color: PlayerColorState): number {
        if (weapon === WeaponType.NinjaSword) return SWORD_INTERVAL;
        return color === PlayerColorState.Yellow ? SHURIKEN_INTERVAL_UPGRADED : SHURIKEN_INTERVAL_BASE;
    }

    private isComboWithJump(nowMs: number): boolean {
        const lastJump = this.jumpState.lastJumpPressTs();
        if (lastJump === undefined) return false;
        const dt = Math.abs(nowMs - lastJump);
        return dt <= this.comboWindowMs;
    }
}
