import {
  HOCKEY_ARENA_MAX_X,
  HOCKEY_ARENA_MIN_X,
  HOCKEY_ARENA_WIDTH,
  HOCKEY_HEALER_GOAL_Z,
  HOCKEY_MIDLINE_Z,
  HOCKEY_NPC_GOAL_Z,
  hockeyReturnDirection,
} from "./hockeyHealing";
import type { WorldPosition } from "./types";

export const BLOCKBREAKER_COLUMN_COUNT = 5;
export const BLOCKBREAKER_BRICK_COLORS = ["cyan", "amber", "magenta", "lime"] as const;
export type BlockbreakerBrickColor = typeof BLOCKBREAKER_BRICK_COLORS[number];

export interface BlockbreakerBrick {
  id: string;
  row: number;
  column: number;
  color: BlockbreakerBrickColor;
}

export type BlockbreakerStatus = "inactive" | "live" | "reserving" | "lost";

export interface BlockbreakerState {
  status: BlockbreakerStatus;
  seed: number;
  randomState: number;
  bricks: BlockbreakerBrick[];
  puckPosition: WorldPosition;
  puckVelocity: WorldPosition;
  aimDirection: WorldPosition;
  bricksBroken: number;
  score: number;
  rowsSpawned: number;
  breaches: number;
  nextRowAt: number;
  serveIndex: number;
  reServeAt: number | null;
  lastBreakAt: number;
  lastBreakCount: number;
  lastScoreAward: number;
  lostAt: number | null;
}

export interface BlockbreakerStep {
  delta: number;
  time: number;
  playerPosition: WorldPosition;
}

export const BLOCKBREAKER_PUCK_RADIUS = 0.42;
export const BLOCKBREAKER_BRICK_WIDTH = HOCKEY_ARENA_WIDTH / BLOCKBREAKER_COLUMN_COUNT - 0.28;
export const BLOCKBREAKER_BRICK_DEPTH = 1.34;
export const BLOCKBREAKER_ROW_SPACING = 1.72;
export const BLOCKBREAKER_SPAWN_Z = HOCKEY_NPC_GOAL_Z + 1.42;
export const BLOCKBREAKER_DANGER_Z = HOCKEY_MIDLINE_Z + (HOCKEY_HEALER_GOAL_Z - HOCKEY_MIDLINE_Z) * 0.5;
export const BLOCKBREAKER_RESERVE_SECONDS = 1;
export const BLOCKBREAKER_BREACH_DAMAGE = 25;
export const BLOCKBREAKER_PUCK_SPEED = 7.25;
export const BLOCKBREAKER_STARTING_ROW_INTERVAL = 10;
export const BLOCKBREAKER_MINIMUM_ROW_INTERVAL = 3;
export const BLOCKBREAKER_MAX_ROWS = Math.ceil((BLOCKBREAKER_DANGER_Z - BLOCKBREAKER_SPAWN_Z) / BLOCKBREAKER_ROW_SPACING) + 1;
export const BLOCKBREAKER_MAX_BRICKS = BLOCKBREAKER_MAX_ROWS * BLOCKBREAKER_COLUMN_COUNT;

const PLAYER_INTERCEPT_RADIUS = 1.05;
const MAX_SUBSTEPS = 12;
const MAX_SUBSTEP_DISTANCE = 0.3;
const MAX_ROW_SPAWNS_PER_STEP = 8;
const SERVE_LANES = [0, -0.62, 0.68, -0.3, 0.36, -0.8, 0.82] as const;

function normalizeSeed(seed: number) {
  const normalized = Math.floor(Number(seed)) >>> 0;
  return normalized || 0x9e3779b9;
}

export function createBlockbreakerSeed(random: () => number = Math.random) {
  const sample = Number(random());
  const normalized = Number.isFinite(sample) ? Math.max(0, Math.min(0.999999999999, sample)) : 0;
  return normalizeSeed(Math.floor(normalized * 0x100000000));
}

function nextRandom(state: number) {
  let next = normalizeSeed(state);
  next ^= next << 13;
  next ^= next >>> 17;
  next ^= next << 5;
  return next >>> 0;
}

export function blockbreakerColumnX(column: number) {
  const laneWidth = HOCKEY_ARENA_WIDTH / BLOCKBREAKER_COLUMN_COUNT;
  return HOCKEY_ARENA_MIN_X + laneWidth * (Math.max(0, Math.min(BLOCKBREAKER_COLUMN_COUNT - 1, column)) + 0.5);
}

export function blockbreakerRowZ(row: number) {
  return BLOCKBREAKER_SPAWN_Z + Math.max(0, row) * BLOCKBREAKER_ROW_SPACING;
}

export function blockbreakerRowInterval(elapsedSeconds: number) {
  const minute = Math.max(0, Math.floor((Number(elapsedSeconds) || 0) / 60));
  return Math.max(BLOCKBREAKER_MINIMUM_ROW_INTERVAL, BLOCKBREAKER_STARTING_ROW_INTERVAL * 0.9 ** minute);
}

export function blockbreakerTimeMultiplierTenths(elapsedSeconds: number) {
  return 10 + Math.max(0, Math.floor((Number(elapsedSeconds) || 0) / 30));
}

export function blockbreakerTimeMultiplier(elapsedSeconds: number) {
  return blockbreakerTimeMultiplierTenths(elapsedSeconds) / 10;
}

export function blockbreakerClusterBase(clusterSize: number) {
  const size = Math.max(0, Math.floor(Number(clusterSize) || 0));
  return 10 * size * (size + 1) / 2;
}

export function blockbreakerClusterScore(clusterSize: number, elapsedSeconds: number) {
  return blockbreakerClusterBase(clusterSize) * blockbreakerTimeMultiplierTenths(elapsedSeconds) / 10;
}

function generateRow(randomState: number, rowSequence: number) {
  const bricks: BlockbreakerBrick[] = [];
  let nextState = randomState;
  for (let column = 0; column < BLOCKBREAKER_COLUMN_COUNT; column += 1) {
    nextState = nextRandom(nextState);
    bricks.push({
      id: `${rowSequence}:${column}`,
      row: 0,
      column,
      color: BLOCKBREAKER_BRICK_COLORS[nextState % BLOCKBREAKER_BRICK_COLORS.length],
    });
  }
  return { bricks, randomState: nextState };
}

function servePosition(serveIndex: number): WorldPosition {
  const lane = SERVE_LANES[serveIndex % SERVE_LANES.length];
  return [lane * (HOCKEY_ARENA_MAX_X - 1.25), BLOCKBREAKER_SPAWN_Z + BLOCKBREAKER_BRICK_DEPTH * 0.5 + 0.72];
}

function serveVelocity(position: WorldPosition, serveIndex: number): WorldPosition {
  const lane = SERVE_LANES[(serveIndex + 3) % SERVE_LANES.length];
  const target: WorldPosition = [lane * (HOCKEY_ARENA_MAX_X - 1.4), HOCKEY_HEALER_GOAL_Z];
  const dx = target[0] - position[0];
  const dz = target[1] - position[1];
  const length = Math.max(0.0001, Math.hypot(dx, dz));
  return [dx / length * BLOCKBREAKER_PUCK_SPEED, dz / length * BLOCKBREAKER_PUCK_SPEED];
}

export function createBlockbreakerState(active = false, requestedSeed = 1): BlockbreakerState {
  const seed = normalizeSeed(requestedSeed);
  const row = generateRow(seed, 0);
  const puckPosition = servePosition(0);
  return {
    status: active ? "live" : "inactive",
    seed,
    randomState: row.randomState,
    bricks: row.bricks,
    puckPosition,
    puckVelocity: active ? serveVelocity(puckPosition, 0) : [0, 0],
    aimDirection: [0, -1],
    bricksBroken: 0,
    score: 0,
    rowsSpawned: 1,
    breaches: 0,
    nextRowAt: BLOCKBREAKER_STARTING_ROW_INTERVAL,
    serveIndex: 0,
    reServeAt: null,
    lastBreakAt: Number.NEGATIVE_INFINITY,
    lastBreakCount: 0,
    lastScoreAward: 0,
    lostAt: null,
  };
}

export function setBlockbreakerAim(state: BlockbreakerState, aim: WorldPosition): BlockbreakerState {
  const x = Number.isFinite(aim[0]) ? aim[0] : 0;
  const z = Number.isFinite(aim[1]) ? aim[1] : 0;
  if (state.aimDirection[0] === x && state.aimDirection[1] === z) return state;
  return { ...state, aimDirection: [x, z] };
}

export function blockbreakerConnectedCluster(bricks: readonly BlockbreakerBrick[], source: BlockbreakerBrick) {
  const byCell = new Map(bricks.map((brick) => [`${brick.row}:${brick.column}`, brick]));
  const connected: BlockbreakerBrick[] = [];
  const visited = new Set<string>();
  const pending: BlockbreakerBrick[] = [source];
  while (pending.length) {
    const brick = pending.pop()!;
    if (visited.has(brick.id) || brick.color !== source.color) continue;
    visited.add(brick.id);
    connected.push(brick);
    for (const [row, column] of [
      [brick.row - 1, brick.column],
      [brick.row + 1, brick.column],
      [brick.row, brick.column - 1],
      [brick.row, brick.column + 1],
    ]) {
      const neighbor = byCell.get(`${row}:${column}`);
      if (neighbor && !visited.has(neighbor.id) && neighbor.color === source.color) pending.push(neighbor);
    }
  }
  return connected;
}

function segmentDistanceSquared(
  startX: number,
  startZ: number,
  endX: number,
  endZ: number,
  point: WorldPosition,
) {
  const dx = endX - startX;
  const dz = endZ - startZ;
  const lengthSquared = dx * dx + dz * dz;
  const projection = lengthSquared < 0.000001
    ? 0
    : Math.max(0, Math.min(1, ((point[0] - startX) * dx + (point[1] - startZ) * dz) / lengthSquared));
  const nearestX = startX + dx * projection;
  const nearestZ = startZ + dz * projection;
  return (point[0] - nearestX) ** 2 + (point[1] - nearestZ) ** 2;
}

type SegmentHit = { time: number; normalX: number; normalZ: number };

function segmentExpandedBrickHit(start: WorldPosition, end: WorldPosition, brick: BlockbreakerBrick): SegmentHit | null {
  const centerX = blockbreakerColumnX(brick.column);
  const centerZ = blockbreakerRowZ(brick.row);
  const minX = centerX - BLOCKBREAKER_BRICK_WIDTH * 0.5 - BLOCKBREAKER_PUCK_RADIUS;
  const maxX = centerX + BLOCKBREAKER_BRICK_WIDTH * 0.5 + BLOCKBREAKER_PUCK_RADIUS;
  const minZ = centerZ - BLOCKBREAKER_BRICK_DEPTH * 0.5 - BLOCKBREAKER_PUCK_RADIUS;
  const maxZ = centerZ + BLOCKBREAKER_BRICK_DEPTH * 0.5 + BLOCKBREAKER_PUCK_RADIUS;
  const dx = end[0] - start[0];
  const dz = end[1] - start[1];
  let entry = 0;
  let exit = 1;
  let normalX = 0;
  let normalZ = 0;
  for (const axis of [
    { start: start[0], delta: dx, min: minX, max: maxX, nx: -Math.sign(dx), nz: 0 },
    { start: start[1], delta: dz, min: minZ, max: maxZ, nx: 0, nz: -Math.sign(dz) },
  ]) {
    if (Math.abs(axis.delta) < 0.000001) {
      if (axis.start < axis.min || axis.start > axis.max) return null;
      continue;
    }
    const near = (axis.min - axis.start) / axis.delta;
    const far = (axis.max - axis.start) / axis.delta;
    const axisEntry = Math.min(near, far);
    const axisExit = Math.max(near, far);
    if (axisEntry > entry) {
      entry = axisEntry;
      normalX = axis.nx;
      normalZ = axis.nz;
    }
    exit = Math.min(exit, axisExit);
    if (entry > exit) return null;
  }
  if (entry < 0 || entry > 1 || exit < 0) return null;
  if (normalX === 0 && normalZ === 0) {
    if (Math.abs(dx) > Math.abs(dz)) normalX = -Math.sign(dx);
    else normalZ = -Math.sign(dz);
  }
  return { time: entry, normalX, normalZ };
}

function addBlockbreakerRow(state: BlockbreakerState, spawnedAt: number) {
  const shifted = state.bricks.map((brick) => ({ ...brick, row: brick.row + 1 }));
  const survivors = shifted.filter((brick) => blockbreakerRowZ(brick.row) < BLOCKBREAKER_DANGER_Z);
  if (survivors.length < shifted.length) state.breaches += 1;
  const row = generateRow(state.randomState, state.rowsSpawned);
  state.randomState = row.randomState;
  state.bricks = [...survivors, ...row.bricks];
  state.rowsSpawned += 1;
}

function breakCluster(state: BlockbreakerState, hitBrick: BlockbreakerBrick, time: number) {
  const cluster = blockbreakerConnectedCluster(state.bricks, hitBrick);
  const ids = new Set(cluster.map((brick) => brick.id));
  const award = blockbreakerClusterScore(cluster.length, time);
  state.bricks = state.bricks.filter((brick) => !ids.has(brick.id));
  state.bricksBroken += cluster.length;
  state.score += award;
  state.lastBreakAt = time;
  state.lastBreakCount = cluster.length;
  state.lastScoreAward = award;
}

function beginReserve(state: BlockbreakerState, time: number) {
  state.status = "reserving";
  state.puckVelocity = [0, 0];
  state.reServeAt = time + BLOCKBREAKER_RESERVE_SECONDS;
}

function completeReserve(state: BlockbreakerState) {
  state.serveIndex += 1;
  state.puckPosition = servePosition(state.serveIndex);
  state.puckVelocity = serveVelocity(state.puckPosition, state.serveIndex);
  state.status = "live";
  state.reServeAt = null;
}

export function blockbreakerAimPreviewVisible(state: BlockbreakerState, playerPosition: WorldPosition) {
  if (state.status !== "live" || state.puckVelocity[1] <= 0) return false;
  const distance = Math.hypot(playerPosition[0] - state.puckPosition[0], playerPosition[1] - state.puckPosition[1]);
  return distance / Math.max(0.001, Math.hypot(state.puckVelocity[0], state.puckVelocity[1])) <= 1.5;
}

export function advanceBlockbreaker(source: BlockbreakerState, step: BlockbreakerStep): BlockbreakerState {
  if (source.status === "inactive" || source.status === "lost" || step.delta <= 0) return source;
  const state: BlockbreakerState = {
    ...source,
    // Bricks are immutable between row shifts and breaks; both operations replace
    // this array, so sharing it avoids cloning every brick on each 10 Hz tick.
    bricks: source.bricks,
    puckPosition: [...source.puckPosition],
    puckVelocity: [...source.puckVelocity],
    aimDirection: [...source.aimDirection],
  };

  let spawnedRows = 0;
  while (state.status !== "lost" && step.time + 0.0001 >= state.nextRowAt && spawnedRows < MAX_ROW_SPAWNS_PER_STEP) {
    const spawnedAt = state.nextRowAt;
    addBlockbreakerRow(state, spawnedAt);
    state.nextRowAt = spawnedAt + blockbreakerRowInterval(spawnedAt);
    spawnedRows += 1;
  }
  if (state.status === "lost") return state;
  if (state.status === "reserving") {
    if (state.reServeAt !== null && step.time >= state.reServeAt) completeReserve(state);
    else return state;
  }

  const speed = Math.hypot(state.puckVelocity[0], state.puckVelocity[1]);
  const substeps = Math.max(1, Math.min(MAX_SUBSTEPS, Math.ceil(speed * step.delta / MAX_SUBSTEP_DISTANCE)));
  const subDelta = step.delta / substeps;
  const interceptRadiusSquared = (PLAYER_INTERCEPT_RADIUS + BLOCKBREAKER_PUCK_RADIUS) ** 2;

  for (let substep = 0; substep < substeps && state.status === "live"; substep += 1) {
    const start: WorldPosition = [...state.puckPosition];
    const end: WorldPosition = [
      start[0] + state.puckVelocity[0] * subDelta,
      start[1] + state.puckVelocity[1] * subDelta,
    ];

    const minX = HOCKEY_ARENA_MIN_X + BLOCKBREAKER_PUCK_RADIUS;
    const maxX = HOCKEY_ARENA_MAX_X - BLOCKBREAKER_PUCK_RADIUS;
    if (end[0] < minX || end[0] > maxX) {
      end[0] = Math.max(minX, Math.min(maxX, end[0]));
      state.puckVelocity[0] *= -1;
    }

    let closest: { brick: BlockbreakerBrick; hit: SegmentHit } | null = null;
    for (const brick of state.bricks) {
      const hit = segmentExpandedBrickHit(start, end, brick);
      if (hit && (!closest || hit.time < closest.hit.time)) closest = { brick, hit };
    }
    if (closest) {
      state.puckPosition = [
        start[0] + (end[0] - start[0]) * closest.hit.time + closest.hit.normalX * 0.012,
        start[1] + (end[1] - start[1]) * closest.hit.time + closest.hit.normalZ * 0.012,
      ];
      if (closest.hit.normalX) state.puckVelocity[0] *= -1;
      if (closest.hit.normalZ) state.puckVelocity[1] *= -1;
      breakCluster(state, closest.brick, step.time);
      continue;
    }

    if (state.puckVelocity[1] > 0
      && segmentDistanceSquared(start[0], start[1], end[0], end[1], step.playerPosition) <= interceptRadiusSquared) {
      const direction = hockeyReturnDirection(state.aimDirection);
      state.puckPosition = [end[0], Math.min(end[1], step.playerPosition[1])];
      state.puckVelocity = [direction[0] * BLOCKBREAKER_PUCK_SPEED, direction[1] * BLOCKBREAKER_PUCK_SPEED];
      continue;
    }

    if (state.puckVelocity[1] < 0 && end[1] <= HOCKEY_NPC_GOAL_Z + BLOCKBREAKER_PUCK_RADIUS) {
      state.puckPosition = [end[0], HOCKEY_NPC_GOAL_Z + BLOCKBREAKER_PUCK_RADIUS];
      state.puckVelocity[1] = Math.abs(state.puckVelocity[1]);
      continue;
    }

    if (state.puckVelocity[1] > 0 && end[1] >= HOCKEY_HEALER_GOAL_Z) {
      state.puckPosition = [end[0], HOCKEY_HEALER_GOAL_Z];
      beginReserve(state, step.time);
      continue;
    }

    state.puckPosition = end;
  }

  return state;
}
