/**
 * Pure geometry functions for splitting a polygon by a line.
 *
 * No OpenLayers dependency — operates on raw coordinate arrays.
 */

/**
 * Compute the intersection point of two 2D line segments.
 * Segment A: p1→p2, Segment B: p3→p4.
 *
 * @param {number[]} p1
 * @param {number[]} p2
 * @param {number[]} p3
 * @param {number[]} p4
 * @param {number} [eps=1e-10] tolerance for parallel check
 * @returns {{ point: number[], t: number, u: number } | null}
 *   t = parametric position on segment A (0–1),
 *   u = parametric position on segment B (0–1)
 */
function segmentIntersection(p1, p2, p3, p4, eps = 1e-10) {
  const dx1 = p2[0] - p1[0];
  const dy1 = p2[1] - p1[1];
  const dx2 = p4[0] - p3[0];
  const dy2 = p4[1] - p3[1];

  const denom = dx1 * dy2 - dy1 * dx2;
  if (Math.abs(denom) < eps) return null; // parallel / collinear

  const dx3 = p3[0] - p1[0];
  const dy3 = p3[1] - p1[1];

  const t = (dx3 * dy2 - dy3 * dx2) / denom;
  const u = (dx3 * dy1 - dy3 * dx1) / denom;

  if (t < -eps || t > 1 + eps || u < -eps || u > 1 + eps) return null;

  return {
    point: [p1[0] + t * dx1, p1[1] + t * dy1],
    t: Math.max(0, Math.min(1, t)),
    u: Math.max(0, Math.min(1, u)),
  };
}

/**
 * Signed area of a ring (shoelace formula).
 * Positive = counter-clockwise, negative = clockwise.
 */
function signedArea(ring) {
  let area = 0;
  for (let i = 0, n = ring.length; i < n - 1; i++) {
    area += (ring[i][0] * ring[i + 1][1]) - (ring[i + 1][0] * ring[i][1]);
  }
  return area / 2;
}

/**
 * Test whether a point is inside a ring (ray-casting algorithm).
 */
function pointInRing(pt, ring) {
  let inside = false;
  for (let i = 0, j = ring.length - 2; i < ring.length - 1; j = i++) {
    const xi = ring[i][0], yi = ring[i][1];
    const xj = ring[j][0], yj = ring[j][1];
    if (((yi > pt[1]) !== (yj > pt[1])) &&
        (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi)) {
      inside = !inside;
    }
  }
  return inside;
}

/**
 * Squared distance between two points.
 */
function dist2(a, b) {
  return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2;
}

/**
 * Find all intersection points between a cutting line and a polygon ring.
 *
 * @param {number[][]} ring  Closed ring coordinates (first === last)
 * @param {number[][]} line  LineString coordinates (2+ points)
 * @returns {Array<{ point: number[], ringSegIdx: number, ringT: number, lineSegIdx: number, lineT: number }>}
 */
function findIntersections(ring, line) {
  const hits = [];
  const eps = 1e-10;

  for (let li = 0; li < line.length - 1; li++) {
    for (let ri = 0; ri < ring.length - 1; ri++) {
      const ix = segmentIntersection(ring[ri], ring[ri + 1], line[li], line[li + 1], eps);
      if (!ix) continue;

      // Skip if intersection is at the very start of the ring segment
      // but was already caught as the end of the previous segment
      const pt = ix.point;

      // Avoid duplicate hits at shared vertices
      let isDup = false;
      for (const h of hits) {
        if (dist2(h.point, pt) < 1e-6) {
          isDup = true;
          break;
        }
      }
      if (isDup) continue;

      hits.push({
        point: pt,
        ringSegIdx: ri,
        ringT: ix.t,
        lineSegIdx: li,
        lineT: ix.u,
      });
    }
  }

  // Sort by position along the cutting line
  hits.sort((a, b) => {
    if (a.lineSegIdx !== b.lineSegIdx) return a.lineSegIdx - b.lineSegIdx;
    return a.lineT - b.lineT;
  });

  return hits;
}

/**
 * Insert intersection points into a ring, returning the expanded ring
 * and the new indices of the inserted points.
 *
 * @param {number[][]} ring  Closed ring (first === last)
 * @param {Array<{ point: number[], ringSegIdx: number, ringT: number }>} hits
 *   Sorted by ringSegIdx then ringT.
 * @returns {{ ring: number[][], indices: number[] }}
 */
function insertPointsIntoRing(ring, hits) {
  // Sort hits by ring position (segment index, then parametric t) so
  // we can insert from back to front without shifting earlier indices.
  const sorted = hits.map((h, i) => ({ ...h, origOrder: i }));
  sorted.sort((a, b) => {
    if (a.ringSegIdx !== b.ringSegIdx) return a.ringSegIdx - b.ringSegIdx;
    return a.ringT - b.ringT;
  });

  const expanded = ring.slice(); // copy
  const indices = new Array(sorted.length);

  // Insert from the end so that earlier insertions don't shift later indices.
  for (let k = sorted.length - 1; k >= 0; k--) {
    const h = sorted[k];
    const insertIdx = h.ringSegIdx + 1;

    // Check if this point is essentially identical to an existing vertex
    const snapDist = 1e-6;
    if (dist2(h.point, expanded[h.ringSegIdx]) < snapDist) {
      indices[h.origOrder] = h.ringSegIdx;
      continue;
    }
    if (dist2(h.point, expanded[h.ringSegIdx + 1]) < snapDist) {
      indices[h.origOrder] = h.ringSegIdx + 1;
      continue;
    }

    // Insert the new point
    expanded.splice(insertIdx, 0, h.point);
    indices[h.origOrder] = insertIdx;

    // Adjust indices for all previously recorded insertions
    // that reference a position >= insertIdx
    for (let j = k + 1; j < sorted.length; j++) {
      if (indices[sorted[j].origOrder] >= insertIdx) {
        indices[sorted[j].origOrder]++;
      }
    }
  }

  return { ring: expanded, indices };
}

/**
 * Extract a slice of a ring from index i0 to i1 (going forward, wrapping).
 * Both endpoints are included.
 *
 * @param {number[][]} ring  Closed ring (first === last); length includes closing vertex
 * @param {number} i0  Start index (inclusive)
 * @param {number} i1  End index (inclusive)
 * @returns {number[][]}
 */
function ringSlice(ring, i0, i1) {
  const n = ring.length - 1; // number of unique vertices (ring is closed)
  // Normalise indices into the [0, n-1] range
  const start = ((i0 % n) + n) % n;
  const end = ((i1 % n) + n) % n;
  const result = [];
  let idx = start;
  while (true) {
    result.push(ring[idx]);
    if (idx === end) break;
    idx = (idx + 1) % n;
  }
  return result;
}

/**
 * Extract the cutting line segment between two intersection points.
 *
 * @param {number[][]} line       Full cutting line coordinates
 * @param {{ point: number[], lineSegIdx: number, lineT: number }} hit0
 * @param {{ point: number[], lineSegIdx: number, lineT: number }} hit1
 * @returns {number[][]}  Coordinates from hit0.point to hit1.point along the line
 */
function cuttingLineSlice(line, hit0, hit1) {
  const result = [hit0.point];

  // Include all intermediate line vertices between the two hit segments
  const startSeg = hit0.lineSegIdx;
  const endSeg = hit1.lineSegIdx;

  for (let i = startSeg + 1; i <= endSeg; i++) {
    result.push(line[i]);
  }

  // Add the end intersection point if it's not the same as the last vertex
  if (dist2(result[result.length - 1], hit1.point) > 1e-10) {
    result.push(hit1.point);
  }

  return result;
}

/**
 * Ensure a ring has the desired winding order.
 * @param {number[][]} ring  Closed ring
 * @param {boolean} ccw  true for counter-clockwise
 * @returns {number[][]}
 */
function ensureWinding(ring, ccw) {
  const area = signedArea(ring);
  if ((ccw && area < 0) || (!ccw && area > 0)) {
    return ring.slice().reverse();
  }
  return ring;
}

/**
 * Close a ring (ensure first === last).
 */
function closeRing(coords) {
  if (coords.length < 2) return coords;
  const first = coords[0];
  const last = coords[coords.length - 1];
  if (dist2(first, last) > 1e-10) {
    return [...coords, first.slice()];
  }
  return coords;
}

/**
 * Extend a cutting line so that both endpoints lie outside the polygon ring.
 * If an endpoint is inside, we extend the first/last segment outward past the
 * bounding box diagonal so it definitely exits.
 *
 * @param {number[][]} line  Cutting line coordinates
 * @param {number[][]} ring  Closed polygon ring
 * @returns {number[][]}  Extended line (may be the original if already outside)
 */
function extendLineOutsideRing(line, ring) {
  // Compute bounding-box diagonal for a generous extension distance
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
  for (const pt of ring) {
    if (pt[0] < minX) minX = pt[0];
    if (pt[1] < minY) minY = pt[1];
    if (pt[0] > maxX) maxX = pt[0];
    if (pt[1] > maxY) maxY = pt[1];
  }
  const diag = Math.sqrt((maxX - minX) ** 2 + (maxY - minY) ** 2) || 1;

  const result = line.slice();

  // Extend start if inside
  if (pointInRing(result[0], ring)) {
    const p0 = result[0];
    const p1 = result[1];
    const dx = p0[0] - p1[0];
    const dy = p0[1] - p1[1];
    const len = Math.sqrt(dx * dx + dy * dy) || 1;
    const scale = diag * 2 / len;
    result[0] = [p0[0] + dx * scale, p0[1] + dy * scale];
  }

  // Extend end if inside
  const last = result.length - 1;
  if (pointInRing(result[last], ring)) {
    const pN = result[last];
    const pN1 = result[last - 1];
    const dx = pN[0] - pN1[0];
    const dy = pN[1] - pN1[1];
    const len = Math.sqrt(dx * dx + dy * dy) || 1;
    const scale = diag * 2 / len;
    result[last] = [pN[0] + dx * scale, pN[1] + dy * scale];
  }

  return result;
}

/**
 * Split a polygon by a cutting line.
 *
 * The cutting line can start or end inside the polygon — the algorithm will
 * automatically extend it outward so it crosses the boundary at exactly 2
 * points.  Multi-vertex cutting lines (with corners or approximated arcs)
 * are fully supported.
 *
 * @param {number[][][]} polygonCoords  Polygon coordinates:
 *   [exteriorRing, ...holeRings] where each ring is closed (first === last)
 * @param {number[][]} lineCoords  Cutting line coordinates (2+ points)
 * @returns {number[][][][] | null}  Two polygon coordinate arrays, or null if split failed
 */
export function splitPolygonByLine(polygonCoords, lineCoords) {
  const exteriorRing = polygonCoords[0];
  const holes = polygonCoords.slice(1);

  // Extend the cutting line if its endpoints are inside the polygon
  const extendedLine = extendLineOutsideRing(lineCoords, exteriorRing);

  // 1. Find intersections between cutting line and exterior ring
  const hits = findIntersections(exteriorRing, extendedLine);

  // We need exactly 2 intersection points for a simple split
  if (hits.length !== 2) {
    console.warn(`[polygonSplit] Expected 2 intersections, got ${hits.length}`);
    return null;
  }

  const [hit0, hit1] = hits;

  // 2. Insert intersection points into the ring
  const { ring: expandedRing, indices } = insertPointsIntoRing(exteriorRing, hits);
  const idx0 = indices[0];
  const idx1 = indices[1];

  // Ensure idx0 < idx1 for consistent traversal
  const [iA, iB] = idx0 < idx1 ? [idx0, idx1] : [idx1, idx0];
  const [hitA, hitB] = idx0 < idx1 ? [hit0, hit1] : [hit1, hit0];

  // 3. Get the cutting line segment between the two intersection points
  const cutForward = idx0 < idx1
    ? cuttingLineSlice(extendedLine, hit0, hit1)
    : cuttingLineSlice(extendedLine, hit1, hit0);
  const cutReverse = cutForward.slice().reverse();

  // 4. Build two polygon rings
  //    Ring A: walk ring from iA to iB (forward), then cutting line reversed back to iA
  const sliceAB = ringSlice(expandedRing, iA, iB);
  const ringA = closeRing([...sliceAB, ...cutReverse.slice(1)]);

  //    Ring B: walk ring from iB to iA (wrapping), then cutting line forward back to iB
  const sliceBA = ringSlice(expandedRing, iB, iA);
  const ringB = closeRing([...sliceBA, ...cutForward.slice(1)]);

  // 5. Match winding order to original
  const originalCCW = signedArea(exteriorRing) > 0;
  const finalA = ensureWinding(ringA, originalCCW);
  const finalB = ensureWinding(ringB, originalCCW);

  // 6. Build polygon coordinate arrays, assigning holes to the correct piece
  const polyA = [finalA];
  const polyB = [finalB];

  for (const hole of holes) {
    // Use the centroid of the hole to determine containment
    const centroid = holeCentroid(hole);
    if (pointInRing(centroid, finalA)) {
      polyA.push(hole);
    } else {
      polyB.push(hole);
    }
  }

  return [polyA, polyB];
}

/**
 * Compute the centroid of a closed ring.
 */
function holeCentroid(ring) {
  let cx = 0, cy = 0;
  const n = ring.length - 1; // exclude closing vertex
  for (let i = 0; i < n; i++) {
    cx += ring[i][0];
    cy += ring[i][1];
  }
  return [cx / n, cy / n];
}
