import type {
  BeaconLayoutResult,
  Machine,
  TilePlacement,
} from "./types.ts";

const BEACON_SIZE = 3;
const SUPPLY_DISTANCE = 3;
export const LOGISTICS_CORRIDOR = 2;
const MAX_EXACT_MACHINES = 60;
const MAX_PREVIEW_MACHINES = 36;

type Rect = { x: number; y: number; width: number; height: number };
type Candidate = Rect & { affects: number[] };

function intersects(a: Rect, b: Rect): boolean {
  return (
    a.x < b.x + b.width &&
    a.x + a.width > b.x &&
    a.y < b.y + b.height &&
    a.y + a.height > b.y
  );
}
function reaches(a: Rect, b: Rect): boolean {
  return (
    a.x < b.x + b.width &&
    a.x + a.width > b.x &&
    a.y < b.y + b.height &&
    a.y + a.height > b.y
  );
}
function expanded(rect: Rect, distance: number): Rect {
  return {
    x: rect.x - distance,
    y: rect.y - distance,
    width: rect.width + distance * 2,
    height: rect.height + distance * 2,
  };
}
function supplyRect(beacon: Rect): Rect {
  return expanded(beacon, SUPPLY_DISTANCE);
}
function machineRects(
  machine: Machine,
  count: number,
  horizontalGap = 0,
): Rect[] {
  const step = machine.width + horizontalGap;
  return Array.from({ length: count }, (_, index) => ({
    x: index * step,
    y: 0,
    width: machine.width,
    height: machine.height,
  }));
}
function placementBounds(placements: TilePlacement[]) {
  if (!placements.length) return { minX: 0, maxX: 1, minY: 0, maxY: 1 };
  return {
    minX: Math.min(...placements.map((p) => p.x)),
    maxX: Math.max(...placements.map((p) => p.x + p.width)),
    minY: Math.min(...placements.map((p) => p.y)),
    maxY: Math.max(...placements.map((p) => p.y + p.height)),
  };
}
function empty(machineCount: number, requested = 0): BeaconLayoutResult {
  const placements: TilePlacement[] = [];
  return {
    requestedPerMachine: requested,
    machineCount,
    counts: Array.from({ length: machineCount }, () => 0),
    average: 0,
    min: 0,
    max: 0,
    uniqueBeacons: 0,
    exact: true,
    extrapolated: false,
    corridor: LOGISTICS_CORRIDOR,
    placements,
    bounds: placementBounds(placements),
  };
}

function logisticsCorridorFor(machine: Machine): number {
  // Rocket silos are supplied internally while preparing a launch and are the
  // sole calculator exception that does not reserve an exterior belt corridor.
  return machine.id === "rocket-silo" ? 0 : LOGISTICS_CORRIDOR;
}

function candidatesFor(
  machine: Machine,
  machines: Rect[],
  corridor = logisticsCorridorFor(machine),
): Candidate[] {
  const first = machines[0];
  const last = machines.at(-1)!;
  const minX = first.x - BEACON_SIZE - SUPPLY_DISTANCE;
  const maxX = last.x + last.width + SUPPLY_DISTANCE;
  const minY = -BEACON_SIZE - SUPPLY_DISTANCE;
  const maxY = machine.height + SUPPLY_DISTANCE;
  const candidates: Candidate[] = [];
  for (let y = minY; y <= maxY; y += 1) {
    for (let x = minX; x <= maxX; x += 1) {
      const rect = { x, y, width: BEACON_SIZE, height: BEACON_SIZE };
      // Top and bottom remain clear for belts, pipes and inserters. Left and
      // right may be flush, which is how compact shared-beacon rows are built.
      if (
        machines.some(
          (target) =>
            intersects(rect, target) ||
            (corridor > 0 &&
              intersects(rect, {
                x: target.x,
                y: target.y - corridor,
                width: target.width,
                height: target.height + corridor * 2,
              })),
        )
      )
        continue;
      const supply = supplyRect(rect);
      const affects = machines.flatMap((target, index) =>
        reaches(supply, target) ? [index] : [],
      );
      if (affects.length) candidates.push({ ...rect, affects });
    }
  }
  return candidates;
}

/** Maximum compact ring for one machine. Top/bottom preserve the two-tile
 * logistics corridor; side beacons are flush as requested by the layout UI. */
export function singleMachineRing(machine: Machine): Candidate[] {
  if (machine.maxBeacons <= 0) return [];
  const candidates: Candidate[] = [];
  const add = (x: number, y: number) => {
    const next = { x, y, width: BEACON_SIZE, height: BEACON_SIZE, affects: [0] };
    if (!candidates.some((placed) => intersects(placed, next))) candidates.push(next);
  };
  if (machine.id === "rocket-silo") {
    // A 9x9 silo and its flush 3x3 beacons form a clean 5x5-cell perimeter:
    // five-beacon boundary columns are shared by neighbouring silos, while
    // three flush beacons complete the top and bottom. The resulting ring is
    // the physical 16-beacon maximum and has no detached outer positions.
    for (
      let y = -BEACON_SIZE;
      y <= machine.height;
      y += BEACON_SIZE
    ) {
      add(-BEACON_SIZE, y);
      add(machine.width, y);
    }
    for (let x = 0; x < machine.width; x += BEACON_SIZE) {
      add(x, -BEACON_SIZE);
      add(x, machine.height);
    }
    return candidates.slice(0, machine.maxBeacons);
  }
  for (let x = -5; x <= machine.width + 2; x += BEACON_SIZE) add(x, -5);
  for (let x = -5; x <= machine.width + 2; x += BEACON_SIZE) add(x, machine.height + LOGISTICS_CORRIDOR);
  for (let y = -2; y <= machine.height; y += BEACON_SIZE) add(-3, y);
  for (let y = -2; y <= machine.height; y += BEACON_SIZE) add(machine.width, y);
  return candidates.slice(0, machine.maxBeacons);
}

function exactCompactDesign(
  machine: Machine,
  count: number,
  requested: number,
  horizontalGap = 0,
): BeaconLayoutResult {
  if (count <= 0) return empty(0, requested);
  const corridor = logisticsCorridorFor(machine);
  const machines = machineRects(machine, count, horizontalGap);
  const machinePlacements: TilePlacement[] = machines.map(
    (rect, machineIndex) => ({
      kind: "machine",
      ...rect,
      machineIndex,
    }),
  );
  if (requested <= 0 || machine.maxBeacons <= 0) {
    return {
      ...empty(count, requested),
      corridor,
      placements: machinePlacements,
      bounds: placementBounds(machinePlacements),
    };
  }
  if (count === 1 && horizontalGap === 0) {
    const ring = singleMachineRing(machine).slice(0, requested);
    const beaconPlacements: TilePlacement[] = ring.map((placement) => ({ kind: "beacon", ...placement }));
    const placements = [...machinePlacements, ...beaconPlacements];
    return {
      requestedPerMachine: requested, machineCount: 1, counts: [ring.length], average: ring.length,
      min: ring.length, max: ring.length, uniqueBeacons: ring.length, exact: ring.length === requested,
      extrapolated: false, corridor, placements, bounds: placementBounds(placements),
    };
  }
  const candidates = candidatesFor(machine, machines, corridor);
  const selected: Candidate[] = [];
  const counts = Array.from({ length: count }, () => 0);
  const rowCenter = (machines[0].x + machines.at(-1)!.x + machine.width) / 2;

  while (true) {
    let best: Candidate | undefined;
    let bestScore = -Infinity;
    for (const candidate of candidates) {
      if (selected.some((placed) => intersects(placed, candidate))) continue;
      if (candidate.affects.some((index) => counts[index] >= requested))
        continue;
      const useful = candidate.affects.filter(
        (index) => counts[index] < requested,
      );
      if (!useful.length) continue;
      const fairness = useful.reduce(
        (sum, index) => sum + (requested - counts[index]) ** 2,
        0,
      );
      const shared = useful.length * 250;
      const edgeBalance = -Math.abs(candidate.x + 1.5 - rowCenter) * 0.001;
      const score = fairness * 1000 + shared + edgeBalance;
      if (score > bestScore) {
        best = candidate;
        bestScore = score;
      }
    }
    if (!best) break;
    selected.push(best);
    for (const index of best.affects) {
      counts[index] += 1;
    }
    if (counts.every((value) => value >= requested)) break;
  }

  const beaconPlacements: TilePlacement[] = selected.map((rect) => ({
    kind: "beacon",
    x: rect.x,
    y: rect.y,
    width: rect.width,
    height: rect.height,
    affects: rect.affects,
  }));
  const placements = [...machinePlacements, ...beaconPlacements];
  const average = counts.reduce((sum, value) => sum + value, 0) / count;
  return {
    requestedPerMachine: requested,
    machineCount: count,
    counts,
    average,
    min: Math.min(...counts),
    max: Math.max(...counts),
    uniqueBeacons: selected.length,
    exact: counts.every((value) => value === requested),
    extrapolated: false,
    corridor,
    placements,
    bounds: placementBounds(placements),
  };
}

const designCache = new Map<string, BeaconLayoutResult>();
function cachedDesign(key: string, create: () => BeaconLayoutResult) {
  const existing = designCache.get(key);
  if (existing) return existing;
  const result = create();
  designCache.set(key, result);
  if (designCache.size > 512) designCache.delete(designCache.keys().next().value!);
  return result;
}

export function calculateBeaconDesign(
  machine: Machine,
  machineCount: number,
  requestedPerMachine: number,
): BeaconLayoutResult {
  const count = Math.max(0, Math.ceil(machineCount));
  const requested = Math.max(
    0,
    Math.min(machine.maxBeacons, Math.floor(requestedPerMachine)),
  );
  return cachedDesign(`generic:${machine.id}:${count}:${requested}`, () => {
    if (count <= MAX_EXACT_MACHINES)
      return exactCompactDesign(machine, count, requested);
    const sample = exactCompactDesign(machine, MAX_EXACT_MACHINES, requested);
    const interior = sample.counts.slice(10, -10);
    const interiorAverage =
      interior.reduce((sum, value) => sum + value, 0) /
      Math.max(1, interior.length);
    const previewCount = Math.min(MAX_PREVIEW_MACHINES, count);
    const preview = exactCompactDesign(machine, previewCount, requested);
    return {
      ...preview,
      machineCount: count,
      counts: sample.counts,
      average: interiorAverage,
      min: sample.min,
      max: sample.max,
      uniqueBeacons: Math.ceil(
        (sample.uniqueBeacons / MAX_EXACT_MACHINES) * count,
      ),
      exact: sample.exact,
      extrapolated: true,
    };
  });
}

/** Blueprint layouts may reserve a horizontal service tile between adjacent
 * fluid machines. Re-run the same exact placement solver against that real
 * spacing instead of visually stretching a zero-gap Beacon solution. */
export function calculateBeaconDesignWithGap(
  machine: Machine,
  machineCount: number,
  requestedPerMachine: number,
  horizontalGap: number,
): BeaconLayoutResult {
  const count = Math.max(0, Math.ceil(machineCount));
  const requested = Math.max(
    0,
    Math.min(machine.maxBeacons, Math.floor(requestedPerMachine)),
  );
  const gap = Math.max(0, Math.floor(horizontalGap));
  return cachedDesign(`gap:${machine.id}:${count}:${requested}:${gap}`, () =>
    exactCompactDesign(machine, count, requested, gap),
  );
}

export interface RocketSiloLoadingLayout {
  mode: "robots" | "inserters" | "ignore";
  inserterId?: "inserter" | "fast-inserter" | "bulk-inserter" | "stack-inserter";
}

/** Rocket silos form one horizontal shared-beacon block. A five-beacon
 * boundary column sits flush between adjacent 9x9 silos, so the block ends
 * cleanly without detached corner beacons. Physical loading reserves each
 * silo's bottom-centre beacon position. */
export function calculateRocketSiloBeaconDesign(
  machine: Machine,
  machineCount: number,
  requestedPerMachine: number,
  loading: RocketSiloLoadingLayout = { mode: "ignore" },
): BeaconLayoutResult {
  const count = Math.max(0, Math.ceil(machineCount));
  const usesPhysicalLoading = loading.mode !== "ignore";
  const inserterId = loading.inserterId ?? "stack-inserter";
  const maximum = Math.max(
    0,
    machine.maxBeacons - (usesPhysicalLoading ? 1 : 0),
  );
  const requested = Math.max(0, Math.min(maximum, Math.floor(requestedPerMachine)));
  return cachedDesign(
    `rocket:${machine.id}:${count}:${requested}:${loading.mode}:${usesPhysicalLoading ? inserterId : 0}`,
    () => {
      if (!count) return empty(0, requested);
      const solveCount = Math.min(count, MAX_EXACT_MACHINES);
      const previewCount = Math.min(solveCount, MAX_PREVIEW_MACHINES);
      const machines = machineRects(machine, solveCount, BEACON_SIZE);
      const reserved = new Set(
        usesPhysicalLoading
          ? machines.map(
              (target) =>
                `${target.x + Math.floor((machine.width - BEACON_SIZE) / 2)}:${machine.height}`,
            )
          : [],
      );
      const candidatesByPosition = new Map<
        string,
        Rect & { affects: Set<number> }
      >();
      const baseRing = singleMachineRing(machine);
      machines.forEach((target, machineIndex) => {
        for (const beacon of baseRing) {
          const x = target.x + beacon.x;
          const y = beacon.y;
          const key = `${x}:${y}`;
          if (reserved.has(key)) continue;
          const candidate = candidatesByPosition.get(key) ?? {
            x,
            y,
            width: BEACON_SIZE,
            height: BEACON_SIZE,
            affects: new Set<number>(),
          };
          candidate.affects.add(machineIndex);
          candidatesByPosition.set(key, candidate);
        }
      });
      const candidates: Candidate[] = [...candidatesByPosition.values()].map(
        (entry) => ({ ...entry, affects: [...entry.affects] }),
      );
      const selected: Candidate[] = [];
      const counts = Array.from({ length: solveCount }, () => 0);
      while (counts.some((value) => value < requested)) {
        let best: Candidate | undefined;
        let bestScore = -Infinity;
        for (const candidate of candidates) {
          if (selected.includes(candidate)) continue;
          if (candidate.affects.some((index) => counts[index] >= requested))
            continue;
          const useful = candidate.affects.filter(
            (index) => counts[index] < requested,
          );
          if (!useful.length) continue;
          const score =
            useful.reduce(
              (sum, index) => sum + (requested - counts[index]) ** 2,
              0,
            ) *
              1000 +
            useful.length * 500 -
            Math.abs(candidate.y - machine.height / 2) * 0.01;
          if (score > bestScore) {
            best = candidate;
            bestScore = score;
          }
        }
        if (!best) break;
        selected.push(best);
        for (const index of best.affects) counts[index] += 1;
      }
      const placements: TilePlacement[] = machines
        .slice(0, previewCount)
        .map((target, machineIndex) => ({
          kind: "machine" as const,
          ...target,
          machineIndex,
        }));
      placements.push(
        ...selected
          .filter((candidate) =>
            candidate.affects.some((index) => index < previewCount),
          )
          .map((candidate) => ({
            kind: "beacon" as const,
            x: candidate.x,
            y: candidate.y,
            width: candidate.width,
            height: candidate.height,
            affects: candidate.affects.filter((index) => index < previewCount),
          })),
      );
      if (usesPhysicalLoading) {
        machines.slice(0, previewCount).forEach((target) => {
          const centreX = target.x + Math.floor(machine.width / 2);
          placements.push({
            kind: "entity",
            entityId: inserterId,
            x: centreX,
            y: machine.height,
            width: 1,
            height: 1,
            direction: "north",
            tone: "fluid",
            label: "Payload inserter",
          });
          placements.push({
            kind: "entity",
            entityId: "buffer-chest",
            x: centreX,
            y: machine.height + 1,
            width: 1,
            height: 1,
            tone: "storage",
            label: "Full buffer chest",
          });
          if (loading.mode === "robots") {
            placements.push({
              kind: "entity",
              entityId: "roboport",
              x: target.x + (machine.width - 4) / 2,
              y: machine.height + 4,
              width: 4,
              height: 4,
              tone: "power",
              label: "Local roboport",
            });
          }
        });
      }
      const average =
        counts.reduce((sum, value) => sum + value, 0) /
        Math.max(1, counts.length);
      const uniqueBeacons =
        count <= solveCount
          ? selected.length
          : Math.ceil((selected.length / solveCount) * count);
      return {
        requestedPerMachine: requested,
        machineCount: count,
        counts,
        average,
        min: Math.min(...counts),
        max: Math.max(...counts),
        uniqueBeacons,
        exact: counts.every((value) => value === requested),
        extrapolated: count > previewCount,
        corridor: 0,
        placements,
        bounds: placementBounds(placements),
      };
    },
  );
}

/** Biolabs form one uninterrupted horizontal sequence. Each 5x5 lab keeps two
 * empty tiles on every side; one vertical five-beacon column is shared by two
 * neighbouring labs. The remaining top/bottom beacons complete the physical
 * 16-beacon maximum for every lab. */
export function calculateBiolabBeaconDesign(
  machine: Machine,
  machineCount: number,
  requestedOrEnabled: number | boolean = true,
): BeaconLayoutResult {
  const count = Math.max(0, Math.ceil(machineCount));
  const requested =
    typeof requestedOrEnabled === "boolean"
      ? requestedOrEnabled
        ? machine.maxBeacons
        : 0
      : Math.max(0, Math.min(machine.maxBeacons, Math.floor(requestedOrEnabled)));
  if (!count || !requested) return calculateBeaconDesign(machine, count, 0);
  return cachedDesign(`biolab:${machine.id}:${count}:${requested}`, () => {
  const solveCount = Math.min(MAX_EXACT_MACHINES, count);
  const previewCount = Math.min(MAX_PREVIEW_MACHINES, solveCount);
  const step = machine.width + LOGISTICS_CORRIDOR * 2 + BEACON_SIZE;
  const machines = machineRects(machine, solveCount, step - machine.width);
  const beaconCoordinates = new Map<string, Rect>();
  const addBeacon = (x: number, y: number) =>
    beaconCoordinates.set(`${x}:${y}`, {
      x,
      y,
      width: BEACON_SIZE,
      height: BEACON_SIZE,
    });
  const top = -LOGISTICS_CORRIDOR - BEACON_SIZE;
  const bottom = machine.height + LOGISTICS_CORRIDOR;
  for (const target of machines) {
    const left = target.x - LOGISTICS_CORRIDOR - BEACON_SIZE;
    const right = target.x + target.width + LOGISTICS_CORRIDOR;
    // Five-beacon boundary columns; neighbouring labs reuse the same column.
    for (let y = top; y <= bottom; y += BEACON_SIZE) {
      addBeacon(left, y);
      addBeacon(right, y);
    }
    // Three additional positions on both horizontal sides complete the ring.
    for (let x = left + BEACON_SIZE; x < right; x += BEACON_SIZE) {
      addBeacon(x, top);
      addBeacon(x, bottom);
    }
  }
  const candidates = Array.from(beaconCoordinates.values()).map((rect) => ({
    ...rect,
    affects: machines.flatMap((target, index) =>
      reaches(supplyRect(rect), target) ? [index] : [],
    ),
  })).filter((candidate) => candidate.affects.length > 0);
  const selected: Candidate[] = [];
  const counts = Array.from({ length: solveCount }, () => 0);
  while (counts.some((value) => value < requested)) {
    let best: Candidate | undefined;
    let bestScore = -Infinity;
    for (const candidate of candidates) {
      if (selected.includes(candidate)) continue;
      if (candidate.affects.some((index) => counts[index] >= requested)) continue;
      const score = candidate.affects.reduce(
        (sum, index) => sum + (requested - counts[index]) ** 2,
        0,
      ) * 1000 + candidate.affects.length * 250;
      if (score > bestScore) {
        best = candidate;
        bestScore = score;
      }
    }
    if (!best) break;
    selected.push(best);
    for (const index of best.affects) counts[index] += 1;
  }
  const shownMachines = machines.slice(0, previewCount);
  const machinePlacements: TilePlacement[] = shownMachines.map((rect, machineIndex) => ({
    kind: "machine",
    ...rect,
    machineIndex,
  }));
  const beaconPlacements: TilePlacement[] = selected
    .filter((rect) => rect.affects.some((index) => index < previewCount))
    .map((rect) => ({
      kind: "beacon",
      x: rect.x,
      y: rect.y,
      width: rect.width,
      height: rect.height,
      affects: rect.affects.filter((index) => index < previewCount),
    }));
  const placements = [...machinePlacements, ...beaconPlacements];
  const average = counts.reduce((sum, value) => sum + value, 0) / solveCount;
  return {
    requestedPerMachine: requested,
    machineCount: count,
    counts,
    average,
    min: Math.min(...counts),
    max: Math.max(...counts),
    uniqueBeacons:
      count <= solveCount
        ? selected.length
        : Math.ceil((selected.length / solveCount) * count),
    exact: counts.every((value) => value === requested),
    extrapolated: count > previewCount,
    corridor: LOGISTICS_CORRIDOR,
    placements,
    bounds: placementBounds(placements),
  };
  });
}

/** Used for Biolabs and explicit isolated-machine quality layouts. */
export function calculateIsolatedBeaconDesign(
  machine: Machine,
  machineCount: number,
  requestedPerMachine = machine.maxBeacons,
): BeaconLayoutResult {
  const count = Math.max(0, Math.ceil(machineCount));
  const requested = Math.max(
    0,
    Math.min(machine.maxBeacons, Math.floor(requestedPerMachine)),
  );
  if (!count || !requested) return calculateBeaconDesign(machine, count, 0);
  const base = exactCompactDesign(machine, 1, requested);
  const baseRing = base.placements.filter(
    (placement) => placement.kind === "beacon",
  );
  const previewCount = Math.min(count, MAX_PREVIEW_MACHINES);
  const step = machine.width + 14;
  const placements: TilePlacement[] = [];
  for (let index = 0; index < previewCount; index += 1) {
    const shift = index * step;
    placements.push({
      kind: "machine",
      x: shift,
      y: 0,
      width: machine.width,
      height: machine.height,
      machineIndex: index,
    });
    for (const beacon of baseRing) {
      placements.push({
        kind: "beacon",
        x: beacon.x + shift,
        y: beacon.y,
        width: BEACON_SIZE,
        height: BEACON_SIZE,
        affects: [index],
      });
    }
  }
  return {
    requestedPerMachine: requested,
    machineCount: count,
    counts: Array.from({ length: count }, () => baseRing.length),
    average: baseRing.length,
    min: baseRing.length,
    max: baseRing.length,
    uniqueBeacons: baseRing.length * count,
    exact: baseRing.length === requested,
    extrapolated: count > previewCount,
    corridor: LOGISTICS_CORRIDOR,
    placements,
    bounds: placementBounds(placements),
  };
}

export const calculateBeaconRow = calculateBeaconDesign;
export function beaconCountSum(layout: BeaconLayoutResult): number {
  return layout.average * layout.machineCount;
}
