import { Capacitor } from "@capacitor/core";

export interface ControllerTokenEvent {
  token: string;
  repeat: boolean;
}

export interface ControllerMovement {
  moveX: number;
  moveY: number;
  lookX: number;
  lookY: number;
}

type TokenListener = (event: ControllerTokenEvent) => void;
type MovementListener = (movement: Readonly<ControllerMovement>) => void;

const NATIVE_TOKEN_EVENT = "iwt-native-controller";
const NATIVE_MOTION_EVENT = "iwt-native-controller-motion";
const NATIVE_RESET_EVENT = "iwt-native-controller-reset";
const INITIAL_REPEAT_MS = 280;
const REPEAT_MS = 90;
const AXIS_THRESHOLD = 0.55;
const MOVEMENT_AXIS_STEPS = 128;
const BUTTON_TOKENS = Array.from({ length: 32 }, (_, index) => `Button${index}`);
const NEGATIVE_AXIS_TOKENS = Array.from({ length: 16 }, (_, index) => `Axis${index}-`);
const POSITIVE_AXIS_TOKENS = Array.from({ length: 16 }, (_, index) => `Axis${index}+`);

const listeners = new Set<TokenListener>();
const movementListeners = new Set<MovementListener>();
const repeatAt = new Map<string, number>();
const lastNativeTokenAt = new Map<string, number>();
let previousTokens = new Set<string>();
let currentTokens = new Set<string>();
let movement: ControllerMovement = { moveX: 0, moveY: 0, lookX: 0, lookY: 0 };
let stopService: (() => void) | null = null;
let dispatchDepth = 0;

function connectedGamepad() {
  const gamepads = navigator.getGamepads?.();
  if (!gamepads) return null;
  for (let index = 0; index < gamepads.length; index += 1) {
    if (gamepads[index]) return gamepads[index];
  }
  return null;
}

function tokensFor(gamepad: Gamepad, tokens: Set<string>) {
  tokens.clear();
  for (let index = 0; index < gamepad.buttons.length; index += 1) {
    const button = gamepad.buttons[index];
    if (button.pressed || button.value > 0.65) tokens.add(BUTTON_TOKENS[index] ?? `Button${index}`);
  }
  for (let index = 0; index < gamepad.axes.length; index += 1) {
    const value = gamepad.axes[index];
    if (value <= -AXIS_THRESHOLD) tokens.add(NEGATIVE_AXIS_TOKENS[index] ?? `Axis${index}-`);
    if (value >= AXIS_THRESHOLD) tokens.add(POSITIVE_AXIS_TOKENS[index] ?? `Axis${index}+`);
  }
}

function canRepeat(token: string) {
  return token === "Button12"
    || token === "Button13"
    || token === "Button14"
    || token === "Button15"
    || token.startsWith("Axis0")
    || token.startsWith("Axis1");
}

export function emitControllerToken(event: ControllerTokenEvent) {
  dispatchDepth += 1;
  try {
    for (const listener of listeners) listener(event);
  } finally {
    dispatchDepth -= 1;
  }
}

export function isControllerDispatchActive() {
  return dispatchDepth > 0;
}

export function subscribeControllerToken(listener: TokenListener) {
  listeners.add(listener);
  return () => {
    listeners.delete(listener);
  };
}

export function subscribeControllerMovement(listener: MovementListener) {
  movementListeners.add(listener);
  return () => {
    movementListeners.delete(listener);
  };
}

export function getControllerMovement(): Readonly<ControllerMovement> {
  return movement;
}

export function setExternalControllerMovement(next: ControllerMovement) {
  setExternalControllerAxes(next.moveX, next.moveY, next.lookX, next.lookY);
}

function normalizedAxis(value: number) {
  return Math.abs(value) >= 0.12 ? Math.round(value * MOVEMENT_AXIS_STEPS) / MOVEMENT_AXIS_STEPS : 0;
}

function setExternalControllerAxes(nextMoveX: number, nextMoveY: number, nextLookX: number, nextLookY: number) {
  const moveX = normalizedAxis(nextMoveX);
  const moveY = normalizedAxis(nextMoveY);
  const lookX = normalizedAxis(nextLookX);
  const lookY = normalizedAxis(nextLookY);
  if (
    moveX === movement.moveX
    && moveY === movement.moveY
    && lookX === movement.lookX
    && lookY === movement.lookY
  ) return;
  movement = { moveX, moveY, lookX, lookY };
  for (const listener of movementListeners) listener(movement);
}

export function resetControllerState() {
  previousTokens.clear();
  currentTokens.clear();
  repeatAt.clear();
  lastNativeTokenAt.clear();
  setExternalControllerAxes(0, 0, 0, 0);
}

export function startControllerInput() {
  if (stopService) return stopService;

  const onNativeToken = (event: Event) => {
    const detail = (event as CustomEvent<ControllerTokenEvent>).detail;
    if (!detail?.token) return;
    const now = performance.now();
    const lastAt = lastNativeTokenAt.get(detail.token) ?? Number.NEGATIVE_INFINITY;
    // Some Android controllers report one D-pad edge through both key and hat-axis paths.
    if (now - lastAt < 24) return;
    lastNativeTokenAt.set(detail.token, now);
    emitControllerToken({ token: detail.token, repeat: Boolean(detail.repeat) });
  };
  const onNativeMotion = (event: Event) => {
    const detail = (event as CustomEvent<ControllerMovement>).detail;
    if (detail) setExternalControllerMovement(detail);
  };
  const onReset = () => resetControllerState();
  const onVisibility = () => {
    if (document.visibilityState !== "visible") resetControllerState();
  };

  window.addEventListener(NATIVE_TOKEN_EVENT, onNativeToken);
  window.addEventListener(NATIVE_MOTION_EVENT, onNativeMotion);
  window.addEventListener(NATIVE_RESET_EVENT, onReset);
  document.addEventListener("visibilitychange", onVisibility);

  let frame = 0;
  let hadConnectedGamepad = false;
  const poll = (now: number) => {
    const gamepad = connectedGamepad();
    if (!gamepad) {
      // Clear held input once on a disconnect edge, not on every idle frame.
      if (hadConnectedGamepad) resetControllerState();
      hadConnectedGamepad = false;
    } else {
      hadConnectedGamepad = true;
      setExternalControllerAxes(
        gamepad.axes[0] ?? 0,
        gamepad.axes[1] ?? 0,
        gamepad.axes[2] ?? 0,
        gamepad.axes[3] ?? 0,
      );
      tokensFor(gamepad, currentTokens);
      for (const token of currentTokens) {
        const pressed = !previousTokens.has(token);
        const nextRepeat = repeatAt.get(token) ?? 0;
        if (pressed || (canRepeat(token) && now >= nextRepeat)) {
          emitControllerToken({ token, repeat: !pressed });
          repeatAt.set(token, now + (pressed ? INITIAL_REPEAT_MS : REPEAT_MS));
        }
      }
      for (const token of repeatAt.keys()) {
        if (!currentTokens.has(token)) repeatAt.delete(token);
      }
      const scratch = previousTokens;
      previousTokens = currentTokens;
      currentTokens = scratch;
    }
    frame = window.requestAnimationFrame(poll);
  };

  // Android key/motion events are authoritative. Browser Gamepad polling remains the fallback.
  if (!Capacitor.isNativePlatform()) frame = window.requestAnimationFrame(poll);

  stopService = () => {
    window.removeEventListener(NATIVE_TOKEN_EVENT, onNativeToken);
    window.removeEventListener(NATIVE_MOTION_EVENT, onNativeMotion);
    window.removeEventListener(NATIVE_RESET_EVENT, onReset);
    document.removeEventListener("visibilitychange", onVisibility);
    if (frame) window.cancelAnimationFrame(frame);
    resetControllerState();
    stopService = null;
  };
  return stopService;
}
