import { EMPTY_INPUT, type GameInput } from './types';

const LEFT = ['ArrowLeft', 'KeyA'];
const RIGHT = ['ArrowRight', 'KeyD'];
const JUMP = ['Space', 'KeyZ'];
const SHOOT = ['KeyJ', 'KeyX'];
const DASH = ['ShiftLeft', 'ShiftRight', 'KeyC'];
const GAME_KEYS = new Set([...LEFT, ...RIGHT, ...JUMP, ...SHOOT, ...DASH]);

/** Physical-key tracking keeps alternate bindings from releasing each other. */
export class KeyboardInput {
  private readonly held = new Set<string>();
  private edges: GameInput = { ...EMPTY_INPUT };
  private disposed = false;

  constructor(
    private readonly onPause: () => void,
    private readonly onConfirm: () => void,
    private readonly onBlur: () => void,
  ) {
    if (typeof window === 'undefined') return;
    window.addEventListener('keydown', this.keyDown);
    window.addEventListener('keyup', this.keyUp);
    window.addEventListener('blur', this.blur);
    document.addEventListener('visibilitychange', this.visibilityChange);
  }

  read(): GameInput {
    const input = {
      ...this.edges,
      left: this.isHeld(LEFT),
      right: this.isHeld(RIGHT),
      jump: this.isHeld(JUMP),
      shoot: this.isHeld(SHOOT),
    };
    this.edges = { ...EMPTY_INPUT };
    return input;
  }

  clear(): void {
    this.held.clear();
    this.edges = { ...EMPTY_INPUT };
  }

  dispose(): void {
    if (this.disposed) return;
    this.disposed = true;
    this.clear();
    if (typeof window === 'undefined') return;
    window.removeEventListener('keydown', this.keyDown);
    window.removeEventListener('keyup', this.keyUp);
    window.removeEventListener('blur', this.blur);
    document.removeEventListener('visibilitychange', this.visibilityChange);
  }

  private isHeld(codes: string[]): boolean {
    return codes.some((code) => this.held.has(code));
  }

  private isUiKey(event: KeyboardEvent): boolean {
    const target = event.target;
    if (!(target instanceof Element)) return false;
    if (
      target.closest('form, input, textarea, select, [role="textbox"]') ||
      (target instanceof HTMLElement && target.isContentEditable)
    ) return true;
    return (event.code === 'Space' || event.code === 'Enter' || event.code === 'NumpadEnter') &&
      Boolean(target.closest('button, [role="button"], a[href], summary'));
  }

  private keyDown = (event: KeyboardEvent): void => {
    if (event.defaultPrevented || this.isUiKey(event) || event.ctrlKey || event.altKey || event.metaKey) return;
    const code = event.code;
    if (code === 'Escape' || code === 'KeyP') {
      event.preventDefault();
      if (!event.repeat) this.onPause();
      return;
    }
    if (code === 'Enter' || code === 'NumpadEnter') {
      event.preventDefault();
      if (!event.repeat) this.onConfirm();
      return;
    }
    if (!GAME_KEYS.has(code)) return;
    event.preventDefault();
    if (event.repeat || this.held.has(code)) return;

    const wasJumping = this.isHeld(JUMP);
    const wasShooting = this.isHeld(SHOOT);
    const wasDashing = this.isHeld(DASH);
    this.held.add(code);
    if (!wasJumping && this.isHeld(JUMP)) this.edges.jumpPressed = true;
    if (!wasShooting && this.isHeld(SHOOT)) this.edges.shootPressed = true;
    if (!wasDashing && this.isHeld(DASH)) this.edges.dashPressed = true;
  };

  private keyUp = (event: KeyboardEvent): void => {
    const code = event.code;
    if (!GAME_KEYS.has(code)) return;
    if (!this.isUiKey(event) && !event.ctrlKey && !event.altKey && !event.metaKey) event.preventDefault();
    const wasJumping = this.isHeld(JUMP);
    const wasShooting = this.isHeld(SHOOT);
    this.held.delete(code);
    if (wasJumping && !this.isHeld(JUMP)) this.edges.jumpReleased = true;
    if (wasShooting && !this.isHeld(SHOOT)) this.edges.shootReleased = true;
  };

  private blur = (): void => {
    this.clear();
    this.onBlur();
  };

  private visibilityChange = (): void => {
    if (document.hidden) this.blur();
  };
}
