/* TEREN — parcel-geometry.jsx
   The shared POLYGON ENGINE. One geometry model for the whole Kreator zabudowy:
   a parcel is a ring of vertices in EPSG:2180 (metres), each EDGE tagged with a
   neighbour ({ulica|dzialka|las|woda} + wall type + building-line distance). The
   buildable area ("obszar zabudowy") is the parcel inset edge-by-edge by each
   edge's §12 setback. A RECTANGLE is just the 4-vertex special case — the manual
   "simplified fit" mode generates one of these and feeds the same engine, so there
   is exactly one set of rules and one drawing path.

   Pure functions only — no React, no DOM. Exposed as window.ParcelGeo.

   THE CONTRACT (what the Kreator passes in / gets back):
     parcel = {
       ring:  [[x,y], …]            // EPSG:2180 metres, open (no repeated vertex)
       edges: [ { neighbour, wall, line, streetDist, at0 }, … ]  // one per edge
       opts:  { forestDist, waterDist, pierzeja }
     }
     analyze(parcel) -> {
       ring, edges, area, perimeter, frontIdx, frontTheta, frontMid,
       setbacks:[…], buildable:{poly,area}, frontWidth, grid
     }
*/
(function () {
  // ---------------------------------------------------------- projection ----
  // geographic (GRS80) → EPSG:2180 (PUWG 1992), metres. lon0=19°, k0=0.9993.
  const A = 6378137, F = 1 / 298.257222101, E2 = F * (2 - F), EP2 = E2 / (1 - E2);
  const D2R = Math.PI / 180;
  function proj2180(lon, lat) {
    const lon0 = 19 * D2R, k0 = 0.9993, FE = 500000, FN = -5300000;
    const phi = lat * D2R, lam = lon * D2R;
    const sp = Math.sin(phi), cp = Math.cos(phi), tp = Math.tan(phi);
    const N = A / Math.sqrt(1 - E2 * sp * sp);
    const T = tp * tp, C = EP2 * cp * cp, a = (lam - lon0) * cp;
    const M = A * ((1 - E2 / 4 - 3 * E2 * E2 / 64 - 5 * E2 ** 3 / 256) * phi
      - (3 * E2 / 8 + 3 * E2 * E2 / 32 + 45 * E2 ** 3 / 1024) * Math.sin(2 * phi)
      + (15 * E2 * E2 / 256 + 45 * E2 ** 3 / 1024) * Math.sin(4 * phi)
      - (35 * E2 ** 3 / 3072) * Math.sin(6 * phi));
    const x = FE + k0 * N * (a + (1 - T + C) * a ** 3 / 6 + (5 - 18 * T + T * T + 72 * C - 58 * EP2) * a ** 5 / 120);
    const y = FN + k0 * (M + N * tp * (a * a / 2 + (5 - T + 9 * C + 4 * C * C) * a ** 4 / 24 + (61 - 58 * T + T * T + 600 * C - 330 * EP2) * a ** 6 / 720));
    return [x, y];
  }
  const ringTo2180 = (ring) => ring.map(([lo, la]) => proj2180(lo, la));

  // --------------------------------------------------------------- WKT -------
  function wktRings(wkt) {
    return [...(wkt || "").matchAll(/\(([^()]+)\)/g)].map((m) =>
      m[1].split(",").map((p) => p.trim().split(/\s+/).map(Number)).filter((p) => p.length >= 2 && !isNaN(p[0]) && !isNaN(p[1]))
    ).filter((r) => r.length >= 3);
  }

  // ------------------------------------------------------------ ring ops -----
  function openRing(r) {
    if (r.length > 1) {
      const a = r[0], b = r[r.length - 1];
      if (Math.hypot(a[0] - b[0], a[1] - b[1]) < 1e-6) return r.slice(0, -1);
    }
    return r;
  }
  const dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
  function area(r) {
    let s = 0;
    for (let i = 0; i < r.length; i++) { const a = r[i], b = r[(i + 1) % r.length]; s += a[0] * b[1] - b[0] * a[1]; }
    return Math.abs(s) / 2;
  }
  function perimeter(r) { let s = 0; for (let i = 0; i < r.length; i++) s += dist(r[i], r[(i + 1) % r.length]); return s; }
  function centroid(r) { const n = r.length; let x = 0, y = 0; r.forEach((p) => { x += p[0]; y += p[1]; }); return [x / n, y / n]; }
  function edges(r) { return r.map((p, i) => ({ a: p, b: r[(i + 1) % r.length], len: dist(p, r[(i + 1) % r.length]) })); }

  // ------------------------------------------------------- front + frame -----
  // largest-ring picker (when a WKT has holes / multipolygon)
  function outerRing(rings) {
    let idx = 0, best = -1;
    rings.forEach((r, i) => { const a = area(openRing(r)); if (a > best) { best = a; idx = i; } });
    return openRing(rings[idx]);
  }
  // front edge heuristic: the southernmost edge (lowest mean northing). Callers may
  // override frontIdx explicitly (e.g. the edge adjacent to a road parcel).
  function detectFront(ring) {
    const eg = edges(ring);
    let idx = 0, fy = Infinity;
    eg.forEach((e, i) => { const my = (e.a[1] + e.b[1]) / 2; if (my < fy) { fy = my; idx = i; } });
    return idx;
  }
  const rot = (p, th) => [p[0] * Math.cos(th) - p[1] * Math.sin(th), p[0] * Math.sin(th) + p[1] * Math.cos(th)];
  // rotation θ that lays the front edge horizontal with the interior ABOVE it
  // (front at the bottom — the Kreator "front na dole" convention).
  function frontTheta(ring, frontIdx) {
    const eg = edges(ring), fe = eg[frontIdx], C = centroid(ring);
    let th = -Math.atan2(fe.b[1] - fe.a[1], fe.b[0] - fe.a[0]);
    const feMidY = (rot(fe.a, th)[1] + rot(fe.b, th)[1]) / 2;
    if (rot(C, th)[1] < feMidY) th += Math.PI;
    return th;
  }
  const rotFns = (th) => ({
    fwd: (p) => [p[0] * Math.cos(th) - p[1] * Math.sin(th), p[0] * Math.sin(th) + p[1] * Math.cos(th)],
    inv: (p) => [p[0] * Math.cos(th) + p[1] * Math.sin(th), -p[0] * Math.sin(th) + p[1] * Math.cos(th)],
  });

  // ------------------------------------------------------- half-plane clip ---
  // Sutherland–Hodgman clip of `poly` to half-plane a*x+b*y+c <= 0
  function clipHP(poly, a, b, c) {
    if (!poly.length) return poly;
    const out = [];
    const side = (p) => a * p[0] + b * p[1] + c;
    for (let i = 0; i < poly.length; i++) {
      const cur = poly[i], nxt = poly[(i + 1) % poly.length];
      const sc = side(cur), sn = side(nxt);
      if (sc <= 0) out.push(cur);
      if ((sc < 0 && sn > 0) || (sc > 0 && sn < 0)) {
        const t = sc / (sc - sn);
        out.push([cur[0] + t * (nxt[0] - cur[0]), cur[1] + t * (nxt[1] - cur[1])]);
      }
    }
    return out;
  }

  // Inset the polygon by a per-edge distance: push each original edge inward (toward
  // the centroid) by setbacks[i] and intersect the half-planes. EXACT for convex
  // parcels (incl. every rectangle); a good approximation for mild concavities. A
  // genuinely non-convex lot (L-shape / flag) would need true polygon offsetting —
  // flagged via `convex` in the result so the UI can warn.
  function insetPolygon(ring, setbacks) {
    const C = centroid(ring);
    let poly = ring.slice();
    for (let i = 0; i < ring.length; i++) {
      const a = ring[i], b = ring[(i + 1) % ring.length];
      let nx = -(b[1] - a[1]), ny = (b[0] - a[0]);
      const nl = Math.hypot(nx, ny) || 1; nx /= nl; ny /= nl;
      if (nx * (C[0] - a[0]) + ny * (C[1] - a[1]) < 0) { nx = -nx; ny = -ny; }
      poly = clipHP(poly, -nx, -ny, nx * a[0] + ny * a[1] + setbacks[i]);
      if (poly.length < 3) return [];
    }
    return poly;
  }
  function isConvex(ring) {
    let sign = 0;
    const n = ring.length;
    for (let i = 0; i < n; i++) {
      const a = ring[i], b = ring[(i + 1) % n], c = ring[(i + 2) % n];
      const cross = (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]);
      if (Math.abs(cross) < 1e-9) continue;
      const s = cross > 0 ? 1 : -1;
      if (sign === 0) sign = s; else if (s !== sign) return false;
    }
    return true;
  }

  // ----------------------------------------------- segment distance (§12) ----
  // Distance from point p to the SEGMENT ab (perpendicular where the foot lands on
  // the segment, else to the nearer endpoint). This is the legally-binding measure:
  // each boundary fragment is treated separately, never as one infinite/averaged line.
  function segDist(p, a, b) {
    const vx = b[0] - a[0], vy = b[1] - a[1];
    const wx = p[0] - a[0], wy = p[1] - a[1];
    const c1 = vx * wx + vy * wy;
    if (c1 <= 0) return Math.hypot(wx, wy);
    const c2 = vx * vx + vy * vy;
    if (c2 <= c1) return Math.hypot(p[0] - b[0], p[1] - b[1]);
    const t = c1 / c2;
    return Math.hypot(p[0] - (a[0] + t * vx), p[1] - (a[1] + t * vy));
  }
  function segCross(p1, p2, p3, p4) {
    const o = (a, b, c) => Math.sign((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]));
    return o(p1, p2, p3) !== o(p1, p2, p4) && o(p3, p4, p1) !== o(p3, p4, p2);
  }
  // min distance between two segments — used to check each WALL fragment against each
  // boundary fragment individually (perpendicular where it lands, endpoint otherwise).
  function segSegDist(p1, p2, p3, p4) {
    if (segCross(p1, p2, p3, p4)) return 0;
    return Math.min(segDist(p1, p3, p4), segDist(p2, p3, p4), segDist(p3, p1, p2), segDist(p4, p1, p2));
  }

  // ------------------------------------------- exact buildable (per-segment) -
  // The buildable region is every point that is (a) inside the parcel AND (b) at
  // least setbacks[i] from EACH boundary segment i, measured as a true segment
  // distance. That makes differing-setback junctions round in by the larger radius
  // around the shared vertex — which half-plane (infinite-line) clipping gets wrong.
  // We evaluate a signed field on a front-aligned grid; ≥0 ⇒ buildable.
  function buildableField(ring, edgesW, setbacks, inv) {
    return function (xf, yf) {
      const pw = inv([xf, yf]);
      if (!inPoly(pw, ring)) return -1000;
      let m = Infinity;
      for (let i = 0; i < edgesW.length; i++) {
        const d = segDist(pw, edgesW[i].a, edgesW[i].b) - setbacks[i];
        if (d < m) m = d;
      }
      return m;
    };
  }
  // front-aligned occupancy grid (cell centre buildable?) for the rect fitters
  function buildableGrid(ring, fwd, field, target) {
    const rr = ring.map(fwd);
    const xs = rr.map((p) => p[0]), ys = rr.map((p) => p[1]);
    const minX = Math.min(...xs), maxX = Math.max(...xs), minY = Math.min(...ys), maxY = Math.max(...ys);
    const span = Math.max(maxX - minX, maxY - minY) || 1;
    const cell = span / (target || 180);
    const cols = Math.max(1, Math.ceil((maxX - minX) / cell));
    const rows = Math.max(1, Math.ceil((maxY - minY) / cell));
    const cw = (maxX - minX) / cols, ch = (maxY - minY) / rows;
    const mask = []; let count = 0;
    for (let r = 0; r < rows; r++) {
      const row = new Uint8Array(cols), y = minY + (r + 0.5) * ch;
      for (let c = 0; c < cols; c++) { const ok = field(minX + (c + 0.5) * cw, y) >= 0 ? 1 : 0; row[c] = ok; count += ok; }
      mask.push(row);
    }
    return { minX, minY, cw, ch, cols, rows, mask, area: count * cw * ch };
  }
  // marching squares on the signed field → smooth contour (front frame), longest loop
  function contour(grid, field) {
    const { minX, minY, cw, ch, cols, rows } = grid;
    const nx = cols + 1, ny = rows + 1, F = [];
    for (let r = 0; r < ny; r++) { const row = []; const y = minY + r * ch; for (let c = 0; c < nx; c++) row.push(field(minX + c * cw, y)); F.push(row); }
    const ip = (x1, y1, v1, x2, y2, v2) => { const t = v1 / (v1 - v2) || 0; return [x1 + t * (x2 - x1), y1 + t * (y2 - y1)]; };
    const segs = [];
    const TBL = { 1: [[3, 2]], 2: [[2, 1]], 3: [[3, 1]], 4: [[0, 1]], 5: [[3, 0], [2, 1]], 6: [[0, 2]], 7: [[3, 0]], 8: [[3, 0]], 9: [[0, 2]], 10: [[3, 2], [0, 1]], 11: [[0, 1]], 12: [[3, 1]], 13: [[2, 1]], 14: [[3, 2]] };
    for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) {
      const x0 = minX + c * cw, y0 = minY + r * ch, x1 = x0 + cw, y1 = y0 + ch;
      const tl = F[r][c], tr = F[r][c + 1], br = F[r + 1][c + 1], bl = F[r + 1][c];
      let k = 0; if (tl >= 0) k |= 8; if (tr >= 0) k |= 4; if (br >= 0) k |= 2; if (bl >= 0) k |= 1;
      const cs = TBL[k]; if (!cs) continue;
      const ept = (e) => e === 0 ? ip(x0, y0, tl, x1, y0, tr) : e === 1 ? ip(x1, y0, tr, x1, y1, br) : e === 2 ? ip(x1, y1, br, x0, y1, bl) : ip(x0, y1, bl, x0, y0, tl);
      cs.forEach(([u, v]) => segs.push([ept(u), ept(v)]));
    }
    if (!segs.length) return [];
    // stitch segments into polylines by endpoint proximity
    const tol = Math.min(cw, ch) * 0.6;
    const key = (p) => Math.round(p[0] / tol) + "," + Math.round(p[1] / tol);
    const map = new Map();
    segs.forEach((s, i) => { for (const p of s) { const kk = key(p); if (!map.has(kk)) map.set(kk, []); map.get(kk).push(i); } });
    const used = new Array(segs.length).fill(false);
    let best = [];
    for (let i = 0; i < segs.length; i++) {
      if (used[i]) continue;
      const loop = [segs[i][0], segs[i][1]]; used[i] = true;
      let guard = 0;
      while (guard++ < segs.length + 5) {
        const tail = loop[loop.length - 1];
        const cand = (map.get(key(tail)) || []).filter((j) => !used[j]);
        if (!cand.length) break;
        const j = cand[0]; used[j] = true;
        const [a, b] = segs[j];
        loop.push(Math.hypot(a[0] - tail[0], a[1] - tail[1]) <= Math.hypot(b[0] - tail[0], b[1] - tail[1]) ? b : a);
      }
      if (loop.length > best.length) best = loop;
    }
    return best;
  }
  function rdp(pts, eps) {
    if (pts.length < 3) return pts;
    let dmax = 0, idx = 0; const a = pts[0], b = pts[pts.length - 1];
    for (let i = 1; i < pts.length - 1; i++) { const d = segDist(pts[i], a, b); if (d > dmax) { dmax = d; idx = i; } }
    if (dmax > eps) { const l = rdp(pts.slice(0, idx + 1), eps), r = rdp(pts.slice(idx), eps); return l.slice(0, -1).concat(r); }
    return [a, b];
  }

  // ----------------------------------------------------- §12 setback rule ----
  // One number per edge, from what is on it. A street edge's "setback" is the
  // building line (linia zabudowy). A parcel edge: 4 m with windows; 3 m blank; on a
  // narrow plot (≤16 m) a blank wall may go to 1.5 m or onto the boundary (0 m).
  function edgeSetback(cfg, narrowApplies) {
    if (!cfg) return 4;
    switch (cfg.neighbour) {
      case "ulica": return cfg.streetDist == null ? 5 : cfg.streetDist;
      case "las": return cfg.forestDist == null ? 12 : cfg.forestDist;
      case "woda": return cfg.waterDist == null ? 1.5 : cfg.waterDist;
      default: // dzialka
        if (cfg.wall === "windows") return 4;
        if (cfg.allowNarrow && narrowApplies) return cfg.at0 ? 0 : 1.5;
        return 3;
    }
  }

  // ---------------------------------------------------------- rasterise ------
  function inPoly(pt, poly) {
    let inside = false;
    for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
      const xi = poly[i][0], yi = poly[i][1], xj = poly[j][0], yj = poly[j][1];
      if (((yi > pt[1]) !== (yj > pt[1])) && (pt[0] < (xj - xi) * (pt[1] - yi) / (yj - yi) + xi)) inside = !inside;
    }
    return inside;
  }
  // raster grid of a front-aligned polygon — row 0 is the FRONT (lowest y)
  function rasterMask(ringRot, cols, rows) {
    cols = cols || 150; rows = rows || 150;
    const xs = ringRot.map((p) => p[0]), ys = ringRot.map((p) => p[1]);
    const minX = Math.min(...xs), maxX = Math.max(...xs), minY = Math.min(...ys), maxY = Math.max(...ys);
    const cw = (maxX - minX) / cols, ch = (maxY - minY) / rows;
    if (!(cw > 0 && ch > 0)) return null;
    const mask = [];
    for (let r = 0; r < rows; r++) {
      const row = new Uint8Array(cols);
      const y = minY + (r + 0.5) * ch;
      for (let c = 0; c < cols; c++) row[c] = inPoly([minX + (c + 0.5) * cw, y], ringRot) ? 1 : 0;
      mask.push(row);
    }
    return { minX, minY, cw, ch, cols, rows, mask };
  }
  // widest front-aligned rectangle of a given depth, sitting on row 0 (the front)
  function rectAtDepth(grid, depthM) {
    if (!grid) return null;
    const { minX, minY, cw, ch, cols, rows, mask } = grid;
    const dRows = Math.max(1, Math.min(rows, Math.round(depthM / ch)));
    const colOK = new Uint8Array(cols);
    for (let c = 0; c < cols; c++) { let ok = 1; for (let r = 0; r < dRows; r++) { if (!mask[r][c]) { ok = 0; break; } } colOK[c] = ok; }
    let bestLen = 0, bestStart = 0, run = 0, runStart = 0;
    for (let c = 0; c <= cols; c++) {
      if (c < cols && colOK[c]) { if (run === 0) runStart = c; run++; }
      else { if (run > bestLen) { bestLen = run; bestStart = runStart; } run = 0; }
    }
    if (!bestLen) return null;
    const x0 = minX + bestStart * cw, x1 = minX + (bestStart + bestLen) * cw;
    return { cornersRot: [[x0, minY], [x1, minY], [x1, minY + dRows * ch], [x0, minY + dRows * ch]], w: x1 - x0, d: dRows * ch };
  }
  // largest-area front-aligned rectangle (histogram method)
  function largestRect(grid) {
    if (!grid) return null;
    const { minX, minY, cw, ch, cols, rows, mask } = grid;
    const heights = new Int32Array(cols);
    let best = { area: 0 };
    for (let r = 0; r < rows; r++) {
      for (let c = 0; c < cols; c++) heights[c] = mask[r][c] ? heights[c] + 1 : 0;
      const stack = [];
      for (let c = 0; c <= cols; c++) {
        const hC = c === cols ? 0 : heights[c];
        while (stack.length && heights[stack[stack.length - 1]] >= hC) {
          const top = stack.pop(), height = heights[top];
          const left = stack.length ? stack[stack.length - 1] + 1 : 0, right = c - 1;
          const ar = height * (right - left + 1) * cw * ch;
          if (ar > best.area) best = { area: ar, r, left, right, height };
        }
        stack.push(c);
      }
    }
    if (!best.area) return null;
    const x0 = minX + best.left * cw, x1 = minX + (best.right + 1) * cw;
    const y0 = minY + (best.r - best.height + 1) * ch, y1 = minY + (best.r + 1) * ch;
    return { cornersRot: [[x0, y0], [x1, y0], [x1, y1], [x0, y1]], w: x1 - x0, d: y1 - y0 };
  }

  // ---------------------------------------------------- rectangle builder ----
  // The "simplified fit" mode: build a 4-vertex parcel from W×D with the front edge
  // (index 0) at the bottom, then right(1), rear(2), left(3). Edge order matches the
  // old Kreator's leftSet/rightSet/rearSet/front so behaviour is identical.
  function rectangleParcel(W, D) {
    return { ring: [[0, 0], [W, 0], [W, D], [0, D]], frontIdx: 0, edgeOrder: ["front", "right", "rear", "left"] };
  }

  // ---------------------------------------------------------- analyze --------
  // The one entry point. `parcel.edges[i]` configures edge i (front auto if omitted).
  function analyze(parcel) {
    const ring = openRing(parcel.ring || []);
    if (ring.length < 3) return { error: "ring < 3 vertices" };
    const eg = edges(ring);
    const frontIdx = parcel.frontIdx != null ? parcel.frontIdx : detectFront(ring);
    const th = frontTheta(ring, frontIdx);
    const { fwd, inv } = rotFns(th);
    const fe = eg[frontIdx];
    const frontMid = [(fe.a[0] + fe.b[0]) / 2, (fe.a[1] + fe.b[1]) / 2];

    // front-frame bbox width → "narrow plot ≤ 16 m" test
    const ringRot = ring.map(fwd);
    const rxs = ringRot.map((p) => p[0]);
    const frontWidth = Math.max(...rxs) - Math.min(...rxs);
    const narrowApplies = frontWidth <= 16;

    const opts = parcel.opts || {};
    const setbacks = eg.map((e, i) => {
      if (opts.pierzeja && i !== frontIdx && isSideEdge(i, frontIdx, eg.length)) return 0;
      const cfg = Object.assign({ allowNarrow: i !== frontIdx, forestDist: opts.forestDist, waterDist: opts.waterDist }, (parcel.edges && parcel.edges[i]) || {});
      return edgeSetback(cfg, narrowApplies);
    });

    // EXACT buildable: distance to each boundary SEGMENT separately (per-segment §12),
    // so differing-setback junctions round in by the larger radius around the shared
    // vertex — unlike infinite-line / half-plane clipping which mitres them wrong.
    const field = buildableField(ring, eg, setbacks, inv);
    const grid = buildableGrid(ring, fwd, field, 240);
    let buildPoly = [];
    const cf = contour(grid, field);
    if (cf.length >= 4) {
      const simp = rdp(cf, Math.min(grid.cw, grid.ch) * 0.6);
      buildPoly = simp.map(inv);
      if (buildPoly.length >= 2) { const f = buildPoly[0], l = buildPoly[buildPoly.length - 1]; if (Math.hypot(f[0] - l[0], f[1] - l[1]) < 1e-6) buildPoly.pop(); }
    }
    // area from the raster occupancy (stable, ~grid-cell accurate); the polygon is for
    // display only. Both encode the same per-segment §12 rule.
    const buildArea = grid.area;

    return {
      ring, edges: eg, frontIdx, frontTheta: th, frontMid, fwd, inv,
      area: area(ring), perimeter: perimeter(ring), convex: isConvex(ring),
      narrowApplies, frontWidth, setbacks,
      buildable: { poly: buildPoly, area: buildArea }, grid,
    };
  }
  // a "side" edge = neither the front nor the edge opposite-ish; for pierzeja we zero
  // the two edges adjacent to the front (its immediate neighbours in the ring).
  function isSideEdge(i, frontIdx, n) {
    return i === (frontIdx + 1) % n || i === (frontIdx - 1 + n) % n;
  }

  window.ParcelGeo = {
    proj2180, ringTo2180, wktRings,
    openRing, area, perimeter, centroid, edges, dist,
    outerRing, detectFront, frontTheta, rotFns, rot,
    clipHP, insetPolygon, isConvex, edgeSetback,
    segDist, segSegDist,
    rasterMask, rectAtDepth, largestRect, rectangleParcel,
    analyze,
  };
})();
