/**
 * HATCH entity handler for dxf-parser (which ships none — dxf-parser 1.1.x drops
 * HATCH silently). Registered via the library's public `registerEntityHandler()`,
 * so dxf-parser itself stays unmodified (MIT-clean).
 *
 * Emits the shape Viewer2D's HATCH renderer consumes:
 *   entity = { type:'HATCH', solidFill:boolean, paths:[{ points:[{x,y}], bulges:[number] }] }
 * Viewer2D fills solid hatches (with island/hole nesting) and draws boundary
 * outlines; bulge arcs on polyline boundaries are honored.
 *
 * Boundary kinds handled:
 *   - polyline boundary (92 flag bit 1)      → vertices 10/20 (+ bulge 42)
 *   - edge boundary: LINE(72=1), ARC(72=2),  → tessellated to points
 *                    ELLIPSE(72=3), SPLINE(72=4, control-point polyline)
 *
 * Pattern fill (non-SOLID) is not expanded into hatch lines — Viewer2D draws the
 * boundary outline as a proxy, matching its DWG behavior.
 */
const DEG2RAD = Math.PI / 180;
const ARC_STEPS = 48;

function tessArc(cx, cy, r, a0, a1, ccw, out) {
  let span = a1 - a0;
  if (ccw) { while (span <= 0) span += Math.PI * 2; }
  else { while (span >= 0) span -= Math.PI * 2; }
  const steps = Math.max(2, Math.ceil(Math.abs(span) / (Math.PI * 2) * ARC_STEPS));
  for (let i = 0; i <= steps; i++) {
    const a = a0 + (span * i) / steps;
    out.push({ x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) });
  }
}

export default class HatchHandler {
  constructor() {
    this.ForEntityName = 'HATCH';
  }

  parseEntity(scanner, curr) {
    const entity = { type: curr.value, paths: [], solidFill: false };
    curr = scanner.next();
    while (!scanner.isEOF()) {
      if (curr.code === 0) break;
      switch (curr.code) {
        case 70: // solid fill flag
          entity.solidFill = curr.value !== 0;
          break;
        case 2: // pattern name
          entity.patternName = curr.value;
          break;
        case 8: // layer
          entity.layer = curr.value;
          break;
        case 62: // color index
          entity.colorIndex = curr.value;
          break;
        case 91: { // number of boundary paths → parse them by count
          const n = curr.value;
          let g = scanner.next();
          for (let i = 0; i < n && !scanner.isEOF(); i++) {
            g = this._readPath(scanner, entity, g);
          }
          curr = g; // g is the group following the last path's data
          continue; // curr already advanced — skip the trailing next()
        }
        default:
          break;
      }
      curr = scanner.next();
    }
    return entity;
  }

  /** Parse one boundary path starting at its 92 group; return the next group after it. */
  _readPath(scanner, entity, g) {
    // g.code should be 92 (path type flag); if not, skip until we find it.
    while (!scanner.isEOF() && g.code !== 92) g = scanner.next();
    const flag = g.value | 0;
    const isPolyline = (flag & 2) !== 0;
    const path = { points: [], bulges: [] };
    let cur = scanner.next();

    if (isPolyline) {
      let hasBulge = false;
      while (cur.code === 72 || cur.code === 73) {
        if (cur.code === 72) hasBulge = cur.value !== 0;
        cur = scanner.next();
      }
      let numVerts = 0;
      if (cur.code === 93) { numVerts = cur.value; cur = scanner.next(); }
      for (let v = 0; v < numVerts && !scanner.isEOF(); v++) {
        let x = 0, y = 0, b = 0;
        if (cur.code === 10) { x = cur.value; cur = scanner.next(); }
        if (cur.code === 20) { y = cur.value; cur = scanner.next(); }
        if (hasBulge && cur.code === 42) { b = cur.value; cur = scanner.next(); }
        path.points.push({ x, y });
        path.bulges.push(b);
      }
    } else {
      let numEdges = 0;
      if (cur.code === 93) { numEdges = cur.value; cur = scanner.next(); }
      for (let e = 0; e < numEdges && !scanner.isEOF(); e++) {
        let edgeType = 0;
        if (cur.code === 72) { edgeType = cur.value; cur = scanner.next(); }
        cur = this._readEdge(scanner, path, edgeType, cur);
      }
    }

    // Consume the per-path trailing "source boundary objects" block (97 + 330×count).
    if (cur.code === 97) {
      const srcN = cur.value | 0;
      cur = scanner.next();
      for (let s = 0; s < srcN && !scanner.isEOF(); s++) {
        if (cur.code === 330) cur = scanner.next();
        else break;
      }
    }
    if (path.points.length >= 2) entity.paths.push(path);
    return cur;
  }

  /** Tessellate one edge into path.points; return the next group. */
  _readEdge(scanner, path, edgeType, cur) {
    if (edgeType === 1) {
      // LINE: 10/20 start, 11/21 end
      let sx = 0, sy = 0, ex = 0, ey = 0;
      if (cur.code === 10) { sx = cur.value; cur = scanner.next(); }
      if (cur.code === 20) { sy = cur.value; cur = scanner.next(); }
      if (cur.code === 11) { ex = cur.value; cur = scanner.next(); }
      if (cur.code === 21) { ey = cur.value; cur = scanner.next(); }
      path.points.push({ x: sx, y: sy });
      path.points.push({ x: ex, y: ey }); // last edge's end closes the loop
    } else if (edgeType === 2) {
      // ARC: 10/20 center, 40 radius, 50 start deg, 51 end deg, 73 ccw
      let cx = 0, cy = 0, r = 0, a0 = 0, a1 = 0, ccw = 1;
      if (cur.code === 10) { cx = cur.value; cur = scanner.next(); }
      if (cur.code === 20) { cy = cur.value; cur = scanner.next(); }
      if (cur.code === 40) { r = cur.value; cur = scanner.next(); }
      if (cur.code === 50) { a0 = cur.value * DEG2RAD; cur = scanner.next(); }
      if (cur.code === 51) { a1 = cur.value * DEG2RAD; cur = scanner.next(); }
      if (cur.code === 73) { ccw = cur.value; cur = scanner.next(); }
      tessArc(cx, cy, r, a0, a1, ccw !== 0, path.points);
    } else if (edgeType === 3) {
      // ELLIPSE: 10/20 center, 11/21 major-axis endpoint, 40 ratio, 50/51 param deg, 73 ccw
      let cx = 0, cy = 0, mx = 1, my = 0, ratio = 1, a0 = 0, a1 = Math.PI * 2, ccw = 1;
      if (cur.code === 10) { cx = cur.value; cur = scanner.next(); }
      if (cur.code === 20) { cy = cur.value; cur = scanner.next(); }
      if (cur.code === 11) { mx = cur.value; cur = scanner.next(); }
      if (cur.code === 21) { my = cur.value; cur = scanner.next(); }
      if (cur.code === 40) { ratio = cur.value; cur = scanner.next(); }
      if (cur.code === 50) { a0 = cur.value * DEG2RAD; cur = scanner.next(); }
      if (cur.code === 51) { a1 = cur.value * DEG2RAD; cur = scanner.next(); }
      if (cur.code === 73) { ccw = cur.value; cur = scanner.next(); }
      const major = Math.hypot(mx, my);
      const minor = major * ratio;
      const rot = Math.atan2(my, mx);
      let span = a1 - a0;
      if (ccw !== 0) { while (span <= 0) span += Math.PI * 2; }
      else { while (span >= 0) span -= Math.PI * 2; }
      const steps = Math.max(4, Math.ceil(Math.abs(span) / (Math.PI * 2) * 72));
      for (let i = 0; i <= steps; i++) {
        const t = a0 + (span * i) / steps;
        const ex = major * Math.cos(t), ey = minor * Math.sin(t);
        path.points.push({ x: cx + ex * Math.cos(rot) - ey * Math.sin(rot), y: cy + ex * Math.sin(rot) + ey * Math.cos(rot) });
      }
    } else if (edgeType === 4) {
      // SPLINE: approximate by its control points (10/20). Skip knots/weights.
      // 94 degree, 73 rational, 74 periodic, 95 numKnots, 96 numCtrl, 40 knots, 10/20 ctrl, 42 weights
      let numKnots = 0, numCtrl = 0;
      const readInt = () => { const v = cur.value | 0; cur = scanner.next(); return v; };
      while (!scanner.isEOF()) {
        if (cur.code === 95) { numKnots = readInt(); }
        else if (cur.code === 96) { numCtrl = readInt(); }
        else if (cur.code === 94 || cur.code === 73 || cur.code === 74) { cur = scanner.next(); }
        else break;
      }
      for (let k = 0; k < numKnots && cur.code === 40 && !scanner.isEOF(); k++) cur = scanner.next();
      for (let c = 0; c < numCtrl && !scanner.isEOF(); c++) {
        let x = 0, y = 0;
        if (cur.code === 10) { x = cur.value; cur = scanner.next(); }
        if (cur.code === 20) { y = cur.value; cur = scanner.next(); }
        if (cur.code === 42) cur = scanner.next(); // weight
        path.points.push({ x, y });
      }
    } else {
      // Unknown edge type: advance one group to avoid stalling.
      cur = scanner.next();
    }
    return cur;
  }
}
