// =====================================================================
// Catmull-Clark per-frame limit position + normal computation.
//
// Port of CatmullClarkPatches::CalcLimitPointsAndNormals
// (src/PipeLib/AmolitePipe/DcciSubdiv.cpp:8754). The implementation is
// pure stencil application: each output vertex's position is a weighted
// sum of source positions, and the normal comes from the cross product
// of two tangent vectors -- also stencil sums over the same source set
// but with different weight columns.
//
// In production this runs every frame (deformation = mutate source
// positions, re-apply stencils). In the viewer's CC mode it's called
// from runCatmullClarkPass (Phase 5) at the position+normal hook.
//
// For algorithm fidelity see CATMULL_CLARK_PLAN.md Phase 2: the loop
// shape MUST mirror amolite's so a bisect against the cc_dump
// reference is meaningful.
//
// Layout (matches amolite's VertexLimitInfo struct 1:1, dumped into
// subdiv.limitInfo by cc_dump and surfaced by ccref_loader.js):
//   - dataStart[v], dataNum[v]: per-output-vertex slice into the flat
//     influence/weight arrays.
//   - influenceData[]: flat source-vertex indices, concatenated.
//   - weightDataP[], weightDataT0[], weightDataT1[]: per-influence
//     weights for position and the two tangent vectors.
//
// Inputs:
//   limitInfo : { dataStart:Int32, dataNum:Int32, influenceData:Int32,
//                 weightDataP:Float32, weightDataT0:Float32,
//                 weightDataT1:Float32 }
//   srcPos    : Float32Array (flat [Px, Py, Pz, ...] -- length 3 * numSrcVerts)
//
// Outputs (resized in-place, allocated if undefined):
//   dstPos    : Float32Array, length 3 * numLimitVerts
//   dstNrm    : Float32Array, length 3 * numLimitVerts
//
// Returns the count of limit verts written.
// =====================================================================
function calcLimitPointsAndNormals(limitInfo, srcPos, dstPos, dstNrm) {
  const dataStart     = limitInfo.dataStart;
  const dataNum       = limitInfo.dataNum;
  const influenceData = limitInfo.influenceData;
  const weightDataP   = limitInfo.weightDataP;
  const weightDataT0  = limitInfo.weightDataT0;
  const weightDataT1  = limitInfo.weightDataT1;

  const numLimitPts = dataNum.length;
  if (dataStart.length !== numLimitPts) {
    throw new Error('cc_limit: dataStart.length ' + dataStart.length +
                    ' != dataNum.length ' + numLimitPts);
  }

  if (!dstPos || dstPos.length !== numLimitPts * 3) {
    // Caller passed wrong-size buffer -- reallocate. Steady-state caller
    // (Phase 5 runCatmullClarkPass) pre-sizes these once at scene load.
    dstPos = new Float32Array(numLimitPts * 3);
  }
  if (!dstNrm || dstNrm.length !== numLimitPts * 3) {
    dstNrm = new Float32Array(numLimitPts * 3);
  }

  for (let dstIter = 0; dstIter < numLimitPts; dstIter++) {
    const inflStart = dataStart[dstIter];
    const inflNum   = dataNum[dstIter];

    let sumPx = 0, sumPy = 0, sumPz = 0;
    let sumT0x = 0, sumT0y = 0, sumT0z = 0;
    let sumT1x = 0, sumT1y = 0, sumT1z = 0;

    for (let inflIter = 0; inflIter < inflNum; inflIter++) {
      const j = inflStart + inflIter;
      const srcIndex = influenceData[j];
      const px = srcPos[srcIndex * 3 + 0];
      const py = srcPos[srcIndex * 3 + 1];
      const pz = srcPos[srcIndex * 3 + 2];

      const wp  = weightDataP [j];
      const wt0 = weightDataT0[j];
      const wt1 = weightDataT1[j];

      sumPx  += wp  * px;  sumPy  += wp  * py;  sumPz  += wp  * pz;
      sumT0x += wt0 * px;  sumT0y += wt0 * py;  sumT0z += wt0 * pz;
      sumT1x += wt1 * px;  sumT1y += wt1 * py;  sumT1z += wt1 * pz;
    }

    // N = normalize(cross(T0, T1)). Matches amolite's
    // Vec3::Normalize(Vec3::Cross(sumT0, sumT1)) at line 8791.
    const nx = sumT0y * sumT1z - sumT0z * sumT1y;
    const ny = sumT0z * sumT1x - sumT0x * sumT1z;
    const nz = sumT0x * sumT1y - sumT0y * sumT1x;
    const len = Math.sqrt(nx*nx + ny*ny + nz*nz);
    const inv = len > 0 ? 1.0 / len : 0;

    dstPos[dstIter * 3 + 0] = sumPx;
    dstPos[dstIter * 3 + 1] = sumPy;
    dstPos[dstIter * 3 + 2] = sumPz;

    dstNrm[dstIter * 3 + 0] = nx * inv;
    dstNrm[dstIter * 3 + 1] = ny * inv;
    dstNrm[dstIter * 3 + 2] = nz * inv;
  }

  return { dstPos, dstNrm, numLimitPts };
}

// Apply deformation deltas (vertexIdx -> [dx, dy, dz]) to a base
// position array and return a fresh Float32Array. Used by the harness
// to mirror cc_dump's --deform flag: the reference dump applies the
// delta to a copy of mesh.posData before calling
// CalcLimitPointsAndNormals; the JS side does the same here so the
// diff is apples-to-apples.
function applyDeformDeltas(basePos, deltas) {
  const out = new Float32Array(basePos);
  for (const [v, d] of deltas) {
    out[v * 3 + 0] += d[0];
    out[v * 3 + 1] += d[1];
    out[v * 3 + 2] += d[2];
  }
  return out;
}

if (typeof module !== 'undefined' && module.exports) {
  module.exports = { calcLimitPointsAndNormals, applyDeformDeltas };
}
