// =====================================================================
// CC render integration. Wraps Phase 1b / 2 / 3 / 4 into a runtime-
// shaped API the viewer's runVertexPass branches into:
//
//   buildCcRenderState(mesh, refJson, irregularProcLevel) -> state
//     Runs the full JS CC topology pipeline (Phase 1b end-to-end)
//     once at scene load. Takes the amolite-dumped triPatchMap
//     directly from refJson, per CATMULL_CLARK_PLAN.md Phase 1
//     ("Do NOT recompute this in JS"). Returns an opaque state
//     object.
//
//   ccBeginFrame(state, srcPositionsFlat) -> void
//     Per-frame work: re-apply limit-position stencils, rebuild the
//     reusable-CP buffer, and re-run irregular-wedge subdivision.
//     Mutates state in place. Call once per source-position update
//     (no-op if source positions are unchanged).
//
//   ccEvalPosNrm(state, triId, w0, w1, w2) -> { P, N }
//     Per-tessellated-vertex lookup. Maps (triId, bary) -> (patchIter,
//     patchUv) via state.triPatchMap, then routes through
//     evalPatchQuadOrTri.
//
// The viewer's runVertexPass branches: when STATE.subdivMode === 'cc'
// the per-vertex (P, N) comes from ccEvalPosNrm; the rest of the
// vertex-pass machinery (UV interpolation, displacement push, pool
// writes) is unchanged.
'use strict';

// Hardcoded ceiling for the irregular-proc level slider. The wedge GPU
// buffer, topology blob's IrregularInfo wedgeStartPoint field, and
// the per-wedge GPU dstOffset are all sized as if the level were
// always MAX_IRREG_LEVEL. The runtime level only drives a single
// per-wedge irregularLevel uniform field; no buffer reallocation,
// blob repack, or ctx recreation needed when the slider moves.
const MAX_IRREG_LEVEL = 8;

// Module-private aliases (per-file unique names so multiple <script>
// tags in browser scope don't collide on a shared `cc_top` binding).
let cc_top_rd, cc_pe_rd;
if (typeof require !== 'undefined') {
  cc_top_rd = require('./cc_topology.js');
  cc_pe_rd  = require('./cc_patcheval.js');
} else {
  // Browser: same const-vs-globalThis pattern as cc_patcheval.js --
  // overlay the script-scope `const`s onto a facade.
  cc_top_rd = Object.assign({}, globalThis, {
    kSubdivPolyType_QuadRegular, kSubdivPolyType_QuadIrregular1,
    kSubdivPolyType_QuadIrregular2, kSubdivPolyType_Triangle,
    kRegionScale_00, kRegionScale_01, kRegionScale_10, kRegionScale_11,
    kRegionScale_Full, kRegionScale_Num,
    SUBDIV_MAX_SHARPNESS, IRREGULAR_GROUP_SIZE,
  });
  cc_pe_rd = globalThis;
}

// ---------------------------------------------------------------------
// buildCcRenderState: one-shot scene-load work.
//
// `mesh` is the same shape the rest of the JS port consumes (see
// test-cc-*.js for how it's built from parseOBJ output): flat
// m_posData, m_polySizes / m_polyIndexList, etc. Mutated in place by
// prepMeshForCatmullClark.
//
// `refJson` is the parsed __cc_ref/<scene>__bind.json. We pull
// triPatchMap from it directly -- amolite's TopoData pipeline tracks
// this per CATMULL_CLARK_PLAN.md, and the JS port doesn't recompute.
//
// Returns an opaque state object holding everything ccBeginFrame +
// ccEvalPosNrm need.
function buildCcRenderState(mesh, refJson, irregularProcLevel) {
  cc_top_rd.prepMeshForCatmullClark(mesh);

  const topo0 = cc_top_rd.makeDcciMeshSubdivTopo();
  cc_top_rd.makeSubdivTopoFromMeshVtx(topo0, mesh, 0);

  const helper0 = cc_top_rd.makeCatmullClarkTopoHelper();
  const topo1   = cc_top_rd.makeDcciMeshSubdivTopo();
  const dstWeights1 = [];
  cc_top_rd.applyCatmullClarkToWeights(topo1, helper0, dstWeights1, topo0, true, 0);

  const helper1 = cc_top_rd.makeCatmullClarkTopoHelper();
  const topo2   = cc_top_rd.makeDcciMeshSubdivTopo();
  const dstWeights2 = [];
  cc_top_rd.applyCatmullClarkToWeights(topo2, helper1, dstWeights2, topo1, true, 0);

  const numBaseVerts = helper0.m_numVerts;
  const posWeights0  = new Array(numBaseVerts);
  for (let i = 0; i < numBaseVerts; i++) {
    posWeights0[i] = cc_top_rd.makeFlatWeightVertexValId(1.0, i);
  }
  const posWeights2 = [];
  cc_top_rd.convolveWeights(posWeights2, dstWeights1, dstWeights2, true);

  const patches = cc_top_rd.makeCatmullClarkPatches();
  cc_top_rd.buildCalcVertexInfo(patches, helper0);
  cc_top_rd.buildCalcPolyInfo(patches, helper0);
  cc_top_rd.buildCalcQuadRegionData(patches, helper0);
  cc_top_rd.buildVertexIrregularData(patches, helper0);
  cc_top_rd.buildTriRegionData(patches, helper0);
  cc_top_rd.collectPointsForPatches(patches, topo0, helper0, topo1, helper1);
  cc_top_rd.collectPointsForIrregular(patches, topo0, helper0, topo1, helper1);
  cc_top_rd.buildRuntimePoints(patches, helper0, posWeights0, dstWeights1, posWeights2);
  cc_top_rd.buildRuntimeIrregularInfo(patches, helper0);
  cc_top_rd.buildLimitPointsInfo(patches, helper0);

  // Per-wedge layout sized for MAX_IRREG_LEVEL so the slider can change
  // the live irregularProcLevel without reallocating GPU buffers,
  // recreating ctxes, rebuilding the topology blob, or invalidating
  // cached bind groups. Per-frame ccBeginFrame still writes only
  // ceil(irregularProcLevel) slices per wedge -- the trailing
  // (MAX_IRREG_LEVEL - ceil(level)) slices stay zero and are never
  // read by patch eval (which clamps sliceLevelForGridCalc to
  // ceil(level) - 1).
  //
  // wedgeLevels[i] still tracks the RUNTIME level (gets read by the
  // CPU patch eval as patchSliceLevel). The GPU side reads its own
  // copy out of the IrregularInfo table in topology.gpuBlob.
  const w = patches.m_weightInflData;
  const numWedges = patches.m_irregularWedgeInfo.length;
  const wedgeLevels      = new Array(numWedges).fill(irregularProcLevel);
  const wedgeStartPoints = new Array(numWedges);
  const wedgeNumDstPoints= new Array(numWedges);
  let totalWedgePts = 0;
  for (let i = 0; i < numWedges; i++) {
    const rii = w.m_runtimeIrregularInfo[i];
    const numPts = cc_top_rd.calcNumDstPointsForWedge(rii.m_valence, !!rii.m_isBorder, MAX_IRREG_LEVEL);
    wedgeStartPoints[i] = totalWedgePts;
    wedgeNumDstPoints[i] = numPts;
    totalWedgePts += numPts;
  }
  const wedgeDstPointsFlat = new Float32Array(totalWedgePts * 3);

  // Build the meshData wrapper the patch eval expects.
  const meshData = {
    m_subdivData: {
      m_polyInfo:                patches.m_polyInfo,
      m_regionData:              patches.m_regionData,
      m_irregularWedgeInfo:      patches.m_irregularWedgeInfo,
      m_weightInflData:          patches.m_weightInflData,
      m_vertexInfo:              patches.m_vertexInfo,
      m_numRegularPoints:        patches.m_numRegularPoints,
      m_numIrregularVertexWedges:patches.m_numIrregularVertexWedges,
      m_numIrregularTriangles:   patches.m_numIrregularTriangles,
    },
  };

  // Pull triPatchMap from the reference JSON. Each entry is
  // {patchIter, rotation, uvBasis[6]}. We don't re-interpret in JS;
  // amolite already canonicalized this for us. The runtime uses
  // uvBasis directly via affine combination of bary weights.
  if (!refJson || !refJson.subdiv || !Array.isArray(refJson.subdiv.triPatchMap)) {
    throw new Error('buildCcRenderState: refJson missing triPatchMap');
  }
  const triPatchMap = refJson.subdiv.triPatchMap;

  return {
    mesh,
    helper0,
    patches,
    meshData,
    triPatchMap,
    irregularProcLevel,
    wedgeLevels, wedgeStartPoints, wedgeNumDstPoints,
    wedgeDstPointsFlat,
    // Populated by ccBeginFrame.
    limitPosData:     null,
    limitNrmData:     null,
    reusableCpData:   null,
    fullWedgeDstPoints: null,
    // Per-frame epoch -- bumps each ccBeginFrame so callers can detect
    // "did the frame actually refresh" without re-running the chain.
    frameEpoch:       0,
  };
}

// ---------------------------------------------------------------------
// ccBeginFrame: refresh per-frame data.
//
// srcPositionsFlat is a Float32Array of 3*N (xyz per source vertex)
// AFTER prepMeshForCatmullClark's UV-id duplication. The viewer
// passes the deformed source positions here once per rebuild.
function ccBeginFrame(state, srcPositionsFlat) {
  const { patches, helper0, wedgeLevels, wedgeStartPoints, wedgeDstPointsFlat } = state;
  const w = patches.m_weightInflData;

  // Apply VertexLimitInfo stencils -> limit positions + normals.
  // (Phase 2's runCcLimitPass inlined here so the runtime doesn't
  // need a separate module.)
  const li = patches.m_vertexLimitInfo;
  const numLimitPoints = li.m_limitDataStart.length;
  const limitPosData = new Array(numLimitPoints);
  const limitNrmData = new Array(numLimitPoints);
  for (let v = 0; v < numLimitPoints; v++) {
    const start = li.m_limitDataStart[v];
    const num   = li.m_limitDataNum[v];
    let px = 0, py = 0, pz = 0;
    let t0x = 0, t0y = 0, t0z = 0;
    let t1x = 0, t1y = 0, t1z = 0;
    for (let k = 0; k < num; k++) {
      const infl = li.m_limitInfluenceData[start + k];
      const wp   = li.m_limitWeightDataP [start + k];
      const wt0  = li.m_limitWeightDataT0[start + k];
      const wt1  = li.m_limitWeightDataT1[start + k];
      const sx = srcPositionsFlat[infl*3+0];
      const sy = srcPositionsFlat[infl*3+1];
      const sz = srcPositionsFlat[infl*3+2];
      px += sx*wp;  py += sy*wp;  pz += sz*wp;
      t0x+= sx*wt0; t0y+= sy*wt0; t0z+= sz*wt0;
      t1x+= sx*wt1; t1y+= sy*wt1; t1z+= sz*wt1;
    }
    limitPosData[v] = [px, py, pz];
    const cx = t0y*t1z - t0z*t1y;
    const cy = t0z*t1x - t0x*t1z;
    const cz = t0x*t1y - t0y*t1x;
    const m  = Math.sqrt(cx*cx + cy*cy + cz*cz);
    limitNrmData[v] = m > 0 ? [cx/m, cy/m, cz/m] : [0, 0, 0];
  }

  // Reusable CP data: one Vec3 per runtime CP.
  const reusableCpData = cc_pe_rd.ccBeginFrameComputeReusableCpData(w, srcPositionsFlat);

  // Per-frame irregular-wedge subdivision -- writes into
  // wedgeDstPointsFlat (re-used buffer; no allocation per frame).
  wedgeDstPointsFlat.fill(0);
  for (let groupIter = 0; groupIter < w.m_irregularGroupNum.length; groupIter++) {
    const wedgeDecoder = cc_top_rd.makeIrregularWedgeDecoder();
    const grpStart = w.m_irregularGroupStart[groupIter];
    const grpNum   = w.m_irregularGroupNum[groupIter];
    const framePtsOffsetPerGroup = new Array(grpNum);
    for (let p = 0; p < grpNum; p++) {
      framePtsOffsetPerGroup[p] = w.m_runtimeIrregularInfo[grpStart + p].m_pointStart;
    }
    // Repack reusableCpData (Array of [x,y,z]) into flat for the
    // decoder (which reads via Float32Array indexing).
    const reusableCpDataFlat = new Float32Array(reusableCpData.length * 3);
    for (let i = 0; i < reusableCpData.length; i++) {
      const v = reusableCpData[i];
      reusableCpDataFlat[i*3+0] = v[0]; reusableCpDataFlat[i*3+1] = v[1]; reusableCpDataFlat[i*3+2] = v[2];
    }
    cc_top_rd.irregularWedgeInitDecoderVec3(
      wedgeDecoder, w,
      reusableCpDataFlat, framePtsOffsetPerGroup, wedgeLevels, groupIter);
    for (let iter = 0; iter < wedgeDecoder.m_maxNumSlices; iter++) {
      cc_top_rd.irregularWedgeRunIteration(wedgeDecoder, iter);
      cc_top_rd.irregularWedgeSavePointsVec3(wedgeDecoder, wedgeDstPointsFlat, wedgeStartPoints, grpStart, iter);
    }
  }

  // Repack wedgeDstPointsFlat into Array of [x,y,z] for evaluateIrregular
  // PointFull, which indexes allPts[i] expecting Vec3-like.
  const fullWedgeDstPoints = new Array(wedgeDstPointsFlat.length / 3);
  for (let i = 0; i < fullWedgeDstPoints.length; i++) {
    fullWedgeDstPoints[i] = [
      wedgeDstPointsFlat[i*3+0],
      wedgeDstPointsFlat[i*3+1],
      wedgeDstPointsFlat[i*3+2],
    ];
  }

  state.limitPosData       = limitPosData;
  state.limitNrmData       = limitNrmData;
  state.reusableCpData     = reusableCpData;
  state.fullWedgeDstPoints = fullWedgeDstPoints;
  state.frameEpoch++;
}

// ---------------------------------------------------------------------
// ccEvalPosNrm: per-tessellated-vertex lookup.
//
// `triId` is the GLOBAL index into the rendered triangle stream (post
// fan-triangulation). `w0/w1/w2` are barycentric weights for that tri
// (corner 0 / 1 / 2 weights respectively, summing to 1).
//
// state.triPatchMap[triId] gives {patchIter, uvBasis[6]} -- the affine
// basis amolite uses to map tri bary into the source patch's
// parametric domain. For a quad source poly: patch UV in [0,1]^2.
// For a triangle source poly: patch bary (the same triangular
// parametrization).
function ccEvalPosNrm(state, triId, w0, w1, w2) {
  const m = state.triPatchMap[triId];
  if (!m) {
    throw new Error('ccEvalPosNrm: no triPatchMap entry for triId ' + triId);
  }
  const ub = m.uvBasis;  // [u0, v0, u1, v1, u2, v2]
  const u  = w0 * ub[0] + w1 * ub[2] + w2 * ub[4];
  const v  = w0 * ub[1] + w1 * ub[3] + w2 * ub[5];

  const w = state.patches.m_weightInflData;
  const r = cc_pe_rd.evalPatchQuadOrTri(
    state.meshData, m.patchIter, [u, v],
    state.limitPosData, state.limitNrmData,
    w.m_regularAndIrregularPoints,
    state.reusableCpData,
    state.fullWedgeDstPoints,
    state.wedgeNumDstPoints, state.wedgeStartPoints, state.wedgeLevels);
  return { P: r.calcP, N: r.calcN };
}

if (typeof module !== 'undefined' && module.exports) {
  module.exports = {
    buildCcRenderState,
    ccBeginFrame,
    ccEvalPosNrm,
    MAX_IRREG_LEVEL,
  };
}
