/**
 * Keyboard input, mapped to named actions.
 *
 * Reads `KeyboardEvent.code` rather than `.key` so the bindings are physical
 * positions — WASD stays WASD on an AZERTY layout.
 */

export type GameAction =
  | "forward"
  | "back"
  | "left"
  | "right"
  | "run"
  | "interact"
  | "quickTurn"
  | "aim"
  | "inventory"
  | "cancel"
  | "toggleControls"
  | "toggleDebug";

const DEFAULT_BINDINGS: Record<string, GameAction> = {
  KeyW: "forward",
  ArrowUp: "forward",
  KeyS: "back",
  ArrowDown: "back",
  KeyA: "left",
  ArrowLeft: "left",
  KeyD: "right",
  ArrowRight: "right",
  ShiftLeft: "run",
  ShiftRight: "run",
  KeyE: "interact",
  Enter: "interact",
  Space: "quickTurn",
  KeyQ: "aim",
  Tab: "inventory",
  Escape: "cancel",
  KeyC: "toggleControls",
  Backquote: "toggleDebug",
};

/** Actions where a stray browser default (scroll, focus change) would hurt. */
const SWALLOW_DEFAULT: ReadonlySet<GameAction> = new Set<GameAction>([
  "forward",
  "back",
  "left",
  "right",
  "quickTurn",
  "inventory",
]);

export class InputManager {
  private readonly held = new Set<GameAction>();
  private readonly pressedThisTick = new Set<GameAction>();
  private readonly bindings: Record<string, GameAction>;
  private attached = false;

  constructor(bindings: Record<string, GameAction> = DEFAULT_BINDINGS) {
    this.bindings = { ...bindings };
  }

  attach(target: Window | HTMLElement = window): () => void {
    if (this.attached) throw new Error("InputManager is already attached");
    this.attached = true;

    const onKeyDown = (event: Event) => {
      const action = this.bindings[(event as KeyboardEvent).code];
      if (!action) return;
      if (SWALLOW_DEFAULT.has(action)) event.preventDefault();
      // Ignore auto-repeat so `wasPressed` stays edge-triggered.
      if (!(event as KeyboardEvent).repeat && !this.held.has(action)) {
        this.pressedThisTick.add(action);
      }
      this.held.add(action);
    };

    const onKeyUp = (event: Event) => {
      const action = this.bindings[(event as KeyboardEvent).code];
      if (action) this.held.delete(action);
    };

    // Losing focus mid-key would otherwise leave the player walking forever.
    const onBlur = () => this.held.clear();

    target.addEventListener("keydown", onKeyDown);
    target.addEventListener("keyup", onKeyUp);
    window.addEventListener("blur", onBlur);

    return () => {
      target.removeEventListener("keydown", onKeyDown);
      target.removeEventListener("keyup", onKeyUp);
      window.removeEventListener("blur", onBlur);
      this.attached = false;
    };
  }

  isHeld(action: GameAction): boolean {
    return this.held.has(action);
  }

  /** True only on the tick the key went down. Call `endTick` to clear. */
  wasPressed(action: GameAction): boolean {
    return this.pressedThisTick.has(action);
  }

  /** Signed axis helper: -1, 0 or 1. */
  axis(negative: GameAction, positive: GameAction): number {
    return (this.held.has(positive) ? 1 : 0) - (this.held.has(negative) ? 1 : 0);
  }

  endTick(): void {
    this.pressedThisTick.clear();
  }
}
