// Keyboard / mouse / gamepad input behind one `Input` global (docs/input-plan.md).
//
// Two kinds of signal, two shapes of API:
//   - BUTTONS — anything that goes down and up: keyboard keys, mouse buttons, gamepad buttons. One
//     code vocabulary ('KeyW', 'MouseLeft', 'GamepadSouth'), one held-poll (`Input.key`) and one
//     event pair (`Input.on('keydown' | 'keyup')`). A mouse button IS a key.
//   - CONTINUOUS — mouse motion/wheel, sticks, triggers: polled every frame (`Input.mouse.delta`,
//     `Input.gamepad(0).axis('leftX')`), never events.
// Bindings ("jump = Space | GamepadSouth") are app code: compare `e.code`, OR a few `Input.key`s.
// Hosts report physical state only (`_creatorUtils.inputKey/inputRead/registerInputEvent`).

/** Continuous channel ids — the `_creatorUtils.inputRead` contract (bridges.d.ts). */
export const InputChannel = {
  MouseX: 0, MouseY: 1, MouseDX: 2, MouseDY: 3, WheelX: 4, WheelY: 5, PointerLocked: 6,
  GamepadCount: 7,
  GamepadBase: 16, GamepadStride: 16,
  PadConnected: 0, PadLeftX: 1, PadLeftY: 2, PadRightX: 3, PadRightY: 4, PadLeftTrigger: 5, PadRightTrigger: 6,
} as const

export type InputEventName = "keydown" | "keyup" | "gamepadconnected" | "gamepaddisconnected"

/** `keydown` / `keyup` payload. `code` is the same string `Input.key()` takes. */
export interface InputKeyEvent {
  readonly type: "keydown" | "keyup"
  /** Button code: KeyboardEvent.code ('KeyW', 'Space'), 'MouseLeft|Right|Middle|Back|Forward', or
   *  'GamepadSouth|East|West|North|L1|R1|L2|R2|Select|Start|L3|R3|Up|Down|Left|Right'. */
  readonly code: string
  /** Which device: 'keyboard' | 'mouse' | 'gamepad'. */
  readonly source: "keyboard" | "mouse" | "gamepad"
  /** Pad index (0..3) for gamepad buttons, undefined otherwise. */
  readonly gamepad?: number
  /** OS auto-repeat keydown (key still held) — ignore for one-shot actions. */
  readonly repeat: boolean
}

export interface InputGamepadEvent {
  readonly type: "gamepadconnected" | "gamepaddisconnected"
  readonly gamepad: number
}

type Listener<E> = (e: E) => void
type InputEventMap = {
  keydown: InputKeyEvent
  keyup: InputKeyEvent
  gamepadconnected: InputGamepadEvent
  gamepaddisconnected: InputGamepadEvent
}

const listeners: { [K in InputEventName]?: Listener<InputEventMap[K]>[] } = {}
let hostListenerInstalled = false

const sourceOf = (code: string): InputKeyEvent["source"] =>
  code.startsWith("Mouse") ? "mouse" : code.startsWith("Gamepad") ? "gamepad" : "keyboard"

const emit = <K extends InputEventName>(name: K, e: InputEventMap[K]) => {
  const list = listeners[name]
  if (!list || list.length === 0) return
  for (const fn of [...list]) fn(e as any)
}

// One fan-out for the host channel and the test/host `_emit` seam.
const deliver = (kind: number, code: string, gamepad: number, repeat: number) => {
  if (kind === 0 || kind === 1) {
    const src = sourceOf(code)
    emit(kind === 0 ? "keydown" : "keyup", {
      type: kind === 0 ? "keydown" : "keyup", code, source: src,
      gamepad: src === "gamepad" && gamepad >= 0 ? gamepad : undefined, repeat: repeat !== 0,
    })
  } else if (kind === 2 || kind === 3) {
    emit(kind === 2 ? "gamepadconnected" : "gamepaddisconnected",
         { type: kind === 2 ? "gamepadconnected" : "gamepaddisconnected", gamepad })
  }
}

// Lazily claim the host's single listener slot (never at module scope — chisel drops top-level
// side effects, and a host without the channel simply never fires).
const ensureHostListener = () => {
  if (hostListenerInstalled) return
  hostListenerInstalled = true
  _creatorUtils.registerInputEvent?.(deliver)
}

const read = (channel: number): number => _creatorUtils.inputRead?.(channel) ?? 0

/** Mouse state — polled. Position is the cursor inside the viewport in logical px; `delta` is the
 *  motion during the previous frame (raw where the OS offers it) and keeps counting while locked —
 *  that's FPS look. Hosts without a mouse report zeros. */
export const mouse = {
  get position(): { x: number, y: number } { return { x: read(InputChannel.MouseX), y: read(InputChannel.MouseY) } },
  get delta(): { x: number, y: number } { return { x: read(InputChannel.MouseDX), y: read(InputChannel.MouseDY) } },
  /** Wheel notches during the previous frame (+y = wheel toward you / scroll down). */
  get wheel(): { x: number, y: number } { return { x: read(InputChannel.WheelX), y: read(InputChannel.WheelY) } },
  /** Pointer lock: cursor hidden + confined, `delta` keeps flowing. Web grants it only from a user
   *  gesture (call it in a 'keydown' handler for MouseLeft); hosts drop it on focus loss and
   *  re-acquire on focus. Escape does NOT unlock by itself on native hosts — call `unlock()`. */
  get locked(): boolean { return read(InputChannel.PointerLocked) !== 0 },
  lock(): boolean { return _creatorUtils.inputSetPointerLock?.(true) ?? false },
  unlock(): void { _creatorUtils.inputSetPointerLock?.(false) },
}

export type GamepadAxisName = "leftX" | "leftY" | "rightX" | "rightY" | "leftTrigger" | "rightTrigger"
const PAD_AXIS: Record<GamepadAxisName, number> = {
  leftX: InputChannel.PadLeftX, leftY: InputChannel.PadLeftY, rightX: InputChannel.PadRightX,
  rightY: InputChannel.PadRightY, leftTrigger: InputChannel.PadLeftTrigger, rightTrigger: InputChannel.PadRightTrigger,
}

export interface GamepadState {
  readonly index: number
  readonly connected: boolean
  /** Stick axes −1..1 (+Y = down, like the web Gamepad API), triggers 0..1. `deadzone` (default
   *  0.15) zeroes small stick drift — pass 0 for the raw value. */
  axis(name: GamepadAxisName, deadzone?: number): number
  /** Held? Same as `Input.key(code, index)`. */
  button(code: string): boolean
}

const padCache: GamepadState[] = []
const gamepadAt = (index: number): GamepadState => {
  let g = padCache[index]
  if (!g) {
    const base = InputChannel.GamepadBase + InputChannel.GamepadStride * index
    g = padCache[index] = {
      index,
      get connected() { return read(base + InputChannel.PadConnected) !== 0 },
      axis(name, deadzone = 0.15) {
        const v = read(base + PAD_AXIS[name])
        return deadzone > 0 && Math.abs(v) < deadzone ? 0 : v
      },
      button(code) { return _creatorUtils.inputKey(code, index) },
    }
  }
  return g
}

export const Input = {
  /**
   * Is a button held? `code` is the physical key (`KeyboardEvent.code`: 'ArrowRight', 'KeyW',
   * 'Space'), a mouse button ('MouseLeft', 'MouseRight', 'MouseMiddle') or a gamepad button
   * ('GamepadSouth' = A/Cross, 'GamepadR2' = right trigger as a button …). Poll inside setLoop for
   * frame-independent movement. `gamepad` scopes a Gamepad* code to one pad; omitted = any pad.
   */
  key(code: string, gamepad?: number): boolean {
    return _creatorUtils.inputKey(code, gamepad)
  },

  /** Listen for a discrete input moment. `keydown`/`keyup` cover EVERY button — keyboard, mouse,
   *  gamepad — filter on `e.code`. Use these for one-shot actions (jump, shoot, charge-release);
   *  `Input.key` for continuous ones (walk). */
  on<K extends InputEventName>(name: K, listener: Listener<InputEventMap[K]>): void {
    ensureHostListener()
    const list = (listeners[name] ??= []) as Listener<InputEventMap[K]>[]
    if (!list.includes(listener)) list.push(listener)
  },
  off<K extends InputEventName>(name: K, listener: Listener<InputEventMap[K]>): void {
    const list = listeners[name] as Listener<InputEventMap[K]>[] | undefined
    if (!list) return
    const i = list.indexOf(listener)
    if (i >= 0) list.splice(i, 1)
  },

  mouse,

  /** Gamepad `index` (0..3). Always returns an object — check `.connected`. */
  gamepad(index = 0): GamepadState { return gamepadAt(index) },
  /** Indices of the currently connected gamepads. */
  gamepads(): number[] {
    const out: number[] = []
    const n = read(InputChannel.GamepadCount)
    for (let i = 0; i < 4 && out.length < n; i++) if (gamepadAt(i).connected) out.push(i)
    return out
  },

  /** @internal — hosts/tests: deliver an event exactly as the host listener would. */
  _emit(kind: number, code: string, gamepad: number, repeat: number): void { deliver(kind, code, gamepad, repeat) },
}
