/** Device-neutral, multi-source play input state. */

export type PlayDigitalControl =
  | "forward"
  | "backward"
  | "left"
  | "right"
  | "ascend"
  | "descend"
  | "pitch-up"
  | "pitch-down"
  | "primary"
  | "secondary";

export type PlayEdgeControl = "assist" | "reset" | "camera";

export interface PlayAxes {
  moveX: number;
  moveY: number;
  lookX: number;
  lookY: number;
  throttle: number;
  brake: number;
}

export interface PlayInputSnapshot extends PlayAxes {
  ascend: number;
  descend: number;
  primary: boolean;
  secondary: boolean;
}

export const NEUTRAL_PLAY_INPUT: Readonly<PlayInputSnapshot> = Object.freeze({
  moveX: 0,
  moveY: 0,
  lookX: 0,
  lookY: 0,
  throttle: 0,
  brake: 0,
  ascend: 0,
  descend: 0,
  primary: false,
  secondary: false,
});

interface SourceState {
  digital: Set<PlayDigitalControl>;
  axes: PlayAxes;
}

function clamp(value: number, min = -1, max = 1): number {
  return Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : 0;
}

function neutralAxes(): PlayAxes {
  return { moveX: 0, moveY: 0, lookX: 0, lookY: 0, throttle: 0, brake: 0 };
}

function strongest(current: number, candidate: number): number {
  return Math.abs(candidate) > Math.abs(current) ? candidate : current;
}

/**
 * Input sources never share held state. A pointer release therefore cannot
 * cancel a keyboard key or another finger that is still down.
 */
export class PlayInputRouter {
  private readonly sources = new Map<string, SourceState>();
  private readonly edges = new Set<PlayEdgeControl>();

  private source(id: string): SourceState {
    const existing = this.sources.get(id);
    if (existing) return existing;
    const created = { digital: new Set<PlayDigitalControl>(), axes: neutralAxes() };
    this.sources.set(id, created);
    return created;
  }

  setDigital(sourceId: string, control: PlayDigitalControl, pressed: boolean): void {
    const source = this.source(sourceId);
    if (pressed) source.digital.add(control);
    else source.digital.delete(control);
    this.dropEmpty(sourceId, source);
  }

  setAxes(sourceId: string, axes: Partial<PlayAxes>): void {
    const source = this.source(sourceId);
    source.axes = {
      moveX: clamp(axes.moveX ?? 0),
      moveY: clamp(axes.moveY ?? 0),
      lookX: clamp(axes.lookX ?? 0),
      lookY: clamp(axes.lookY ?? 0),
      throttle: clamp(axes.throttle ?? 0, 0, 1),
      brake: clamp(axes.brake ?? 0, 0, 1),
    };
    this.dropEmpty(sourceId, source);
  }

  request(control: PlayEdgeControl): void {
    this.edges.add(control);
  }

  consumeRequests(): ReadonlySet<PlayEdgeControl> {
    const result = new Set(this.edges);
    this.edges.clear();
    return result;
  }

  clearSource(sourceId: string): void {
    this.sources.delete(sourceId);
  }

  clearAll(): void {
    this.sources.clear();
    this.edges.clear();
  }

  snapshot(): PlayInputSnapshot {
    let moveX = 0;
    let moveY = 0;
    let lookX = 0;
    let lookY = 0;
    let throttle = 0;
    let brake = 0;
    let ascend = 0;
    let descend = 0;
    let primary = false;
    let secondary = false;
    for (const source of this.sources.values()) {
      const horizontal = (source.digital.has("right") ? 1 : 0) -
        (source.digital.has("left") ? 1 : 0);
      const vertical = (source.digital.has("forward") ? 1 : 0) -
        (source.digital.has("backward") ? 1 : 0);
      const pitch = (source.digital.has("pitch-up") ? 1 : 0) -
        (source.digital.has("pitch-down") ? 1 : 0);
      moveX = strongest(moveX, strongest(source.axes.moveX, horizontal));
      moveY = strongest(moveY, strongest(source.axes.moveY, vertical));
      lookX = strongest(lookX, source.axes.lookX);
      lookY = strongest(lookY, strongest(source.axes.lookY, pitch));
      throttle = Math.max(throttle, source.axes.throttle);
      brake = Math.max(brake, source.axes.brake);
      ascend = Math.max(ascend, source.digital.has("ascend") ? 1 : 0);
      descend = Math.max(descend, source.digital.has("descend") ? 1 : 0);
      primary ||= source.digital.has("primary");
      secondary ||= source.digital.has("secondary");
    }
    return {
      moveX,
      moveY,
      lookX,
      lookY,
      throttle,
      brake,
      ascend,
      descend,
      primary,
      secondary,
    };
  }

  activeSourceCount(): number {
    return this.sources.size;
  }

  private dropEmpty(id: string, source: SourceState): void {
    const axes = source.axes;
    if (
      source.digital.size === 0 && axes.moveX === 0 && axes.moveY === 0 &&
      axes.lookX === 0 && axes.lookY === 0 && axes.throttle === 0 && axes.brake === 0
    ) this.sources.delete(id);
  }
}

export interface StandardPlayGamepadLike {
  axes: readonly number[];
  buttons: readonly { pressed: boolean; value: number }[];
}

export interface StandardPlayGamepadButtons {
  assist: boolean;
  reset: boolean;
  camera: boolean;
}

export interface StandardPlayGamepadSample {
  axes: PlayAxes;
  digital: ReadonlySet<PlayDigitalControl>;
  requests: ReadonlySet<PlayEdgeControl>;
  buttons: StandardPlayGamepadButtons;
}

function axis(value: number | undefined, deadzone = 0.12): number {
  const raw = clamp(value ?? 0);
  if (Math.abs(raw) <= deadzone) return 0;
  return Math.sign(raw) * ((Math.abs(raw) - deadzone) / (1 - deadzone));
}

function button(pad: StandardPlayGamepadLike, index: number): number {
  const found = pad.buttons[index];
  if (!found) return 0;
  return clamp(Number.isFinite(found.value) ? found.value : found.pressed ? 1 : 0, 0, 1);
}

/** Standard layout: two sticks, triggers, shoulders and rising-edge face buttons. */
export function sampleStandardPlayGamepad(
  pad: StandardPlayGamepadLike,
  previous: StandardPlayGamepadButtons = { assist: false, reset: false, camera: false },
): StandardPlayGamepadSample {
  const buttons = {
    assist: button(pad, 3) > 0.5,
    reset: button(pad, 2) > 0.5,
    camera: button(pad, 9) > 0.5,
  };
  const digital = new Set<PlayDigitalControl>();
  if (button(pad, 5) > 0.5) digital.add("ascend");
  if (button(pad, 4) > 0.5) digital.add("descend");
  if (button(pad, 1) > 0.5) digital.add("primary");
  if (button(pad, 0) > 0.5) digital.add("secondary");
  const requests = new Set<PlayEdgeControl>();
  if (buttons.assist && !previous.assist) requests.add("assist");
  if (buttons.reset && !previous.reset) requests.add("reset");
  if (buttons.camera && !previous.camera) requests.add("camera");
  return {
    axes: {
      moveX: axis(pad.axes[0]),
      moveY: -axis(pad.axes[1]),
      lookX: axis(pad.axes[2]),
      lookY: -axis(pad.axes[3]),
      brake: button(pad, 6),
      throttle: button(pad, 7),
    },
    digital,
    requests,
    buttons,
  };
}

export interface PlanarDirection {
  x: number;
  z: number;
}

/** Camera-relative input without importing a renderer or allocating vectors. */
export function cameraRelativePlanar(
  input: Pick<PlayInputSnapshot, "moveX" | "moveY">,
  cameraForward: Readonly<PlanarDirection>,
): PlanarDirection {
  const length = Math.hypot(cameraForward.x, cameraForward.z);
  const fx = length > 1e-8 ? cameraForward.x / length : 0;
  const fz = length > 1e-8 ? cameraForward.z / length : -1;
  const rx = -fz;
  const rz = fx;
  const x = rx * input.moveX + fx * input.moveY;
  const z = rz * input.moveX + fz * input.moveY;
  const magnitude = Math.hypot(x, z);
  return magnitude > 1 ? { x: x / magnitude, z: z / magnitude } : { x, z };
}

export function vehicleActionsFromPlay(
  input: Readonly<PlayInputSnapshot>,
): {
  throttle: number;
  brake: number;
  steering: number;
  handbrake: boolean;
} {
  return {
    throttle: Math.max(input.throttle, input.moveY > 0 ? input.moveY : 0),
    brake: Math.max(input.brake, input.moveY < 0 ? -input.moveY : 0),
    steering: input.moveX,
    handbrake: input.primary,
  };
}

export function aircraftActionsFromPlay(
  input: Readonly<PlayInputSnapshot>,
): { throttle: number; yaw: number; pitch: number; roll: number } {
  return {
    throttle: Math.max(input.throttle, input.primary ? 1 : 0),
    yaw: clamp(input.ascend - input.descend + input.lookX),
    pitch: input.moveY !== 0 ? input.moveY : input.lookY,
    roll: input.moveX,
  };
}

export function crowActionsFromPlay(
  input: Readonly<PlayInputSnapshot>,
): {
  forward: number;
  right: number;
  turn: number;
  pitch: number;
  climb: number;
  sprint: boolean;
  glide: boolean;
} {
  return {
    forward: input.moveY,
    right: 0,
    turn: strongest(input.moveX, input.lookX),
    pitch: input.lookY,
    climb: clamp(input.ascend - input.descend),
    sprint: false,
    glide: input.secondary,
  };
}

/** Convert a desired camera-relative world direction into the actor's local axes. */
export function groundActorActionsFromPlay(
  input: Readonly<PlayInputSnapshot>,
  yaw: number,
  cameraForward: Readonly<PlanarDirection>,
): {
  forward: number;
  right: number;
  turn: number;
  sprint: boolean;
} {
  const desired = cameraRelativePlanar(input, cameraForward);
  const cos = Math.cos(yaw);
  const sin = Math.sin(yaw);
  return {
    right: desired.x * cos - desired.z * sin,
    forward: -desired.x * sin - desired.z * cos,
    turn: input.lookX,
    sprint: input.primary,
  };
}
