export type Vec2 = [number, number];

export type VoronoiCell = {
  d: string;
  points: Vec2[];
};

function distSq(a: Vec2, b: Vec2) {
  const dx = a[0] - b[0];
  const dy = a[1] - b[1];
  return dx * dx + dy * dy;
}

function intersectEdge(a: Vec2, b: Vec2, keep: Vec2, other: Vec2): Vec2 {
  const da = distSq(a, keep) - distSq(a, other);
  const db = distSq(b, keep) - distSq(b, other);
  const denom = da - db;
  const t = Math.abs(denom) < 1e-9 ? 0.5 : da / denom;
  return [a[0] + t * (b[0] - a[0]), a[1] + t * (b[1] - a[1])];
}

function clipPolygonByHalfPlane(poly: Vec2[], keep: Vec2, other: Vec2): Vec2[] {
  if (poly.length === 0) return [];
  const inside = (p: Vec2) => distSq(p, keep) <= distSq(p, other) + 1e-6;
  const out: Vec2[] = [];

  for (let i = 0; i < poly.length; i++) {
    const curr = poly[i];
    const prev = poly[(i - 1 + poly.length) % poly.length];
    const currIn = inside(curr);
    const prevIn = inside(prev);

    if (prevIn && currIn) out.push(curr);
    else if (prevIn && !currIn) out.push(intersectEdge(prev, curr, keep, other));
    else if (!prevIn && currIn) {
      out.push(intersectEdge(prev, curr, keep, other));
      out.push(curr);
    }
  }

  return out;
}

function voronoiPolygon(site: Vec2, sites: Vec2[], bounds: Vec2[]): Vec2[] {
  let poly = bounds;
  for (const other of sites) {
    if (other === site) continue;
    poly = clipPolygonByHalfPlane(poly, site, other);
    if (poly.length < 3) return [];
  }
  return poly;
}

export function polygonPath(points: Vec2[]): string {
  if (points.length < 3) return "";
  return (
    points
      .map((p, i) => `${i === 0 ? "M" : "L"} ${p[0].toFixed(2)} ${p[1].toFixed(2)}`)
      .join(" ") + " Z"
  );
}

function len(a: Vec2, b: Vec2) {
  return Math.hypot(b[0] - a[0], b[1] - a[1]);
}

/** Soft pebble edges — rounded corners on each Voronoi cell. */
export function roundedPolygonPath(points: Vec2[], radius = 1.35): string {
  const n = points.length;
  if (n < 3) return polygonPath(points);

  const parts: string[] = [];

  for (let i = 0; i < n; i++) {
    const prev = points[(i - 1 + n) % n];
    const curr = points[i];
    const next = points[(i + 1) % n];

    const e1: Vec2 = [prev[0] - curr[0], prev[1] - curr[1]];
    const e2: Vec2 = [next[0] - curr[0], next[1] - curr[1]];
    const l1 = Math.hypot(e1[0], e1[1]) || 1;
    const l2 = Math.hypot(e2[0], e2[1]) || 1;
    const cut = Math.min(radius, l1 * 0.38, l2 * 0.38);

    const p1: Vec2 = [curr[0] + (e1[0] / l1) * cut, curr[1] + (e1[1] / l1) * cut];
    const p2: Vec2 = [curr[0] + (e2[0] / l2) * cut, curr[1] + (e2[1] / l2) * cut];

    if (i === 0) parts.push(`M ${p1[0].toFixed(2)} ${p1[1].toFixed(2)}`);
    else parts.push(`L ${p1[0].toFixed(2)} ${p1[1].toFixed(2)}`);
    parts.push(`Q ${curr[0].toFixed(2)} ${curr[1].toFixed(2)} ${p2[0].toFixed(2)} ${p2[1].toFixed(2)}`);
  }

  return parts.join(" ") + " Z";
}

/** Radial pebble field — smaller cells near center, larger toward edges (rock mosaic). */
export function generateRadialRockSites(
  count: number,
  width: number,
  height: number,
  seed: number
): Vec2[] {
  const rng = createRng(seed);
  const cx = width / 2;
  const cy = height / 2;
  const sites: Vec2[] = [];

  for (let i = 0; i < count; i++) {
    const angle = rng() * Math.PI * 2;
    const r = Math.pow(rng(), 0.55) * 0.46;
    sites.push([cx + Math.cos(angle) * r * width, cy + Math.sin(angle) * r * height * 0.92]);
  }

  return sites;
}

export function createRng(seed: number) {
  let s = seed % 2147483646 || 1;
  return () => {
    s = (s * 16807) % 2147483647;
    return (s - 1) / 2147483646;
  };
}

export function generatePoissonSites(
  count: number,
  width: number,
  height: number,
  seed: number,
  minDist: number
): Vec2[] {
  const rng = createRng(seed);
  const sites: Vec2[] = [];
  let attempts = 0;

  while (sites.length < count && attempts < count * 120) {
    const p: Vec2 = [rng() * width, rng() * height];
    if (sites.every((s) => distSq(s, p) >= minDist * minDist)) sites.push(p);
    attempts++;
  }

  return sites;
}

/** Ghost sites pull tessellation to the edges (stained-glass border). */
export function borderGhostSites(width: number, height: number, pad = 18): Vec2[] {
  const ghosts: Vec2[] = [];
  const steps = 5;
  for (let i = 0; i <= steps; i++) {
    const t = i / steps;
    ghosts.push([-pad, height * t]);
    ghosts.push([width + pad, height * t]);
    ghosts.push([width * t, -pad]);
    ghosts.push([width * t, height + pad]);
  }
  return ghosts;
}

export type VoronoiMeshOptions = {
  siteGenerator?: "poisson" | "radial";
  shape?: "rounded" | "sharp";
  /** Poisson minimum spacing — lower = finer cells */
  minDist?: number;
  cornerRadiusMax?: number;
  cornerRadiusFactor?: number;
};

export function buildVoronoiMesh(
  siteCount: number,
  width: number,
  height: number,
  seed: number,
  options: VoronoiMeshOptions = {}
): VoronoiCell[] {
  const {
    siteGenerator = "poisson",
    shape = "rounded",
    minDist = 8,
    cornerRadiusMax = 2.2,
    cornerRadiusFactor = 0.22,
  } = options;
  const sites =
    siteGenerator === "radial"
      ? generateRadialRockSites(siteCount, width, height, seed)
      : generatePoissonSites(siteCount, width, height, seed, minDist);
  const allSites = [...sites, ...borderGhostSites(width, height)];
  const bounds: Vec2[] = [
    [0, 0],
    [width, 0],
    [width, height],
    [0, height],
  ];

  const cells: VoronoiCell[] = [];

  for (const site of sites) {
    const points = voronoiPolygon(site, allSites, bounds);
    if (points.length < 3) continue;
    const avgEdge =
      points.reduce((sum, p, i) => sum + len(p, points[(i + 1) % points.length]), 0) /
      points.length;
    const d =
      shape === "sharp"
        ? polygonPath(points)
        : roundedPolygonPath(
            points,
            Math.min(cornerRadiusMax, avgEdge * cornerRadiusFactor)
          );
    if (d) cells.push({ d, points });
  }

  return cells;
}
