// =====================================================================
// OBJ loader. Tri-or-quad canonical. Parses v/vn/vt/f plus the
// non-standard `t crease` / `t corner` / `t interpolateboundary`
// directives used by the OpenSubdiv test shapes. Faces with 5 or more
// corners throw -- the Catmull-Clark consumer downstream only supports
// triangles and quads, matching amolite's AmoliteMesh contract
// (m_polySizes[i] in {3, 4}).
//
// Output: array of objects, each:
//   { name, positions, normals, texcoords,
//     polygons: [{
//       corners:          [[posIdx, normIdx, texIdx], ...],  // 3 or 4
//       perimEdges:       [[posA, posB], ...],               // ring
//       longestPerimEdge: [posA, posB],
//     }, ...],
//     creaseEdges:        [[lhs, rhs, weight], ...],   // 0-indexed
//     creaseVerts:        [[idx, weight], ...],         // 0-indexed
//     interpolateBoundary: 0 | 1 | 2 | -1,              // -1 = unset
//   }
//
// Fan-triangulation has moved into buildBuiltData (topo.js). Both the
// Phong path and the Catmull-Clark path consume obj.polygons[] -- the
// loader does not emit `faces`.
//
// Crease tag syntax (from OpenSubdiv / DcciMeshUtil.cpp):
//   t crease M1/M2/M3 vLhs vRhs sharpness
//   t corner M1/M2/M3 v sharpness
//   t interpolateboundary M1/M2/M3 N
// OBJ indices are 1-based; we store 0-based.
// =====================================================================
function parseOBJ(text) {
  const positions = [], normals = [], texcoords = [];
  const objects = [];
  let cur = null;
  const ensure = () => {
    if (!cur) {
      cur = {
        name: 'default',
        polygons: [],
        creaseEdges: [],
        creaseVerts: [],
        interpolateBoundary: -1,
      };
      objects.push(cur);
    }
  };

  const lines = text.split('\n');
  for (let lineNo = 0; lineNo < lines.length; lineNo++) {
    const line = lines[lineNo].trim();
    if (!line || line[0] === '#') continue;
    const parts = line.split(/\s+/);
    const tok = parts[0];

    if (tok === 'v') {
      positions.push([+parts[1], +parts[2], +parts[3]]);
    } else if (tok === 'vn') {
      normals.push([+parts[1], +parts[2], +parts[3]]);
    } else if (tok === 'vt') {
      texcoords.push([+parts[1], +parts[2]]);
    } else if (tok === 'o') {
      cur = {
        name: parts.slice(1).join(' '),
        polygons: [],
        creaseEdges: [],
        creaseVerts: [],
        interpolateBoundary: -1,
      };
      objects.push(cur);
    } else if (tok === 'f') {
      ensure();
      const corners = [];
      for (let i = 1; i < parts.length; i++) {
        const sp = parts[i].split('/');
        const pi = +sp[0] - 1;
        const ti = (sp[1] !== undefined && sp[1] !== '') ? +sp[1] - 1 : -1;
        const ni = (sp[2] !== undefined && sp[2] !== '') ? +sp[2] - 1 : -1;
        corners.push([pi, ni, ti]);
      }
      if (corners.length < 3 || corners.length > 4) {
        throw new Error(
          'OBJ line ' + (lineNo + 1) + ': face has ' + corners.length +
          ' corners; only triangles (3) and quads (4) are supported. ' +
          'Line: ' + line);
      }
      // Perimeter edges (position pairs) in ring order + longest.
      const perimEdges = [];
      let longestLenSq = -1;
      let longestPair = null;
      for (let i = 0; i < corners.length; i++) {
        const a = corners[i][0];
        const b = corners[(i + 1) % corners.length][0];
        perimEdges.push([a, b]);
        const pa = positions[a], pb = positions[b];
        const dx = pa[0]-pb[0], dy = pa[1]-pb[1], dz = pa[2]-pb[2];
        const lsq = dx*dx + dy*dy + dz*dz;
        if (lsq > longestLenSq) { longestLenSq = lsq; longestPair = [a, b]; }
      }
      cur.polygons.push({ corners, perimEdges, longestPerimEdge: longestPair });
    } else if (tok === 't') {
      // Non-standard subdiv tags. Format:
      //   t crease M1/M2/M3 vLhs vRhs sharpness
      //   t corner M1/M2/M3 v sharpness
      //   t interpolateboundary M1/M2/M3 N
      // The M1/M2/M3 triple is metadata we ignore (matches
      // DcciMeshUtil.cpp's parser).
      ensure();
      const sub = parts[1];
      if (sub === 'crease') {
        // parts[2] = M1/M2/M3 (metadata, ignored),
        // parts[3] = vLhs, parts[4] = vRhs, parts[5] = sharpness.
        //
        // IMPORTANT: unlike OBJ's `f` directive (which is 1-indexed),
        // OpenSubdiv's `t crease` directive is 0-indexed -- the
        // OpenSubdiv test fixtures contain references to vertex 0,
        // which would be invalid in 1-indexed land. Amolite's parser
        // (FwCore/OpenDCCI/DcciMeshUtil.cpp:1616) stores the indices
        // verbatim. Match that.
        const lhs = +parts[3];
        const rhs = +parts[4];
        const w = +parts[5];
        cur.creaseEdges.push([lhs, rhs, w]);
      } else if (sub === 'corner') {
        // parts[2] = M1/M2/M3 (metadata), parts[3] = vIdx,
        // parts[4] = sharpness. Same 0-indexed convention as crease.
        const idx = +parts[3];
        const w = +parts[4];
        cur.creaseVerts.push([idx, w]);
      } else if (sub === 'interpolateboundary') {
        // parts[2] = M1/M2/M3, parts[3] = mode
        cur.interpolateBoundary = +parts[3];
      }
      // Unknown `t` subtokens are silently ignored to stay
      // forward-compatible with future tags.
    }
  }

  for (const o of objects) {
    o.positions = positions;
    o.normals   = normals;
    o.texcoords = texcoords;
  }
  return objects;
}


// =====================================================================
// Replace per-corner normals with smooth per-position normals.
//
// Walks polygons-then-triangle-fan to accumulate area-weighted face
// normals. For a quad (4 corners) the fan is (c0,c1,c2) + (c0,c2,c3);
// for a triangle (3 corners) the fan is just (c0,c1,c2). Each child
// triangle's cross product is unnormalized (length == 2x area), so
// larger faces contribute proportionally more to a shared corner --
// standard area-weighted smoothing.
//
// All objects in one parseOBJ result share the same positions array,
// so the accumulator is global across `objs`. After this function:
//   - obj.normals is per-position (size === obj.positions.length).
//   - For every polygon corner, normIdx == posIdx (the original
//     per-corner normal indices are discarded).
// =====================================================================
function smoothNormalsInPlace(objs) {
  if (objs.length === 0) return;
  const positions = objs[0].positions;
  const npos = positions.length;
  const accum = new Array(npos);
  for (let i = 0; i < npos; i++) accum[i] = [0, 0, 0];

  for (const obj of objs) {
    for (const poly of obj.polygons) {
      const c = poly.corners;
      // Fan (c[0], c[i], c[i+1]) for i = 1 .. c.length - 2.
      for (let i = 1; i < c.length - 1; i++) {
        const i0 = c[0][0];
        const i1 = c[i][0];
        const i2 = c[i+1][0];
        const fn = V3.cross(V3.sub(positions[i1], positions[i0]),
                            V3.sub(positions[i2], positions[i0]));
        accum[i0][0] += fn[0]; accum[i0][1] += fn[1]; accum[i0][2] += fn[2];
        accum[i1][0] += fn[0]; accum[i1][1] += fn[1]; accum[i1][2] += fn[2];
        accum[i2][0] += fn[0]; accum[i2][1] += fn[1]; accum[i2][2] += fn[2];
      }
    }
  }

  const smooth = accum.map(n => V3.norm(n));
  for (const obj of objs) {
    obj.normals = smooth;
    for (const poly of obj.polygons) {
      for (const corner of poly.corners) corner[1] = corner[0];
    }
  }
}
