import type { BlueprintBlockSettings } from "../../app/lib/blueprint-contract.ts";
import type { TilePlacement } from "../../app/lib/types.ts";

// Supply width and wire reach are equal for every vanilla quality.
export const SUBSTATION_REACH = { normal: 18, uncommon: 20, rare: 22, epic: 24, legendary: 28 } as const;

export function substationWires(entities: Array<{ entity_number: number; name: string; position: { x: number; y: number }; quality?: keyof typeof SUBSTATION_REACH }>) {
  const poles = entities.filter((entity) => entity.name === "substation");
  const byPosition = new Map(poles.map((p) => [`${p.position.x},${p.position.y}`, p]));
  const wires: number[][] = [];
  for (const pole of poles) {
    const reach = SUBSTATION_REACH[pole.quality ?? "normal"];
    for (const [dx, dy] of [[reach, 0], [0, reach]]) {
      const other = byPosition.get(`${pole.position.x + dx},${pole.position.y + dy}`);
      // Factorio 2.x: pole_copper connector = 5. Export explicit copper wires,
      // including the connections across merged process-block boundaries.
      if (other) wires.push([pole.entity_number, 5, other.entity_number, 5]);
    }
  }
  return wires;
}

export interface BoundaryLines {
  left: number;
  right: number;
  top: number;
  bottom: number;
}

/** Every movable block has four explicit alignment lines. Annotations never
 * contribute a line or reserve physical space. */
export function boundaryLines(placements: TilePlacement[]): BoundaryLines {
  const physical = placements.filter((p) => p.kind !== "link");
  return {
    left: Math.min(...physical.map((p) => p.x)),
    right: Math.max(...physical.map((p) => p.x + p.width)),
    top: Math.min(...physical.map((p) => p.y)),
    bottom: Math.max(...physical.map((p) => p.y + p.height)),
  };
}

/** Fixed means fixed: exactly one Roboport on the selected side of each pole.
 * North is above the pole, south below it, in both blueprint orientations. */
export function fixedRoboports(poles: TilePlacement[], side: "north" | "south"): TilePlacement[] {
  return poles.map((pole) => ({
    kind: "entity", entityId: "roboport", tone: "power",
    x: pole.x - 1, y: pole.y + (side === "north" ? -4 : 2), width: 4, height: 4,
  }));
}

function alignedOffset(start: number, end: number, limitStart: number, limitEnd: number, alignment: "start" | "center" | "end") {
  if (alignment === "start") return limitStart - start;
  if (alignment === "end") return limitEnd - end;
  return (limitStart + limitEnd - start - end) / 2;
}

/** Subtract intervals where production would cross a fixed building's facing
 * edge. Equality is allowed, so the footprints meet with no empty tile. */
function horizontalClearance(production: BoundaryLines[], obstacles: BoundaryLines[], y: number, min: number, max: number) {
  const blocked: Array<[number, number]> = [];
  for (const obstacle of obstacles) for (const box of production) {
    if (box.top + y >= obstacle.bottom || box.bottom + y <= obstacle.top) continue;
    const start = Math.max(min, Math.floor(obstacle.left - box.right) + 1);
    const end = Math.min(max, Math.ceil(obstacle.right - box.left) - 1);
    if (start <= end) blocked.push([start, end]);
  }
  blocked.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
  const available: Array<[number, number]> = [];
  let cursor = min;
  for (const [start, end] of blocked) {
    if (cursor < start) available.push([cursor, start - 1]);
    cursor = Math.max(cursor, end + 1);
    if (cursor > max) break;
  }
  if (cursor <= max) available.push([cursor, max]);
  return available;
}

/** Keep the support grid at (0, 0), then translate the connected production
 * block by matching its boundary lines to the Substation's inward faces.
 * Fixed Roboport faces constrain the fit; they never become the center target. */
export function planPowerGrid(content: TilePlacement[], settings: BlueprintBlockSettings) {
  const reach = SUBSTATION_REACH[settings.substationQuality];
  const real = content.filter((p) => p.kind !== "link");
  const machines = real.filter((p) => p.kind === "machine");
  const machineLines = boundaryLines(machines.length ? machines : real);
  const footprint = boundaryLines(real);
  const production = real.map((p) => boundaryLines([p]));
  const horizontal = settings.horizontalAlignment ?? "center";
  const vertical = settings.verticalAlignment ?? "center";
  const minCols = Math.max(1, Math.ceil((footprint.right - footprint.left + 4) / reach));
  const minRows = Math.max(1, Math.ceil((footprint.bottom - footprint.top + 4) / reach));
  const better = (score: number[], previous: number[]) => {
    const i = score.findIndex((value, index) => value !== previous[index]);
    return i >= 0 && score[i] < previous[i];
  };
  type Frame = {
    grid: { x: number; y: number };
    translation: { x: number; y: number };
    placements: TilePlacement[];
    limits: BoundaryLines;
    score: number[];
  };
  let best: Frame | undefined;
  let fallback: Frame | undefined;
  for (const [cols, rows] of [[minCols, minRows], [minCols + 1, minRows], [minCols, minRows + 1], [minCols + 1, minRows + 1]]) {
    const grid = { x: cols * reach, y: rows * reach };
    // Bottom uses the top edge of the bottom poles; top uses the bottom edge
    // of the top poles. Left/right use the corresponding inward vertical edge.
    const limits = { left: 1, right: grid.x - 1, top: 1, bottom: grid.y - 1 };
    const targetX = alignedOffset(machineLines.left, machineLines.right, limits.left, limits.right,
      horizontal === "left" ? "start" : horizontal === "right" ? "end" : "center");
    const targetY = alignedOffset(machineLines.top, machineLines.bottom, limits.top, limits.bottom,
      vertical === "top" ? "start" : vertical === "bottom" ? "end" : "center");
    const closestCenter = (horizontal === "center" ? Math.abs(Math.round(targetX) - targetX) : 0)
      + (vertical === "center" ? Math.abs(Math.round(targetY) - targetY) : 0);
    if (best && best.score[0] === closestCenter && cols * rows > best.score[1]) continue;
    const poles: TilePlacement[] = [];
    const neighbouringPoles: TilePlacement[] = [];
    // Adjacent copies share the same poles and Roboports. Their fixed boxes
    // also constrain production that reaches beyond a boundary wire line.
    for (let row = -1; row <= rows + 1; row++) for (let col = -1; col <= cols + 1; col++) {
      const pole: TilePlacement = { kind: "entity", entityId: "substation", tone: "power", x: col * reach - 1, y: row * reach - 1, width: 2, height: 2 };
      neighbouringPoles.push(pole);
      if (row >= 0 && row <= rows && col >= 0 && col <= cols) poles.push(pole);
    }
    const ports = settings.roboportCoverage ? fixedRoboports(poles, settings.roboportSide) : [];
    const placements = [...poles, ...ports];
    const obstacles = [...neighbouringPoles, ...(settings.roboportCoverage ? fixedRoboports(neighbouringPoles, settings.roboportSide) : [])].map((p) => boundaryLines([p]));
    fallback ??= { grid, translation: { x: Math.round(targetX), y: Math.round(targetY) }, placements, limits, score: [] };
    const minX = Math.ceil(limits.left - machineLines.left), maxX = Math.floor(limits.right - machineLines.right);
    const minY = Math.ceil(limits.top - machineLines.top), maxY = Math.floor(limits.bottom - machineLines.bottom);
    const ys = Array.from({ length: Math.max(0, maxY - minY + 1) }, (_, index) => minY + index)
      .sort((a, b) => Math.abs(a - targetY) - Math.abs(b - targetY) || b - a);
    for (const y of ys) {
      const verticalError = Math.abs(y - targetY);
      if (best && vertical === "center" && verticalError > best.score[0]) continue;
      for (const [start, end] of horizontalClearance(production, obstacles, y, minX, maxX)) {
        const x = Math.max(start, Math.min(end, Math.round(targetX)));
        const horizontalError = Math.abs(x - targetX);
        // Preserve a centered axis before approaching an edge on the other
        // axis. A blocking Roboport stops that edge at its first facing line.
        const centerError = (horizontal === "center" ? horizontalError : 0) + (vertical === "center" ? verticalError : 0);
        const edgeError = (horizontal === "center" ? 0 : horizontalError) + (vertical === "center" ? 0 : verticalError);
        const score = [centerError, cols * rows, poles.length, edgeError];
        if (!best || better(score, best.score)) best = { grid, translation: { x, y }, placements, limits, score };
      }
    }
  }
  const frame = best ?? fallback!;
  const ports = frame.placements.filter((p) => p.entityId === "roboport");
  const roboportCoverageComplete = !settings.roboportCoverage || production.every((p) =>
    [p.left, p.right].every((x) => [p.top, p.bottom].every((y) => ports.some((r) =>
      Math.abs(x + frame.translation.x - r.x - 2) <= 25 && Math.abs(y + frame.translation.y - r.y - 2) <= 25))));
  return {
    placements: frame.placements,
    translation: frame.translation,
    grid: frame.grid,
    boundaries: { production: machineLines, footprint, substations: frame.limits },
    complete: Boolean(best),
    roboportCoverageComplete,
  };
}
