/**
 * 절단 배치(Bin Packing) 알고리즘
 * - 면적형 2D: 2-stage Guillotine (FFDH 변형) — 직선 절단 보장 (유리/판재 절단)
 * - 면적형 동일품목 우선 배치 — 각 품목별로 독립된 원판 (다른 품목과 안 섞임)
 * - 길이형 1D: FFD (packLength)
 */

export type CutType = "area" | "length";
export type PackMode = "mixed" | "homo";
export type Dir = "무관" | "가로" | "세로";

export interface Material {
  code: string;
  name: string;
  id?: string;
  width?: number;
  height?: number;
  length?: number;
  stock?: number;
  unit?: string;
}

export interface PlanItem {
  name: string;          // 품목명 (item_info.item_name)
  code?: string;         // 품목코드 (item_info.item_number)
  item_id?: string;      // item_info.id (cutting_plan_item.item_id 저장용)
  width: number;
  height: number;
  length?: number;
  qty: number;
  dir: Dir;
  color: string;
  placed?: number;
}

export interface Placement {
  x: number;
  y: number;
  w: number;
  h: number;
  color: string;
  name: string;
  itemIdx: number;
  rotated: boolean;
}

export interface Shelf {
  y: number;
  h: number;
  currentX: number;
}

export interface RemnantItem {
  id: string;
  x: number;
  y: number;
  w: number;
  h: number;
  status: "keep" | "discard";
}

export interface Sheet {
  id: number;
  matW: number;
  matH: number;
  matCode: string;
  matName: string;
  shelves: Shelf[];
  placements: Placement[];
  // 사용자가 편집한 자투리. undefined이면 placements로부터 자동 추출 (extractInitialRemnants).
  remnants?: RemnantItem[];
}

export interface Segment {
  len: number;
  color: string;
  name: string;
  itemIdx: number;
  startX: number;
}

export interface Pipe {
  id: number;
  matLen: number;
  matCode: string;
  matName: string;
  remaining: number;
  segments: Segment[];
}

export interface SheetGroup {
  count: number;
  repIdx: number;
  representative: Sheet;
  indices: number[];
}

export interface PipeGroup {
  count: number;
  repIdx: number;
  representative: Pipe;
  indices: number[];
}

export interface AreaResult {
  sheets: Sheet[];
  sheetGroups?: SheetGroup[];
}

export interface LengthResult {
  pipes: Pipe[];
  pipeGroups?: PipeGroup[];
}

export const COLORS = [
  "#3b82f6", "#10b981", "#f59e0b", "#8b5cf6", "#ef4444",
  "#06b6d4", "#ec4899", "#84cc16", "#f97316", "#0ea5e9",
];

// ─────────────────────────────────────────────────────────
// 면적형 2D 배치 — 2-stage Guillotine (FFDH 기반)
// ─────────────────────────────────────────────────────────
// 모든 절단선이 원판 가장자리에서 가장자리까지 직선으로 이어지도록 보장.
// 1단계: 원판을 가로 shelf로 분할 (shelf 간 = 가로 직선 절단)
// 2단계: 각 shelf 안에서 piece들을 좌→우 배치 (shelf 내 = 세로 직선 절단)

type PieceInput = {
  w: number; h: number; canRot: boolean;
  color: string; name: string; itemIdx: number;
};

type FitResult = { w: number; h: number; rotated: boolean };

// shelf 시작 piece용 — 둘 다 fit이면 height 큰 쪽 (shelf height 최대화)
function fitForShelfStart(p: PieceInput, maxH: number, maxW: number): FitResult | null {
  const a = p.w <= maxW && p.h <= maxH ? { w: p.w, h: p.h, rotated: false } : null;
  const b = p.canRot && p.h <= maxW && p.w <= maxH ? { w: p.h, h: p.w, rotated: true } : null;
  if (a && b) return a.h >= b.h ? a : b;
  return a || b;
}

// shelf 내부 piece용 — 둘 다 fit이면 width 작은 쪽 (남은 width 보존)
function fitInShelf(p: PieceInput, shelfH: number, availW: number): FitResult | null {
  const a = p.w <= availW && p.h <= shelfH ? { w: p.w, h: p.h, rotated: false } : null;
  const b = p.canRot && p.h <= availW && p.w <= shelfH ? { w: p.h, h: p.w, rotated: true } : null;
  if (a && b) return a.w <= b.w ? a : b;
  return a || b;
}

function packGuillotineSingle(
  primary: Material, pieces: PieceInput[], kerf: number,
  sortFn: (a: PieceInput, b: PieceInput) => number, startId: number
): Sheet[] {
  const matW = primary.width || 0;
  const matH = primary.height || 0;
  if (!matW || !matH || !pieces.length) return [];

  const remaining = [...pieces].sort(sortFn);
  const sheets: Sheet[] = [];
  let sheetId = startId;

  while (remaining.length > 0) {
    const sheet: Sheet = {
      id: sheetId++, matW, matH,
      matCode: primary.code, matName: primary.name,
      shelves: [], placements: [],
    };
    let currentY = 0;
    let placedAny = false;

    while (currentY < matH) {
      const remH = matH - currentY;
      let firstIdx = -1;
      let firstFit: FitResult | null = null;
      for (let i = 0; i < remaining.length; i++) {
        const fit = fitForShelfStart(remaining[i], remH, matW);
        if (fit) { firstIdx = i; firstFit = fit; break; }
      }
      if (firstIdx === -1 || !firstFit) break;

      const first = remaining[firstIdx];
      const shelfH = firstFit.h;
      sheet.placements.push({
        x: 0, y: currentY, w: firstFit.w, h: firstFit.h,
        color: first.color, name: first.name, itemIdx: first.itemIdx,
        rotated: firstFit.rotated,
      });
      let currentX = firstFit.w + kerf;
      remaining.splice(firstIdx, 1);
      placedAny = true;

      let i = 0;
      while (i < remaining.length) {
        const availW = matW - currentX;
        if (availW <= 0) break;
        const fit = fitInShelf(remaining[i], shelfH, availW);
        if (!fit) { i++; continue; }
        const p = remaining[i];
        sheet.placements.push({
          x: currentX, y: currentY, w: fit.w, h: fit.h,
          color: p.color, name: p.name, itemIdx: p.itemIdx,
          rotated: fit.rotated,
        });
        currentX += fit.w + kerf;
        remaining.splice(i, 1);
      }

      currentY += shelfH + kerf;
    }

    if (!placedAny) break;
    sheets.push(sheet);
  }

  return sheets;
}

// 4가지 정렬 전략 비교 후 best 선택 (원판 수 최소 → 이용률 최대)
function runGuillotine(primary: Material, pieces: PieceInput[], kerf: number, startId: number = 1): Sheet[] {
  const matW = primary.width || 0;
  const matH = primary.height || 0;
  if (!matW || !matH || !pieces.length) return [];
  const totalArea = matW * matH;

  const sorts = [
    (a: PieceInput, b: PieceInput) => Math.max(b.w, b.h) - Math.max(a.w, a.h), // 긴 변 큰 순
    (a: PieceInput, b: PieceInput) => b.h - a.h || b.w - a.w,                   // 높이 큰 순
    (a: PieceInput, b: PieceInput) => b.w - a.w || b.h - a.h,                   // 폭 큰 순
    (a: PieceInput, b: PieceInput) => b.w * b.h - a.w * a.h,                    // 면적 큰 순
  ];

  let best: Sheet[] = [];
  let bestScore: [number, number] = [Infinity, -Infinity];

  for (const sortFn of sorts) {
    const sheets = packGuillotineSingle(primary, pieces, kerf, sortFn, startId);
    if (!sheets.length) continue;
    const count = sheets.length;
    const usedArea = sheets.reduce((s, sh) => s + sh.placements.reduce((ss, p) => ss + p.w * p.h, 0), 0);
    const util = usedArea / (count * totalArea);
    const score: [number, number] = [count, -util];
    if (
      score[0] < bestScore[0] ||
      (score[0] === bestScore[0] && score[1] < bestScore[1])
    ) {
      best = sheets;
      bestScore = score;
    }
  }
  return best;
}

function expandPieces(items: PlanItem[]): PieceInput[] {
  const out: PieceInput[] = [];
  items.forEach((item, itemIdx) => {
    const qty = parseInt(String(item.qty)) || 0;
    for (let i = 0; i < qty; i++) {
      out.push({
        w: item.width, h: item.height,
        canRot: item.dir === "무관",
        color: item.color, name: item.name, itemIdx,
      });
    }
  });
  return out;
}

// ─────────────────────────────────────────────────────────
// packArea — 혼합 최적 (모든 품목 합쳐서 guillotine packing)
// ─────────────────────────────────────────────────────────
export function packArea(mats: Material[], items: PlanItem[], kerf: number): AreaResult {
  if (!mats.length || !items.length) return { sheets: [] };
  const primary = mats[0];
  const pieces = expandPieces(items);
  return { sheets: runGuillotine(primary, pieces, kerf, 1) };
}

// ─────────────────────────────────────────────────────────
// packAreaHomogeneous — 동일 품목 우선 + 잔여는 혼합 최적
// ─────────────────────────────────────────────────────────
// 1) 각 품목별로 한 판 가득 채울 수 있는 만큼 단독 판 생성 (floor(qty / maxCap))
// 2) 가득 못 채우는 잔여 수량은 모든 품목 모아서 혼합 packing (효율 추구)
export function packAreaHomogeneous(mats: Material[], items: PlanItem[], kerf: number): AreaResult {
  if (!mats.length || !items.length) return { sheets: [] };
  const primary = mats[0];
  const matW = primary.width || 0;
  const matH = primary.height || 0;
  if (!matW || !matH) return { sheets: [] };

  const allSheets: Sheet[] = [];
  let sheetId = 1;
  const remQty = items.map((i) => parseInt(String(i.qty)) || 0);

  // 1) 각 품목 가득 찬 단독 판 생성
  items.forEach((item, idx) => {
    const qty = remQty[idx];
    if (!qty || !item.width || !item.height) return;

    // 실제 maxCap 측정 — runGuillotine 8전략으로 한 판 최대 수용량
    const itemArea = item.width * item.height;
    const upper = Math.max(1, Math.ceil((matW * matH) / itemArea) + 5);
    const testPieces: PieceInput[] = [];
    const trialN = Math.min(qty, upper);
    for (let i = 0; i < trialN; i++) {
      testPieces.push({
        w: item.width, h: item.height,
        canRot: item.dir === "무관",
        color: item.color, name: item.name, itemIdx: idx,
      });
    }
    const trialSheets = runGuillotine(primary, testPieces, kerf, 0);
    const maxCap = trialSheets[0]?.placements.length || 0;
    if (maxCap === 0) return;

    const fullSheets = Math.floor(qty / maxCap);
    if (fullSheets === 0) return; // 한 판도 못 채움 → 전부 잔여로

    const usedQty = fullSheets * maxCap;
    const fullPieces: PieceInput[] = [];
    for (let i = 0; i < usedQty; i++) {
      fullPieces.push({
        w: item.width, h: item.height,
        canRot: item.dir === "무관",
        color: item.color, name: item.name, itemIdx: idx,
      });
    }
    const itemSheets = runGuillotine(primary, fullPieces, kerf, sheetId);
    sheetId += itemSheets.length;
    allSheets.push(...itemSheets);
    remQty[idx] = qty - usedQty;
  });

  // 2) 잔여 수량을 모아서 혼합 packing (효율 추구)
  const remPieces: PieceInput[] = [];
  items.forEach((item, idx) => {
    const q = remQty[idx];
    if (!q || !item.width || !item.height) return;
    for (let i = 0; i < q; i++) {
      remPieces.push({
        w: item.width, h: item.height,
        canRot: item.dir === "무관",
        color: item.color, name: item.name, itemIdx: idx,
      });
    }
  });
  if (remPieces.length) {
    const mixSheets = runGuillotine(primary, remPieces, kerf, sheetId);
    allSheets.push(...mixSheets);
  }

  return { sheets: allSheets };
}

// ─────────────────────────────────────────────────────────
// 자투리 형태 추출 (직사각형 단위)
// ─────────────────────────────────────────────────────────
// 원판에서 빈 공간을 직사각형 자투리들로 분할.
// guillotine 배치 결과를 가정하고 shelf 우측 / shelf 내부 piece 위 / 마지막 shelf 아래 자투리 추출.

export interface Remnant {
  x: number;
  y: number;
  w: number;
  h: number;
}

export function computeRemnants(sheet: Sheet, kerf: number = 0): Remnant[] {
  const matW = sheet.matW;
  const matH = sheet.matH;
  if (!sheet.placements.length) {
    return [{ x: 0, y: 0, w: matW, h: matH }];
  }

  // piece 점유 영역을 kerf 만큼 확장 (칼날 폭은 자투리 아님)
  // 가장자리(0, matW, matH)는 자르지 않으므로 그쪽으로는 확장하지 않음
  const expanded = sheet.placements.map((p) => ({
    x: Math.max(0, p.x - kerf),
    y: Math.max(0, p.y - kerf),
    x2: Math.min(matW, p.x + p.w + kerf),
    y2: Math.min(matH, p.y + p.h + kerf),
  }));

  // 1) 모든 piece의 y 경계로 horizontal strip 분할 (확장된 좌표)
  const ySet = new Set<number>([0, matH]);
  for (const p of expanded) {
    if (p.y > 0 && p.y < matH) ySet.add(p.y);
    if (p.y2 > 0 && p.y2 < matH) ySet.add(p.y2);
  }
  const ys = [...ySet].sort((a, b) => a - b);

  // 2) 각 strip 안에서 piece가 차지하지 않는 x 구간 추출
  const stripRects: Remnant[] = [];
  for (let i = 0; i < ys.length - 1; i++) {
    const y1 = ys[i];
    const y2 = ys[i + 1];
    const sh = y2 - y1;
    if (sh < 1) continue;

    // 이 strip에 걸치는 piece들의 x 범위 (확장된 좌표)
    const occupied: { x1: number; x2: number }[] = [];
    for (const p of expanded) {
      if (p.y < y2 && p.y2 > y1) {
        occupied.push({ x1: p.x, x2: p.x2 });
      }
    }
    occupied.sort((a, b) => a.x1 - b.x1);

    // 빈 x 구간 찾기 (occupied 사이의 gap + 좌우 끝)
    let cursor = 0;
    for (const o of occupied) {
      if (o.x1 > cursor + 0.5) {
        stripRects.push({ x: cursor, y: y1, w: o.x1 - cursor, h: sh });
      }
      cursor = Math.max(cursor, o.x2);
    }
    if (cursor < matW - 0.5) {
      stripRects.push({ x: cursor, y: y1, w: matW - cursor, h: sh });
    }
  }

  // 3) 세로 인접 + 같은 x 범위 자투리 합치기 (반복 적용)
  const verticallyMerged = mergeAdjacent(stripRects, "vertical");
  // 4) 가로 인접 + 같은 y 범위 자투리 합치기 (위에서 합쳐진 결과 위에)
  const finalRects = mergeAdjacent(verticallyMerged, "horizontal");

  // 너무 좁은 조각 제거 (kerf 이하는 절단 시 사라지는 영역, 미세 오차)
  const minDim = Math.max(1, kerf);
  return finalRects.filter((r) => r.w > minDim && r.h > minDim);
}

// 인접 사각형 합치기 — direction별로 같은 축 정렬되고 인접하면 병합
function mergeAdjacent(rects: Remnant[], direction: "vertical" | "horizontal"): Remnant[] {
  const out: Remnant[] = rects.map((r) => ({ ...r }));
  let changed = true;
  while (changed) {
    changed = false;
    for (let i = 0; i < out.length && !changed; i++) {
      for (let j = i + 1; j < out.length; j++) {
        const a = out[i];
        const b = out[j];
        if (direction === "vertical") {
          // 같은 x 범위 + 세로 인접 (a 아래에 b 또는 b 아래에 a)
          if (Math.abs(a.x - b.x) < 0.5 && Math.abs(a.w - b.w) < 0.5) {
            if (Math.abs(a.y + a.h - b.y) < 0.5) {
              out[i] = { x: a.x, y: a.y, w: a.w, h: a.h + b.h };
              out.splice(j, 1);
              changed = true;
              break;
            }
            if (Math.abs(b.y + b.h - a.y) < 0.5) {
              out[i] = { x: a.x, y: b.y, w: a.w, h: a.h + b.h };
              out.splice(j, 1);
              changed = true;
              break;
            }
          }
        } else {
          // 같은 y 범위 + 가로 인접
          if (Math.abs(a.y - b.y) < 0.5 && Math.abs(a.h - b.h) < 0.5) {
            if (Math.abs(a.x + a.w - b.x) < 0.5) {
              out[i] = { x: a.x, y: a.y, w: a.w + b.w, h: a.h };
              out.splice(j, 1);
              changed = true;
              break;
            }
            if (Math.abs(b.x + b.w - a.x) < 0.5) {
              out[i] = { x: b.x, y: a.y, w: a.w + b.w, h: a.h };
              out.splice(j, 1);
              changed = true;
              break;
            }
          }
        }
      }
    }
  }
  return out;
}

// computeRemnants 결과에 ID + 기본 status를 부여해서 RemnantItem[]로 변환.
// 사용자가 자투리를 편집하지 않은 sheet에서 초기값 생성용.
export function extractInitialRemnants(sheet: Sheet, idPrefix: string = "", kerf: number = 0): RemnantItem[] {
  return computeRemnants(sheet, kerf).map((rm, i) => ({
    id: `${idPrefix}r${i}`,
    x: rm.x,
    y: rm.y,
    w: rm.w,
    h: rm.h,
    status: "discard",
  }));
}

// ─────────────────────────────────────────────────────────
// 자투리 그룹화 — 변 공유하는 자투리들을 한 덩어리로 묶음 (Union-Find)
// ─────────────────────────────────────────────────────────

export interface RemnantGroup {
  groupId: number;
  rects: RemnantItem[];
}

function sharesEdgeBox(
  a: { x: number; y: number; w: number; h: number },
  b: { x: number; y: number; w: number; h: number }
): boolean {
  const eps = 0.5;
  // 가로 인접 (수직 변 공유)
  const horizAdj = Math.abs(a.x + a.w - b.x) < eps || Math.abs(b.x + b.w - a.x) < eps;
  const yOverlap = Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y);
  if (horizAdj && yOverlap > eps) return true;
  // 세로 인접 (수평 변 공유)
  const vertAdj = Math.abs(a.y + a.h - b.y) < eps || Math.abs(b.y + b.h - a.y) < eps;
  const xOverlap = Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x);
  if (vertAdj && xOverlap > eps) return true;
  return false;
}

export function computeRemnantGroups(rects: RemnantItem[]): RemnantGroup[] {
  const n = rects.length;
  if (n === 0) return [];
  const parent = Array.from({ length: n }, (_, i) => i);
  const find = (i: number): number => {
    while (parent[i] !== i) {
      parent[i] = parent[parent[i]];
      i = parent[i];
    }
    return i;
  };
  const union = (i: number, j: number) => {
    const ri = find(i), rj = find(j);
    if (ri !== rj) parent[ri] = rj;
  };
  for (let i = 0; i < n; i++) {
    for (let j = i + 1; j < n; j++) {
      if (sharesEdgeBox(rects[i], rects[j])) union(i, j);
    }
  }
  const groupMap = new Map<number, RemnantItem[]>();
  for (let i = 0; i < n; i++) {
    const root = find(i);
    const arr = groupMap.get(root) || [];
    arr.push(rects[i]);
    groupMap.set(root, arr);
  }
  return [...groupMap.values()].map((arr, gi) => ({ groupId: gi, rects: arr }));
}

// ─────────────────────────────────────────────────────────
// 그룹 외곽선 (polygon) 계산 — 사각형들의 union outline
// ─────────────────────────────────────────────────────────
// 알고리즘: 모든 사각형의 4변을 방향성(시계방향) segment로 추출 →
//   격자점 기준 unit segment로 분해 → 같은 위치 반대 방향 segment 쌍 제거 →
//   남은 segment를 head-tail 매칭으로 polygon path 구성 (구멍 포함 가능).

export interface OutlinePath {
  points: { x: number; y: number }[];
}

export function computeGroupOutline(rects: { x: number; y: number; w: number; h: number }[]): OutlinePath[] {
  if (rects.length === 0) return [];

  // 1) x, y 격자 좌표 모음
  const xSet = new Set<number>();
  const ySet = new Set<number>();
  rects.forEach((r) => {
    xSet.add(r.x); xSet.add(r.x + r.w);
    ySet.add(r.y); ySet.add(r.y + r.h);
  });
  const xs = [...xSet].sort((a, b) => a - b);
  const ys = [...ySet].sort((a, b) => a - b);
  const xi = (x: number) => xs.indexOf(x);
  const yi = (y: number) => ys.indexOf(y);

  // 2) 격자 셀 occupancy (셀 [i, j] 가 채워져 있는지)
  const occ: boolean[][] = [];
  for (let j = 0; j < ys.length - 1; j++) {
    const row: boolean[] = [];
    const cy = (ys[j] + ys[j + 1]) / 2;
    for (let i = 0; i < xs.length - 1; i++) {
      const cx = (xs[i] + xs[i + 1]) / 2;
      const o = rects.some((r) => cx >= r.x && cx <= r.x + r.w && cy >= r.y && cy <= r.y + r.h);
      row.push(o);
    }
    occ.push(row);
  }

  // 3) 각 셀의 4변 중 외부 경계인 것만 directed edge로 등록 (시계방향)
  // 격자점은 (xs[i], ys[j]). 셀 [i, j]는 (xs[i], ys[j])-(xs[i+1], ys[j+1]).
  // 셀이 채워져 있고 인접 셀이 비어있으면 그 변은 외곽.
  type EdgeKey = string;
  const edges = new Map<EdgeKey, { from: [number, number]; to: [number, number] }>();
  const addEdge = (fi: number, fj: number, ti: number, tj: number) => {
    const key = `${fi},${fj}-${ti},${tj}`;
    edges.set(key, { from: [fi, fj], to: [ti, tj] });
  };
  const cellOcc = (i: number, j: number) =>
    j >= 0 && j < occ.length && i >= 0 && i < occ[0].length ? occ[j][i] : false;

  for (let j = 0; j < occ.length; j++) {
    for (let i = 0; i < occ[0].length; i++) {
      if (!occ[j][i]) continue;
      // top edge (셀 위쪽이 비어있음 → 시계방향: left→right at y=ys[j])
      if (!cellOcc(i, j - 1)) addEdge(i, j, i + 1, j);
      // right edge (셀 오른쪽이 비어있음 → 시계방향: top→bottom at x=xs[i+1])
      if (!cellOcc(i + 1, j)) addEdge(i + 1, j, i + 1, j + 1);
      // bottom edge (셀 아래가 비어있음 → 시계방향: right→left at y=ys[j+1])
      if (!cellOcc(i, j + 1)) addEdge(i + 1, j + 1, i, j + 1);
      // left edge (셀 왼쪽이 비어있음 → 시계방향: bottom→top at x=xs[i])
      if (!cellOcc(i - 1, j)) addEdge(i, j + 1, i, j);
    }
  }

  // 4) 같은 from 격자점에서 시작하는 edge 인덱스
  const fromMap = new Map<string, { from: [number, number]; to: [number, number] }[]>();
  edges.forEach((e) => {
    const k = `${e.from[0]},${e.from[1]}`;
    const arr = fromMap.get(k) || [];
    arr.push(e);
    fromMap.set(k, arr);
  });

  // 5) edge들을 head-tail로 연결해서 닫힌 polygon들 추출
  const paths: OutlinePath[] = [];
  const used = new Set<string>();
  edges.forEach((startEdge, startKey) => {
    if (used.has(startKey)) return;
    const points: { x: number; y: number }[] = [];
    let cur = startEdge;
    let curKey = startKey;
    while (cur && !used.has(curKey)) {
      used.add(curKey);
      points.push({ x: xs[cur.from[0]], y: ys[cur.from[1]] });
      const nextKeyPrefix = `${cur.to[0]},${cur.to[1]}`;
      const candidates = fromMap.get(nextKeyPrefix) || [];
      const next = candidates.find((e) => !used.has(`${e.from[0]},${e.from[1]}-${e.to[0]},${e.to[1]}`));
      if (!next) break;
      cur = next;
      curKey = `${next.from[0]},${next.from[1]}-${next.to[0]},${next.to[1]}`;
    }
    if (points.length >= 3) paths.push({ points: simplifyCollinear(points) });
  });
  return paths;
}

// ─────────────────────────────────────────────────────────
// 자투리 그룹 재분할 — 그룹의 union 영역을 다른 전략으로 직사각형 분해
// ─────────────────────────────────────────────────────────
// strategy:
//   "h" — 가로 strip 우선 (기본 자동 추출과 같음, 세로 인접 합침)
//   "v" — 세로 strip 우선 (가로 인접 합침)
//   "max" — Maximum Empty Rectangle 반복 (가장 큰 사각형부터 추출)

export function decomposeUnion(
  rects: { x: number; y: number; w: number; h: number }[],
  strategy: "h" | "v" | "max"
): { x: number; y: number; w: number; h: number }[] {
  if (rects.length === 0) return [];

  // 격자 좌표
  const xs = [...new Set(rects.flatMap((r) => [r.x, r.x + r.w]))].sort((a, b) => a - b);
  const ys = [...new Set(rects.flatMap((r) => [r.y, r.y + r.h]))].sort((a, b) => a - b);

  // 격자 occupancy
  const cols = xs.length - 1;
  const rows = ys.length - 1;
  const occ: boolean[][] = [];
  for (let j = 0; j < rows; j++) {
    const row: boolean[] = [];
    const cy = (ys[j] + ys[j + 1]) / 2;
    for (let i = 0; i < cols; i++) {
      const cx = (xs[i] + xs[i + 1]) / 2;
      const o = rects.some((r) => cx >= r.x && cx <= r.x + r.w && cy >= r.y && cy <= r.y + r.h);
      row.push(o);
    }
    occ.push(row);
  }

  if (strategy === "h") {
    // 각 row strip에서 occupied 셀들을 가로 인접으로 합침
    const out: { x: number; y: number; w: number; h: number }[] = [];
    for (let j = 0; j < rows; j++) {
      let i = 0;
      while (i < cols) {
        if (!occ[j][i]) { i++; continue; }
        let i2 = i;
        while (i2 < cols && occ[j][i2]) i2++;
        out.push({ x: xs[i], y: ys[j], w: xs[i2] - xs[i], h: ys[j + 1] - ys[j] });
        i = i2;
      }
    }
    return mergeAdjacentLocal(out, "vertical");
  }

  if (strategy === "v") {
    // 각 column strip에서 occupied 셀들을 세로 인접으로 합침
    const out: { x: number; y: number; w: number; h: number }[] = [];
    for (let i = 0; i < cols; i++) {
      let j = 0;
      while (j < rows) {
        if (!occ[j][i]) { j++; continue; }
        let j2 = j;
        while (j2 < rows && occ[j2][i]) j2++;
        out.push({ x: xs[i], y: ys[j], w: xs[i + 1] - xs[i], h: ys[j2] - ys[j] });
        j = j2;
      }
    }
    return mergeAdjacentLocal(out, "horizontal");
  }

  // "max" — Maximum Empty Rectangle 반복
  const out: { x: number; y: number; w: number; h: number }[] = [];
  const occCopy = occ.map((row) => [...row]);
  while (true) {
    const best = findLargestRectangleInGrid(occCopy, xs, ys);
    if (!best || best.w * best.h < 1) break;
    out.push(best);
    // 사용한 영역 점유 해제
    for (let j = 0; j < rows; j++) {
      for (let i = 0; i < cols; i++) {
        const cx = (xs[i] + xs[i + 1]) / 2;
        const cy = (ys[j] + ys[j + 1]) / 2;
        if (cx >= best.x && cx <= best.x + best.w && cy >= best.y && cy <= best.y + best.h) {
          occCopy[j][i] = false;
        }
      }
    }
  }
  return out;
}

// 격자에서 가장 큰 직사각형 (모든 셀 occupied) 찾기 — Histogram 기반 O(rows × cols)
function findLargestRectangleInGrid(
  occ: boolean[][],
  xs: number[],
  ys: number[]
): { x: number; y: number; w: number; h: number } | null {
  const rows = occ.length;
  if (rows === 0) return null;
  const cols = occ[0].length;
  if (cols === 0) return null;

  // 각 셀 위쪽 연속 occupied 셀 수 (히스토그램)
  const heights: number[][] = [];
  for (let j = 0; j < rows; j++) {
    const row: number[] = [];
    for (let i = 0; i < cols; i++) {
      row.push(occ[j][i] ? (j > 0 ? heights[j - 1][i] + 1 : 1) : 0);
    }
    heights.push(row);
  }

  let bestArea = 0;
  let bestRect: { x: number; y: number; w: number; h: number } | null = null;

  for (let j = 0; j < rows; j++) {
    // 각 행마다 히스토그램에서 가장 큰 직사각형 (실제 mm 면적 기준)
    const stack: number[] = [];
    for (let i = 0; i <= cols; i++) {
      const cur = i === cols ? 0 : heights[j][i];
      while (stack.length && (i === cols || heights[j][stack[stack.length - 1]] > cur)) {
        const top = stack.pop()!;
        const left = stack.length ? stack[stack.length - 1] + 1 : 0;
        const right = i - 1;
        const cellH = heights[j][top];
        // 실제 좌표
        const x = xs[left];
        const w = xs[right + 1] - xs[left];
        const y = ys[j - cellH + 1];
        const h = ys[j + 1] - ys[j - cellH + 1];
        const area = w * h;
        if (area > bestArea) {
          bestArea = area;
          bestRect = { x, y, w, h };
        }
      }
      stack.push(i);
    }
  }
  return bestRect;
}

// 인접 합치기 (decomposeUnion 전용 로컬 버전 — coord 비교만)
function mergeAdjacentLocal(
  rects: { x: number; y: number; w: number; h: number }[],
  direction: "vertical" | "horizontal"
): { x: number; y: number; w: number; h: number }[] {
  const out = rects.map((r) => ({ ...r }));
  let changed = true;
  while (changed) {
    changed = false;
    for (let i = 0; i < out.length && !changed; i++) {
      for (let j = i + 1; j < out.length; j++) {
        const a = out[i];
        const b = out[j];
        if (direction === "vertical") {
          if (Math.abs(a.x - b.x) < 0.5 && Math.abs(a.w - b.w) < 0.5) {
            if (Math.abs(a.y + a.h - b.y) < 0.5) {
              out[i] = { x: a.x, y: a.y, w: a.w, h: a.h + b.h };
              out.splice(j, 1);
              changed = true;
              break;
            }
            if (Math.abs(b.y + b.h - a.y) < 0.5) {
              out[i] = { x: a.x, y: b.y, w: a.w, h: a.h + b.h };
              out.splice(j, 1);
              changed = true;
              break;
            }
          }
        } else {
          if (Math.abs(a.y - b.y) < 0.5 && Math.abs(a.h - b.h) < 0.5) {
            if (Math.abs(a.x + a.w - b.x) < 0.5) {
              out[i] = { x: a.x, y: a.y, w: a.w + b.w, h: a.h };
              out.splice(j, 1);
              changed = true;
              break;
            }
            if (Math.abs(b.x + b.w - a.x) < 0.5) {
              out[i] = { x: b.x, y: a.y, w: a.w + b.w, h: a.h };
              out.splice(j, 1);
              changed = true;
              break;
            }
          }
        }
      }
    }
  }
  return out;
}

// 같은 직선 위 연속점 제거 (꼭짓점만 남김)
function simplifyCollinear(points: { x: number; y: number }[]): { x: number; y: number }[] {
  if (points.length < 3) return points;
  const out: { x: number; y: number }[] = [];
  const n = points.length;
  for (let i = 0; i < n; i++) {
    const prev = points[(i - 1 + n) % n];
    const cur = points[i];
    const next = points[(i + 1) % n];
    const collinearH = prev.y === cur.y && cur.y === next.y;
    const collinearV = prev.x === cur.x && cur.x === next.x;
    if (!collinearH && !collinearV) out.push(cur);
  }
  return out.length >= 3 ? out : points;
}

// ─────────────────────────────────────────────────────────
// 길이형 1D 배치 (FFD)
// ─────────────────────────────────────────────────────────

export function packLength(mats: Material[], items: PlanItem[], kerf: number): LengthResult {
  const pieces: { len: number; name: string; color: string; itemIdx: number }[] = [];
  items.forEach((item, idx) => {
    const q = parseInt(String(item.qty)) || 0;
    const len = item.length || 0;
    for (let i = 0; i < q; i++) pieces.push({ len, name: item.name, color: item.color, itemIdx: idx });
  });
  pieces.sort((a, b) => b.len - a.len);

  const pipes: Pipe[] = [];
  for (const p of pieces) {
    let placed = false;
    for (const pipe of pipes) {
      if (pipe.remaining >= p.len) {
        pipe.segments.push({
          len: p.len, color: p.color, name: p.name, itemIdx: p.itemIdx,
          startX: pipe.matLen - pipe.remaining,
        });
        pipe.remaining -= p.len + kerf;
        placed = true;
        break;
      }
    }
    if (!placed) {
      const mat = mats.find((m) => m && (m.length || 0) >= p.len);
      if (!mat) continue;
      pipes.push({
        id: pipes.length + 1,
        matLen: mat.length || 0,
        matCode: mat.code,
        matName: mat.name,
        remaining: (mat.length || 0) - p.len - kerf,
        segments: [{ len: p.len, color: p.color, name: p.name, itemIdx: p.itemIdx, startX: 0 }],
      });
    }
  }
  return { pipes };
}

// ─────────────────────────────────────────────────────────
// 동일 배치 그룹화
// ─────────────────────────────────────────────────────────

export function computeSheetGroups(sheets: Sheet[]): SheetGroup[] {
  const groups: SheetGroup[] = [];
  const used = new Set<number>();
  sheets.forEach((sheet, si) => {
    if (used.has(si)) return;
    const indices = [si];
    sheets.forEach((other, oi) => {
      if (oi <= si || used.has(oi)) return;
      if (isSameSheetLayout(sheet, other)) { indices.push(oi); used.add(oi); }
    });
    used.add(si);
    groups.push({ count: indices.length, repIdx: si, representative: sheet, indices });
  });
  return groups;
}

function isSameSheetLayout(a: Sheet, b: Sheet): boolean {
  if (a.matCode !== b.matCode || a.placements.length !== b.placements.length) return false;
  return a.placements.every((pa, i) => {
    const pb = b.placements[i];
    return (
      pa.itemIdx === pb.itemIdx && pa.w === pb.w && pa.h === pb.h &&
      Math.abs(pa.x - pb.x) < 0.5 && Math.abs(pa.y - pb.y) < 0.5
    );
  });
}

export function computePipeGroups(pipes: Pipe[]): PipeGroup[] {
  const groups: PipeGroup[] = [];
  const used = new Set<number>();
  pipes.forEach((pipe, pi) => {
    if (used.has(pi)) return;
    const indices = [pi];
    pipes.forEach((other, oi) => {
      if (oi <= pi || used.has(oi)) return;
      if (isSamePipeLayout(pipe, other)) { indices.push(oi); used.add(oi); }
    });
    used.add(pi);
    groups.push({ count: indices.length, repIdx: pi, representative: pipe, indices });
  });
  return groups;
}

function isSamePipeLayout(a: Pipe, b: Pipe): boolean {
  if (a.matCode !== b.matCode || a.segments.length !== b.segments.length) return false;
  return a.segments.every((sa, i) => {
    const sb = b.segments[i];
    return sa.itemIdx === sb.itemIdx && sa.len === sb.len;
  });
}

// ─────────────────────────────────────────────────────────
// 통계
// ─────────────────────────────────────────────────────────

export function computeAreaStats(result: AreaResult) {
  const sheets = result.sheets;
  const count = sheets.length;
  const totalPieces = sheets.reduce((s, sh) => s + sh.placements.length, 0);
  const rotated = sheets.reduce(
    (s, sh) => s + sh.placements.filter((p) => p.rotated).length, 0
  );
  const totalMat = sheets.reduce((s, sh) => s + sh.matW * sh.matH, 0);
  const usedArea = sheets.reduce(
    (s, sh) => s + sh.placements.reduce((ss, p) => ss + p.w * p.h, 0), 0
  );
  const util = totalMat > 0 ? (usedArea / totalMat) * 100 : 0;
  const loss = totalMat - usedArea;
  return { count, totalPieces, rotated, util, loss, usedArea, totalMat };
}

export function computeLengthStats(result: LengthResult) {
  const pipes = result.pipes;
  const count = pipes.length;
  const totalPieces = pipes.reduce((s, p) => s + p.segments.length, 0);
  const totalLen = pipes.reduce((s, p) => s + p.matLen, 0);
  const usedLen = pipes.reduce(
    (s, p) => s + p.segments.reduce((ss, seg) => ss + seg.len, 0), 0
  );
  const util = totalLen > 0 ? (usedLen / totalLen) * 100 : 0;
  const loss = totalLen - usedLen;
  return { count, totalPieces, util, loss, usedLen, totalLen };
}
