// FloorCanvas — wall graph + room (face) detection
//
// Pipeline:
//   1. snapEndpoints(walls, tol) — merge wall endpoints within `tol` feet into shared node positions.
//   2. buildGraph(walls) — node list + adjacency. Each wall becomes two directed half-edges.
//   3. findFaces(graph) — planar face traversal: at each node, sort incident edges by angle.
//      Each directed edge has a unique "next" = the edge whose reverse-angle is the next one
//      clockwise around the destination node. Walking next.next.next… returns to start, tracing a face.
//   4. The outer face has negative signed area (CW) when interior faces are CCW (or vice versa).
//      We filter out the most-negative-area face and any tiny degenerate cycles.

const SNAP_TOL = 0.4;     // feet — endpoints within this distance fuse
const MIN_ROOM_AREA = 4;  // sq ft — filter dust

// Snap a list of wall shapes so endpoints that are within tol of each other share coords.
function snapWallEndpoints(walls, tol = SNAP_TOL) {
  if (!walls.length) return walls;
  // Collect all endpoints
  const pts = [];
  walls.forEach((w, i) => { pts.push({ x: w.a.x, y: w.a.y, idx: i, end: 'a' }); pts.push({ x: w.b.x, y: w.b.y, idx: i, end: 'b' }); });
  // Greedy cluster: for each point, find earlier point within tol and merge.
  const clusters = []; // {x, y, members: [{idx, end}]}
  for (const p of pts) {
    let best = -1, bestD = tol;
    for (let i = 0; i < clusters.length; i++) {
      const d = Math.hypot(clusters[i].x - p.x, clusters[i].y - p.y);
      if (d < bestD) { bestD = d; best = i; }
    }
    if (best >= 0) {
      const c = clusters[best];
      c.members.push({ idx: p.idx, end: p.end });
      // running average
      c.x = (c.x * (c.members.length - 1) + p.x) / c.members.length;
      c.y = (c.y * (c.members.length - 1) + p.y) / c.members.length;
    } else {
      clusters.push({ x: p.x, y: p.y, members: [{ idx: p.idx, end: p.end }] });
    }
  }
  // Materialize snapped walls
  const out = walls.map((w) => ({ ...w, a: { ...w.a }, b: { ...w.b } }));
  for (const c of clusters) {
    for (const m of c.members) {
      out[m.idx][m.end] = { x: c.x, y: c.y };
    }
  }
  // Drop degenerate (zero-length) walls
  return out.filter((w) => Math.hypot(w.b.x - w.a.x, w.b.y - w.a.y) > 0.05);
}

// Build planar graph: nodes (deduplicated points) + edges.
function buildGraph(walls) {
  const nodes = []; // {x, y, edges: [{to, angle, edgeId, reverseId}]}
  const nodeIndex = new Map();
  const keyFor = (p) => `${p.x.toFixed(3)},${p.y.toFixed(3)}`;
  const getNode = (p) => {
    const k = keyFor(p);
    if (nodeIndex.has(k)) return nodeIndex.get(k);
    const idx = nodes.length;
    nodes.push({ x: p.x, y: p.y, edges: [] });
    nodeIndex.set(k, idx);
    return idx;
  };

  // Half-edges: each undirected wall → 2 directed edges (id 2k, 2k+1 are paired).
  const halfEdges = [];
  walls.forEach((w) => {
    const ai = getNode(w.a);
    const bi = getNode(w.b);
    if (ai === bi) return;
    const ang1 = Math.atan2(nodes[bi].y - nodes[ai].y, nodes[bi].x - nodes[ai].x);
    const ang2 = Math.atan2(nodes[ai].y - nodes[bi].y, nodes[ai].x - nodes[bi].x);
    const e1 = halfEdges.length;
    halfEdges.push({ from: ai, to: bi, angle: ang1, twin: e1 + 1 });
    halfEdges.push({ from: bi, to: ai, angle: ang2, twin: e1 });
    nodes[ai].edges.push(e1);
    nodes[bi].edges.push(e1 + 1);
  });

  // For each node, sort incident edges by their outgoing angle, ascending.
  for (const n of nodes) {
    n.edges.sort((a, b) => halfEdges[a].angle - halfEdges[b].angle);
  }
  return { nodes, halfEdges };
}

// For directed half-edge `e` (from → to), the next edge in a face traversal is:
// the half-edge leaving `to` whose angle is immediately clockwise of the reverse of e.
function nextEdgeInFace(graph, eId) {
  const { halfEdges, nodes } = graph;
  const e = halfEdges[eId];
  const twin = halfEdges[e.twin];
  const node = nodes[e.to]; // we arrived here; outgoing edges live here
  // Find twin.id position in node.edges
  const sorted = node.edges; // sorted by ascending angle
  const pos = sorted.indexOf(e.twin);
  if (pos < 0) return -1;
  // Next clockwise = previous in ascending-angle order (wrap)
  const nextPos = (pos - 1 + sorted.length) % sorted.length;
  return sorted[nextPos];
}

function findFaces(graph) {
  const { halfEdges, nodes } = graph;
  const visited = new Set();
  const faces = [];
  for (let i = 0; i < halfEdges.length; i++) {
    if (visited.has(i)) continue;
    const cycle = [];
    let cur = i;
    let guard = 0;
    while (!visited.has(cur) && guard++ < 1000) {
      visited.add(cur);
      cycle.push(cur);
      cur = nextEdgeInFace(graph, cur);
      if (cur < 0) { cycle.length = 0; break; }
      if (cur === i) break;
    }
    if (cycle.length >= 3) {
      const pts = cycle.map((eId) => {
        const n = nodes[halfEdges[eId].from];
        return { x: n.x, y: n.y };
      });
      faces.push({ pts, edges: cycle });
    }
  }
  return faces;
}

function signedArea(pts) {
  let a = 0;
  for (let i = 0; i < pts.length; i++) {
    const j = (i + 1) % pts.length;
    a += pts[i].x * pts[j].y - pts[j].x * pts[i].y;
  }
  return a / 2;
}

function centroid(pts) {
  let cx = 0, cy = 0, a = 0;
  for (let i = 0; i < pts.length; i++) {
    const j = (i + 1) % pts.length;
    const cross = pts[i].x * pts[j].y - pts[j].x * pts[i].y;
    a += cross;
    cx += (pts[i].x + pts[j].x) * cross;
    cy += (pts[i].y + pts[j].y) * cross;
  }
  a /= 2;
  if (Math.abs(a) < 1e-6) {
    // fallback: simple average
    const n = pts.length;
    let sx = 0, sy = 0; for (const p of pts) { sx += p.x; sy += p.y; }
    return { x: sx / n, y: sy / n };
  }
  cx /= (6 * a); cy /= (6 * a);
  return { x: cx, y: cy };
}

// Split walls at T-junctions: for every endpoint of any wall, if it lies on the
// interior of another wall segment within tol, split that wall in two at the point.
// Iterate until no more splits happen (a wall can be split multiple times).
function splitAtTJunctions(walls, tol = SNAP_TOL) {
  let out = walls.map((w) => ({ ...w, a: { ...w.a }, b: { ...w.b } }));
  let changed = true;
  let guard = 0;
  while (changed && guard++ < 20) {
    changed = false;
    // collect endpoints
    const pts = [];
    out.forEach((w) => { pts.push({ x: w.a.x, y: w.a.y }); pts.push({ x: w.b.x, y: w.b.y }); });
    const next = [];
    for (const w of out) {
      const A = w.a, B = w.b;
      const ABx = B.x - A.x, ABy = B.y - A.y;
      const len = Math.hypot(ABx, ABy);
      if (len < 1e-3) { next.push(w); continue; }
      // find the closest endpoint that lies strictly in the interior of this segment
      let splitPt = null, splitT = 0;
      for (const p of pts) {
        const t = ((p.x - A.x) * ABx + (p.y - A.y) * ABy) / (len * len);
        if (t < 0.02 || t > 0.98) continue; // skip near-endpoint
        const cx = A.x + t * ABx, cy = A.y + t * ABy;
        const d = Math.hypot(p.x - cx, p.y - cy);
        if (d < tol) {
          // pick the point closest to wall's midpoint to make stable choice
          if (!splitPt || Math.abs(t - 0.5) < Math.abs(splitT - 0.5)) {
            splitPt = { x: p.x, y: p.y }; splitT = t;
          }
        }
      }
      if (splitPt) {
        next.push({ ...w, id: w.id, a: A, b: splitPt });
        next.push({ ...w, id: w.id + '-s' + guard, a: splitPt, b: B });
        changed = true;
      } else {
        next.push(w);
      }
    }
    out = next;
  }
  return out;
}

// Pure helper: walls → snapped+split walls + detected rooms (face polygons).
function detectRooms(walls) {
  const snapped = snapWallEndpoints(walls);
  const split = splitAtTJunctions(snapped);
  const graph = buildGraph(split);
  const faces = findFaces(graph);
  // Each face has a signed area. In a planar graph traversed via "next clockwise around endpoint",
  // interior faces come out clockwise (negative area in standard math y-up); the outer boundary
  // comes out counter-clockwise (positive). Our SVG y is screen-down, which flips the sign — but
  // the discriminating principle holds: the outer face has the *largest absolute area* and
  // opposite sign from the interior majority.
  const withArea = faces.map((f) => ({ ...f, area: signedArea(f.pts) }));
  // Drop the face with the largest absolute area (outer boundary).
  let outerIdx = -1, outerAbs = 0;
  withArea.forEach((f, i) => { if (Math.abs(f.area) > outerAbs) { outerAbs = Math.abs(f.area); outerIdx = i; } });
  const interior = withArea.filter((f, i) => i !== outerIdx && Math.abs(f.area) >= MIN_ROOM_AREA);
  // Sort by area descending so larger rooms render first (UI niceness)
  interior.sort((a, b) => Math.abs(b.area) - Math.abs(a.area));
  return { snappedWalls: snapped, rooms: interior.map((f) => ({
    pts: f.pts,
    area: Math.abs(f.area),
    centroid: centroid(f.pts),
  })) };
}

// Group walls by thickness so each group renders as one path with mitered joins.
// We chain segments that share endpoints into polyline runs — that's what produces
// the crisp corner. Walls that don't share endpoints become solo 2-point paths.
function buildWallPaths(walls) {
  // Group by thickness
  const byThickness = new Map();
  walls.forEach((w, i) => {
    const t = (w.thickness || 0.5).toFixed(3);
    if (!byThickness.has(t)) byThickness.set(t, []);
    byThickness.get(t).push({ ...w, _i: i });
  });

  const groups = [];
  for (const [tStr, ws] of byThickness.entries()) {
    const thickness = +tStr;
    // Build adjacency by endpoint position
    const keyFor = (p) => `${p.x.toFixed(3)},${p.y.toFixed(3)}`;
    const adj = new Map(); // key → [{wallIdx, otherEnd}]
    ws.forEach((w, idx) => {
      const ka = keyFor(w.a), kb = keyFor(w.b);
      if (!adj.has(ka)) adj.set(ka, []);
      if (!adj.has(kb)) adj.set(kb, []);
      adj.get(ka).push({ idx, end: 'a' });
      adj.get(kb).push({ idx, end: 'b' });
    });

    // Eulerian-ish chaining: pick a wall, walk both directions until dead end or branch.
    // A "chain" continues only if the next node has exactly 2 incident walls (us + one other).
    const used = new Set();
    const chains = [];
    for (let start = 0; start < ws.length; start++) {
      if (used.has(start)) continue;
      const seq = [{ idx: start, dir: 1 }];
      used.add(start);
      // Walk forward (from b)
      let curIdx = start, curDir = 1;
      while (true) {
        const w = ws[curIdx];
        const endPt = curDir === 1 ? w.b : w.a;
        const k = keyFor(endPt);
        const list = adj.get(k) || [];
        if (list.length !== 2) break;
        const nextRef = list.find((x) => x.idx !== curIdx);
        if (!nextRef || used.has(nextRef.idx)) break;
        used.add(nextRef.idx);
        const nextDir = nextRef.end === 'a' ? 1 : -1;
        seq.push({ idx: nextRef.idx, dir: nextDir });
        curIdx = nextRef.idx; curDir = nextDir;
      }
      // Walk backward (from a of original)
      curIdx = start; curDir = 1;
      while (true) {
        const w = ws[curIdx];
        const startPt = curDir === 1 ? w.a : w.b;
        const k = keyFor(startPt);
        const list = adj.get(k) || [];
        if (list.length !== 2) break;
        const prevRef = list.find((x) => x.idx !== curIdx);
        if (!prevRef || used.has(prevRef.idx)) break;
        used.add(prevRef.idx);
        const prevDir = prevRef.end === 'b' ? 1 : -1;
        seq.unshift({ idx: prevRef.idx, dir: prevDir });
        curIdx = prevRef.idx; curDir = prevDir;
      }
      chains.push(seq);
    }
    // Convert chains → point sequences
    const polylines = chains.map((seq) => {
      const pts = [];
      seq.forEach((step, i) => {
        const w = ws[step.idx];
        const A = step.dir === 1 ? w.a : w.b;
        const B = step.dir === 1 ? w.b : w.a;
        if (i === 0) pts.push(A);
        pts.push(B);
      });
      // detect closed ring
      const first = pts[0], last = pts[pts.length - 1];
      const closed = pts.length > 2 && Math.hypot(first.x - last.x, first.y - last.y) < 1e-3;
      return { pts, closed, wallIds: seq.map((s) => ws[s.idx].id) };
    });
    groups.push({ thickness, polylines });
  }
  return groups;
}

window.FCRooms = { detectRooms, buildWallPaths, snapWallEndpoints, SNAP_TOL };
