/*
  site-plan.jsx — SHARED design surface module (window.SitePlan).
  Extracted verbatim from the Kreator's Plan component (index.html) so it is
  preserved as a reusable Track-B module (see docs/architecture-modules.md): the
  geometry sanity-check + drag/rotate/resize an imported building + setback/viz
  modes. DXF loading stays in window.ProjectLoader.

  Stack rules (CLAUDE.md): plain browser script, NO import/export — share via
  window.*. Load with <script type="text/babel" src="site-plan.jsx"> AFTER React
  + parcel-geometry-v6.jsx and BEFORE the page that renders <SitePlan>.

  Fully prop-driven (no host-state reads); mutations go out via the on* callbacks.
  Self-injects its .plan-svg / .viz-alert CSS with host-var fallbacks. Note: the
  SVG uses the host --plan-* palette (var(--plan-fill) etc.) inline — a page other
  than the Kreator must define that palette for the colours to resolve.
*/
(function () {
  // one-time self-injected CSS (host vars + fallbacks)
  if (!document.getElementById("siteplan-css")) {
    const st = document.createElement("style");
    st.id = "siteplan-css";
    st.textContent = `
  .plan-svg { display: block; width: 100%; height: auto; background: var(--plan-bg,#f0ece2); border: 1px solid var(--line,#ddd6c8); border-radius: var(--radius,6px); }
  @keyframes vizAlertBlink { 0%, 100% { opacity: 1; } 50% { opacity: .3; } }
  .viz-alert { animation: vizAlertBlink 1.4s ease-in-out infinite; }
  @media (prefers-reduced-motion: reduce) { .viz-alert { animation: none; } }`;
    document.head.appendChild(st);
  }

  // nf + SIDE_COLORS are mirrors of index.html (used app-wide there — keep in
  // sync); NB is Plan-only, so it lives here now.
  const nf = (n, d = 1) => (n == null || isNaN(n)) ? "—" : n.toLocaleString("pl-PL", { minimumFractionDigits: d, maximumFractionDigits: d });
  const SIDE_COLORS = ["#9a6a3a", "#4a7c52", "#3a6a8a", "#8a5a7a", "#7a7a3a", "#a8503e", "#5a5a8a", "#6a8a4a", "#5a8a6a", "#8a7a3a"];
  const NB = {
    ulica: { fill: "url(#road)", label: "ULICA" },
    las: { fill: "var(--plan-forest)", label: "LAS" },
    woda: { fill: "var(--plan-water)", label: "WODA" },
    dzialka: { fill: "var(--plan-fill)", label: "DZIAŁKA", dash: true },
  };

  function SitePlan({ an, build, gates, kinds, orient, editable, resizable, manual, bbF, violation, legal, wallFlags, vizMode, wallViz, fitLayout, onMove, onRotate, onResize, onBeginManual }) {
    const VW = 460, VH = 380, pad = 40;
    const T = orient === "real" ? ((p) => p) : an.fwd;
    const svgRef = React.useRef(null);
    const drag = React.useRef(null);
    const ringT = an.ring.map(T);
    const n = ringT.length;
    // centroid (front frame) to orient outward normals
    const C = [ringT.reduce((s, p) => s + p[0], 0) / n, ringT.reduce((s, p) => s + p[1], 0) / n];
    const BAND = 3.4, GAP = 0.5;
    // per-edge geometry in the front frame: outward normal, band quad, building-line segment
    const edgeGeo = ringT.map((a, i) => {
      const b = ringT[(i + 1) % n];
      let nx = -(b[1] - a[1]), ny = (b[0] - a[0]); const nl = Math.hypot(nx, ny) || 1; nx /= nl; ny /= nl;
      const mid = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
      if (nx * (C[0] - mid[0]) + ny * (C[1] - mid[1]) > 0) { nx = -nx; ny = -ny; } // point outward
      const off = (p, d) => [p[0] + nx * d, p[1] + ny * d];
      const set = (an.setbacks && an.setbacks[i]) || 0;
      return { a, b, nx, ny, mid,
        band: [off(a, GAP), off(b, GAP), off(b, GAP + BAND), off(a, GAP + BAND)],
        bandMid: off(mid, GAP + BAND / 2),
        line: [off(a, -set), off(b, -set)], // building/setback line, inset inward
      };
    });
    // bbox includes ring + all band quads + the building, so nothing ever clips
    const buildT = build ? build.corners.map((p) => T(p)) : [];
    const allPts = ringT.concat(...edgeGeo.map((g) => g.band)).concat(buildT).concat(...(wallViz || []).map((w) => w.zone.map(T)));
    const xs = allPts.map((p) => p[0]), ys = allPts.map((p) => p[1]);
    const minX = Math.min(...xs), maxX = Math.max(...xs), minY = Math.min(...ys), maxY = Math.max(...ys);
    const sx = (maxX - minX) || 1, sy = (maxY - minY) || 1;
    const sc = Math.min((VW - 2 * pad) / sx, (VH - 2 * pad) / sy);
    const ox = (VW - sc * sx) / 2, oy = (VH - sc * sy) / 2;
    const X = (p) => ox + (p[0] - minX) * sc;
    const Y = (p) => VH - (oy + (p[1] - minY) * sc);
    const PT = (p) => X(p).toFixed(1) + "," + Y(p).toFixed(1);
    const polyT = (pts) => pts.map((p) => PT(T(p))).join(" ");     // for 2180-space polys
    const polyL = (pts) => pts.map((p) => PT(p)).join(" ");        // for front-frame polys
    const ringStr = ringT.map((p) => PT(p)).join(" ");
    const bpoly = an.buildable.poly.length >= 3 ? polyT(an.buildable.poly) : null;
    const brect = build ? polyT(build.corners) : null;
    const bad = !!violation;
    const bc = build ? [(build.corners[0][0] + build.corners[2][0]) / 2, (build.corners[0][1] + build.corners[2][1]) / 2] : null;
    const kindOf = (i) => (kinds && kinds[i]) || (i === an.frontIdx ? "ulica" : "dzialka");

    // ---- direct manipulation (drag / rotate / stretch) — front-down orientation only ----
    const canEdit = editable && build && bbF;
    const ff = (e) => { const p = svgRef.current.createSVGPoint(); p.x = e.clientX; p.y = e.clientY; const l = p.matrixTransform(svgRef.current.getScreenCTM().inverse()); return [minX + (l.x - ox) / sc, minY + (VH - l.y - oy) / sc]; };
    const startDrag = (mode) => (e) => {
      if (!canEdit) { if (mode === "move" && onBeginManual) onBeginManual(); return; }
      e.stopPropagation(); try { svgRef.current.setPointerCapture(e.pointerId); } catch (x) {}
      drag.current = { mode, start: ff(e), off: Object.assign({}, manual.bOff), ang: manual.bAngle, C: [bbF.cx + manual.bOff.dx, bbF.cy + manual.bOff.dy] };
    };
    const onPM = (e) => {
      const dg = drag.current; if (!dg) return;
      const cur = ff(e);
      if (dg.mode === "move") onMove({ dx: dg.off.dx + (cur[0] - dg.start[0]), dy: dg.off.dy + (cur[1] - dg.start[1]) });
      else if (dg.mode === "rotate") onRotate(Math.round(Math.atan2(cur[1] - dg.C[1], cur[0] - dg.C[0]) * 180 / Math.PI - 90));
      else if (dg.mode === "resize") { const a = dg.ang * Math.PI / 180; const rx = cur[0] - dg.C[0], ry = cur[1] - dg.C[1]; const lx = rx * Math.cos(a) + ry * Math.sin(a); const ly = -rx * Math.sin(a) + ry * Math.cos(a); onResize(Math.max(4, Math.abs(lx) * 2), Math.max(4, Math.abs(ly) * 2)); }
    };
    const onPU = () => { drag.current = null; };

    // handle geometry (front frame)
    let handleEls = null;
    if (canEdit) {
      const a = manual.bAngle * Math.PI / 180, cs = Math.cos(a), sn = Math.sin(a);
      const Cc = [bbF.cx + manual.bOff.dx, bbF.cy + manual.bOff.dy];
      const ux = [cs, sn], uy = [-sn, cs], hw = manual.reqW / 2, hd = manual.reqD / 2;
      const corner = (sxn, syn) => [Cc[0] + ux[0] * sxn * hw + uy[0] * syn * hd, Cc[1] + ux[1] * sxn * hw + uy[1] * syn * hd];
      const corners = [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(([a1, b1]) => corner(a1, b1));
      const rotPt = [Cc[0] + uy[0] * (hd + 3.5), Cc[1] + uy[1] * (hd + 3.5)];
      const topMid = [Cc[0] + uy[0] * hd, Cc[1] + uy[1] * hd];
      handleEls = (
        <g>
          <line x1={X(topMid)} y1={Y(topMid)} x2={X(rotPt)} y2={Y(rotPt)} stroke="var(--accent-ink)" strokeWidth="1" />
          <circle cx={X(rotPt)} cy={Y(rotPt)} r="4.5" fill="var(--surface)" stroke="var(--accent-ink)" strokeWidth="1.4" style={{ cursor: "grab" }} onPointerDown={startDrag("rotate")} />
          {resizable !== false && corners.map((c, ci) => <rect key={ci} x={X(c) - 4} y={Y(c) - 4} width="8" height="8" rx="1.5" fill="var(--surface)" stroke="var(--accent-ink)" strokeWidth="1.4" style={{ cursor: "nwse-resize" }} onPointerDown={startDrag("resize")} />)}
        </g>
      );
    }

    return (
      <svg ref={svgRef} className="plan-svg" viewBox={"0 0 " + VW + " " + VH} preserveAspectRatio="xMidYMid meet" onPointerMove={onPM} onPointerUp={onPU} onPointerLeave={onPU}>
        <defs>
          <pattern id="grid" width="14" height="14" patternUnits="userSpaceOnUse"><path d="M14 0 L0 0 0 14" fill="none" stroke="var(--plan-line)" strokeWidth="0.4" opacity="0.3" /></pattern>
          <pattern id="road" width="6" height="6" patternTransform="rotate(45)" patternUnits="userSpaceOnUse"><rect width="6" height="6" fill="var(--surface-2)" /><line x1="0" y1="0" x2="0" y2="6" stroke="var(--plan-line)" strokeWidth="0.8" opacity="0.7" /></pattern>
          <marker id="arrC" markerWidth="7" markerHeight="7" refX="3.5" refY="3.5" orient="auto-start-reverse"><path d="M0.5,0.5 L6.5,3.5 L0.5,6.5 Z" fill="var(--danger)" /></marker>
        </defs>
        <rect width={VW} height={VH} fill="url(#grid)" />

        {/* neighbour context bands (what is on each side) */}
        {edgeGeo.map((g, i) => { const k = kindOf(i); const s = NB[k] || NB.dzialka; const deg0 = Math.atan2(Y(g.b) - Y(g.a), X(g.b) - X(g.a)) * 180 / Math.PI; const deg = (deg0 > 90 || deg0 < -90) ? deg0 + 180 : deg0; const c = g.bandMid; return (
          <g key={"nb" + i}>
            <polygon points={polyL(g.band)} fill={s.fill} stroke="var(--plan-line)" strokeWidth="0.4" strokeDasharray={s.dash ? "2 1.5" : "none"} opacity={s.dash ? 0.55 : 0.85} />
            <text x={X(c)} y={Y(c)} dy="2.6" textAnchor="middle" transform={"rotate(" + deg.toFixed(1) + " " + X(c).toFixed(1) + " " + Y(c).toFixed(1) + ")"} fontFamily="var(--font-mono)" fontSize="7.5" letterSpacing="0.5" fill="var(--ink-faint)">{s.label}</text>
          </g>
        ); })}

        <polygon points={ringStr} fill="var(--plan-fill)" stroke="var(--plan-ink)" strokeWidth="1.4" strokeLinejoin="round" />
        {bpoly && <polygon points={bpoly} fill="var(--plan-green)" opacity="0.7" stroke="var(--ok)" strokeWidth="1" strokeDasharray="4 2" />}
        {/* fitter layout: packed footprints (world coords) — heuristic, drawn distinct */}
        {(fitLayout || []).map((b, i) => {
          const c = T([(b.corners[0][0] + b.corners[2][0]) / 2, (b.corners[0][1] + b.corners[2][1]) / 2]);
          return (
            <g key={"fit" + i} style={{ pointerEvents: "none" }}>
              <polygon points={polyT(b.corners)} fill="var(--plan-building)" fillOpacity="0.82" stroke="var(--accent-ink)" strokeWidth="1" strokeLinejoin="round" />
              <text x={X(c)} y={Y(c)} textAnchor="middle" dy="3" fontFamily="var(--font-mono)" fontSize="8" fontWeight="700" fill="#fff">{i + 1}</text>
            </g>
          );
        })}
        {brect && (
          <g>
            <polygon points={brect} fill={bad ? "color-mix(in srgb,var(--danger) 30%,transparent)" : "var(--plan-building)"} stroke="none" style={{ cursor: canEdit ? "move" : "pointer" }} onPointerDown={startDrag("move")} />
            {buildT.map((p, i) => { const q = buildT[(i + 1) % buildT.length]; const isBad = legal && legal.badWalls && legal.badWalls[i]; const blank = wallFlags && wallFlags[i] === "blank"; return (
              <line key={"w" + i} x1={X(p)} y1={Y(p)} x2={X(q)} y2={Y(q)} stroke={isBad ? "var(--danger)" : "var(--accent-ink)"} strokeWidth={isBad ? 3 : 2} strokeDasharray={blank ? "5 3" : "none"} strokeLinecap="round" style={{ pointerEvents: "none" }} />
            ); })}
            {editable && buildT.map((p, i) => { const q = buildT[(i + 1) % buildT.length]; const m = [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2]; const isBad = legal && legal.badWalls && legal.badWalls[i]; return (
              <text key={"wl" + i} x={X(m)} y={Y(m)} dy="3" textAnchor="middle" fontFamily="var(--font-mono)" fontSize="8" fontWeight="700" fill={isBad ? "var(--danger)" : "var(--accent-ink)"} style={{ paintOrder: "stroke", stroke: "var(--surface)", strokeWidth: 2.5, strokeLinejoin: "round", pointerEvents: "none" }}>{String.fromCharCode(97 + i)}</text>
            ); })}
          </g>
        )}

        {/* parcel edges */}
        {ringT.map((a, i) => { const b = ringT[(i + 1) % n]; return (
          <line key={i} x1={X(a)} y1={Y(a)} x2={X(b)} y2={Y(b)} stroke={i === an.frontIdx ? "var(--accent)" : SIDE_COLORS[i % SIDE_COLORS.length]} strokeWidth={i === an.frontIdx ? 3.4 : 1.8} strokeLinecap="round" opacity={i === an.frontIdx ? 1 : 0.6} />
        ); })}

        {/* setback / building lines — działka: 4 m solid (z oknami) + 3 m dashed (ślepa); street: linia zabudowy; las/woda: fixed */}
        {vizMode === "edges" && edgeGeo.map((g, i) => { const k = kindOf(i); const street = k === "ulica";
          if (k === "dzialka") {
            const i4a = [g.a[0] - g.nx * 4, g.a[1] - g.ny * 4], i4b = [g.b[0] - g.nx * 4, g.b[1] - g.ny * 4];
            const i3a = [g.a[0] - g.nx * 3, g.a[1] - g.ny * 3], i3b = [g.b[0] - g.nx * 3, g.b[1] - g.ny * 3];
            return (<g key={"bl" + i}>
              <line x1={X(i4a)} y1={Y(i4a)} x2={X(i4b)} y2={Y(i4b)} stroke="var(--plan-line)" strokeWidth="1.1" opacity="0.85" />
              <line x1={X(i3a)} y1={Y(i3a)} x2={X(i3b)} y2={Y(i3b)} stroke="var(--plan-line)" strokeWidth="0.9" strokeDasharray="3 2.5" opacity="0.65" />
            </g>);
          }
          if (!(an.setbacks && an.setbacks[i] > 0.05)) return null;
          return (
            <line key={"bl" + i} x1={X(g.line[0])} y1={Y(g.line[0])} x2={X(g.line[1])} y2={Y(g.line[1])} stroke={street ? "var(--accent)" : "var(--plan-line)"} strokeWidth={street ? 1.5 : 1} strokeDasharray={street ? "6 3" : "3 2.5"} opacity={street ? 0.95 : 0.7} />
          );
        })}

        {/* v7 · stały pas — every wall projects the clear space it legally needs (okno 4 / ślepa 3; ulica/las/woda = własna linia). Red when it crosses the boundary. */}
        {vizMode === "band" && (wallViz || []).map((w) => {
          const c = w.breach ? "var(--danger)" : w.color;
          const lab = [w.mid[0] + w.nx * w.req * 0.55, w.mid[1] + w.ny * w.req * 0.55];
          return (
            <g key={"band" + w.i}>
              <polygon points={polyT(w.zone)} fill={c} fillOpacity={w.breach ? 0.24 : 0.14} stroke={c} strokeWidth={w.breach ? 1.7 : 1.2} strokeLinejoin="round" />
              <text x={X(T(lab))} y={Y(T(lab))} dy="3" textAnchor="middle" fontFamily="var(--font-mono)" fontSize="8" fontWeight="600" fill={c} style={{ paintOrder: "stroke", stroke: "var(--plan-bg)", strokeWidth: 2.4, strokeLinejoin: "round", pointerEvents: "none" }}>{nf(w.req, w.req % 1 ? 1 : 0)} m</text>
            </g>
          );
        })}

        {/* v7 · alert kolizji — silent until a wall collides; then its band pulses red with the required-distance arrow */}
        {vizMode === "alert" && (wallViz || []).filter((w) => w.breach).map((w) => {
          const s = w.mid, e = [w.mid[0] + w.nx * w.req, w.mid[1] + w.ny * w.req];
          const lab = [(s[0] + e[0]) / 2, (s[1] + e[1]) / 2];
          return (
            <g key={"al" + w.i} className="viz-alert" style={{ pointerEvents: "none" }}>
              <polygon points={polyT(w.zone)} fill="var(--danger)" fillOpacity="0.13" stroke="var(--danger)" strokeWidth="1.9" strokeLinejoin="round" />
              <line x1={X(T(s))} y1={Y(T(s))} x2={X(T(e))} y2={Y(T(e))} stroke="var(--danger)" strokeWidth="1.5" markerStart="url(#arrC)" markerEnd="url(#arrC)" />
              <rect x={X(T(lab)) - 14} y={Y(T(lab)) - 8} width="28" height="16" rx="3.5" fill="var(--plan-bg)" stroke="var(--danger)" strokeWidth="1.1" />
              <text x={X(T(lab))} y={Y(T(lab))} dy="3.6" textAnchor="middle" fontFamily="var(--font-mono)" fontSize="9" fontWeight="700" fill="var(--danger)">{nf(w.req, w.req % 1 ? 1 : 0)} m</text>
            </g>
          );
        })}

        {/* edge letters */}
        {ringT.map((a, i) => { const b = ringT[(i + 1) % n]; const m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; return (
          <text key={"l" + i} x={X(m)} y={Y(m)} dy="3.5" textAnchor="middle" fontFamily="var(--font-mono)" fontSize="9.5" fontWeight="700" fill="#fff" style={{ paintOrder: "stroke", stroke: i === an.frontIdx ? "var(--accent)" : SIDE_COLORS[i % SIDE_COLORS.length], strokeWidth: 3, strokeLinejoin: "round" }}>{String.fromCharCode(65 + i)}</text>
        ); })}

        {/* entrance gates — main = solid accent, others lighter */}
        {gates.map((gt, gi) => { const a = T(gt.g0), b = T(gt.g1); const nx = (b[1] - a[1]), ny = -(b[0] - a[0]); const nl = Math.hypot(nx, ny) || 1; const ux = nx / nl, uy = ny / nl; return (
          <g key={"g" + gi}>
            <line x1={X(a)} y1={Y(a)} x2={X(b)} y2={Y(b)} stroke="var(--accent)" strokeWidth={gt.main ? 5 : 3.4} strokeLinecap="round" opacity={gt.main ? 1 : 0.6} />
            <line x1={X(a)} y1={Y(a)} x2={X([a[0] + ux * 1.4, a[1] + uy * 1.4])} y2={Y([a[0] + ux * 1.4, a[1] + uy * 1.4])} stroke="var(--accent)" strokeWidth="1" opacity={gt.main ? 1 : 0.6} />
            <line x1={X(b)} y1={Y(b)} x2={X([b[0] + ux * 1.4, b[1] + uy * 1.4])} y2={Y([b[0] + ux * 1.4, b[1] + uy * 1.4])} stroke="var(--accent)" strokeWidth="1" opacity={gt.main ? 1 : 0.6} />
            {gt.main && <circle cx={X([(a[0] + b[0]) / 2, (a[1] + b[1]) / 2])} cy={Y([(a[0] + b[0]) / 2, (a[1] + b[1]) / 2])} r="2.4" fill="var(--accent)" stroke="var(--surface)" strokeWidth="1" />}
          </g>
        ); })}
        {handleEls}
        {brect && <text x={X(T(bc))} y={Y(T(bc))} dy="3.5" textAnchor="middle" fontFamily="var(--font-mono)" fontSize="11" fill="var(--plan-ink)" style={{ paintOrder: "stroke", stroke: "var(--plan-building)", strokeWidth: 2.5, strokeLinejoin: "round", pointerEvents: "none" }}>{nf(build.w, 1)} × {nf(build.d, 1)} m</text>}
      </svg>
    );
  }

  window.SitePlan = SitePlan;
})();
