// =====================================================================
// Catmull-Clark reference-dump loader. Reads the JSON the cc-dump
// C++ tool writes and exposes typed-array views over the per-vertex
// position/normal blobs + per-sample (P, N) pairs.
//
// The dump schema is defined in CATMULL_CLARK_PLAN.md (Phase 0c).
// `version` is the gate -- any mismatch must crash loud so reference
// JSONs from older dumper builds don't silently feed a newer harness.
// =====================================================================
const CCREF_SCHEMA_VERSION = 17;

async function loadCcReference(url) {
  const res = await fetch(url);
  if (!res.ok) {
    throw new Error('loadCcReference: fetch ' + url + ' -> ' + res.status);
  }
  const raw = await res.json();
  return wrapCcReference(raw, url);
}

// Same accessor surface, but for Node-side tests / diff scripts that
// already have the parsed JSON in hand.
function wrapCcReference(raw, sourceLabel) {
  if (!raw || typeof raw !== 'object') {
    throw new Error('ccref: ' + sourceLabel + ' is not an object');
  }
  if (raw.version !== CCREF_SCHEMA_VERSION) {
    throw new Error('ccref: ' + sourceLabel +
                    ' schema version ' + raw.version +
                    ' != expected ' + CCREF_SCHEMA_VERSION);
  }
  // Pre-build typed-array views for the hot fields. The JSON parser
  // returns plain JS arrays; converting to Float32Array up front keeps
  // diff loops cache-friendly and means callers don't accidentally
  // mutate the source.
  const mesh = raw.mesh || {};
  const subdiv = raw.subdiv || {};
  const frame = raw.frame || {};
  const samples = raw.samples || [];

  function toF32(arr, name) {
    if (!arr) return null;
    if (!Array.isArray(arr)) {
      throw new Error('ccref: ' + sourceLabel + '.' + name + ' is not an array');
    }
    return new Float32Array(arr);
  }
  function toI32(arr, name) {
    if (!arr) return null;
    if (!Array.isArray(arr)) {
      throw new Error('ccref: ' + sourceLabel + '.' + name + ' is not an array');
    }
    return new Int32Array(arr);
  }

  // Sample (P, N) blobs: pack into flat Float32Arrays for fast diff.
  // Skipped sentinels (s.skipped is set) carry no P/N -- they encode
  // amolite's "this corner has 2+ semi-sharp edges and the patch
  // evaluator can't handle it" limitation. We surface them as a
  // separate skipped[] array so callers can decide whether to honor
  // the limitation or do better.
  let samplePos = null, sampleNrm = null;
  let sampleIter = null;
  let sampleUv  = null;
  const okSamples = [];
  const skippedSamples = [];
  for (let i = 0; i < samples.length; i++) {
    const s = samples[i];
    if (s.skipped) skippedSamples.push(s);
    else           okSamples.push(s);
  }
  if (okSamples.length > 0) {
    samplePos  = new Float32Array(okSamples.length * 3);
    sampleNrm  = new Float32Array(okSamples.length * 3);
    sampleIter = new Int32Array(okSamples.length);
    sampleUv   = new Float32Array(okSamples.length * 2);
    for (let i = 0; i < okSamples.length; i++) {
      const s = okSamples[i];
      samplePos[i*3 + 0] = s.P[0]; samplePos[i*3 + 1] = s.P[1]; samplePos[i*3 + 2] = s.P[2];
      sampleNrm[i*3 + 0] = s.N[0]; sampleNrm[i*3 + 1] = s.N[1]; sampleNrm[i*3 + 2] = s.N[2];
      sampleIter[i] = s.iter;
      sampleUv[i*2 + 0] = s.u; sampleUv[i*2 + 1] = s.v;
    }
  }

  return {
    version: raw.version,
    source: raw.source || null,
    mesh: {
      posData:        toF32(mesh.posData,        'mesh.posData'),
      polySizes:      toI32(mesh.polySizes,      'mesh.polySizes'),
      polyIndexList:  toI32(mesh.polyIndexList,  'mesh.polyIndexList'),
      uvData:         toF32(mesh.uvData,         'mesh.uvData'),
      uvIds:          toI32(mesh.uvIds,          'mesh.uvIds'),
      creaseEdges:    mesh.creaseEdges || [],
      creaseVerts:    mesh.creaseVerts || [],
    },
    subdiv: {
      patchCount:           subdiv.patchCount | 0,
      irregularWedgeCount:  subdiv.irregularWedgeCount | 0,
      // limitInfo -- 1:1 match for amolite's VertexLimitInfo. The
      // Phase-1 JS topology port re-creates these; the Phase-2 stencil
      // application consumes them.
      limitInfo: subdiv.limitInfo ? {
        dataStart:     toI32(subdiv.limitInfo.dataStart,     'subdiv.limitInfo.dataStart'),
        dataNum:       toI32(subdiv.limitInfo.dataNum,       'subdiv.limitInfo.dataNum'),
        influenceData: toI32(subdiv.limitInfo.influenceData, 'subdiv.limitInfo.influenceData'),
        weightDataP:   toF32(subdiv.limitInfo.weightDataP,   'subdiv.limitInfo.weightDataP'),
        weightDataT0:  toF32(subdiv.limitInfo.weightDataT0,  'subdiv.limitInfo.weightDataT0'),
        weightDataT1:  toF32(subdiv.limitInfo.weightDataT1,  'subdiv.limitInfo.weightDataT1'),
      } : null,
      // vertexInfo[] / polyInfo[] -- per-vertex / per-polygon
      // classification output of BuildCalcVertexInfo /
      // BuildCalcPolyInfo. Surfaces v3 fields; Phase 1b consumes
      // these as oracle for the JS classifier ports.
      vertexInfo: subdiv.vertexInfo || [],
      polyInfo:   subdiv.polyInfo   || [],
      // edgeLookup -- 1:1 match for amolite's EdgeLookup struct.
      // Per-edge identity, face counts, sharpness, hard-edge flags,
      // and the per-edge face list (PolyCorner[]). Phase 1b.2 JS
      // port validates buildEdgeLookup against this.
      edgeLookup: subdiv.edgeLookup || null,
      // adjVertData / adjFaceData -- per-vertex adjacency tables.
      // adjVert: neighboring vertices; adjFace: incident polygons.
      // Phase 1b.3 JS port validates buildVertAdjData / buildFaceAdjData
      // against these.
      adjVertData: subdiv.adjVertData || null,
      adjFaceData: subdiv.adjFaceData || null,
      // vertIsBorder / edgeIsBorder -- output of the inline
      // border-detection block. wedges -- output of BuildWedgeData.
      // Phase 1b.4 JS port validates these.
      vertIsBorder: subdiv.vertIsBorder || null,
      edgeIsBorder: subdiv.edgeIsBorder || null,
      wedges:       subdiv.wedges       || null,
      // edgeSharpness / vertSharpness -- output of
      // DecompressCreaseSharpness + the enableSemiSharp block.
      // Phase 1b.5 JS port validates these.
      edgeSharpness: subdiv.edgeSharpness ? toF32(subdiv.edgeSharpness, 'subdiv.edgeSharpness') : null,
      vertSharpness: subdiv.vertSharpness ? toF32(subdiv.vertSharpness, 'subdiv.vertSharpness') : null,
      // prepMesh -- post-PrepMeshForCatmullClark UV-id duplication.
      // Phase 1b.9 JS port validates this.
      prepMesh: subdiv.prepMesh || null,
      // applyCC1 -- output of the first ApplyCatmullClarkToWeights:
      // m_topo1 (numVerts/polySizes/polyIndexList/crease...) and
      // m_posWeights1 (FlatWeightVertex[] compact-encoded via
      // nums/starts/influences/weights). Phase 1b.10 JS port
      // validates against this.
      applyCC1: subdiv.applyCC1 || null,
      // level2 -- output of ConvolveWeights + ApplyWeightsToPoints
      // chain: m_posWeights2, m_posData1, m_posData2. Phase 1b.11 JS
      // port validates these.
      level2: subdiv.level2 || null,
      // regionData + quadRegionGrids -- output of
      // BuildCalcQuadRegionData. Phase 1b.12a JS port validates these.
      // quadRegionGrids also carries pointsStart/pointsNum from
      // CollectPointsForPatches (Phase 1b.12d).
      regionData:      subdiv.regionData      || [],
      quadRegionGrids: subdiv.quadRegionGrids || null,
      // triRegionGrids -- per-tri-subgrid pointsStart/pointsNum from
      // CollectPointsForPatches. Parallel arrays of length 3*numPolys.
      triRegionGrids:  subdiv.triRegionGrids  || null,
      // regularVertexIndices -- flat ControlPoint[] from
      // CatmullClarkPatches::m_regularVertexIndices (Phase 1b.12d).
      regularVertexIndices: subdiv.regularVertexIndices || null,
      // irregularVertexIndices + irregularWedgeInfo -- output of
      // CollectPointsForIrregular (Phase 1b.12e). irregularVertexIndices
      // is a flat ControlPoint[] (4 parallel arrays); irregularWedgeInfo
      // is per-irregular-wedge metadata (m_isCircular/m_valence set by
      // this pass + m_irregularVertexStart/Num).
      irregularVertexIndices: subdiv.irregularVertexIndices || null,
      irregularWedgeInfo:     subdiv.irregularWedgeInfo     || null,
      // weightInflData + controlPointRedirect -- output of
      // BuildRuntimePoints (Phase 1b.12f). The runtime-shape stencil
      // table + the per-CP remap into the deduped slot.
      // weightInflData also carries m_irregularEdgeSharpness +
      // m_irregularGroupStart/Num from BuildRuntimeIrregularInfo
      // (Phase 1b.12g).
      weightInflData:       subdiv.weightInflData       || null,
      controlPointRedirect: subdiv.controlPointRedirect || null,
      // runtimeIrregularInfo -- per-irregular-wedge runtime info
      // (Phase 1b.12g). 11 parallel arrays.
      runtimeIrregularInfo: subdiv.runtimeIrregularInfo || null,
      // bsplineOracle -- fixture-independent BSpline basis sweep used
      // by the Phase 1b.13a gate. {sharpness:[{...}], basis:[{...}]}.
      bsplineOracle: subdiv.bsplineOracle || null,
      // limitWeightsPerWedge -- per-irregular-wedge (weightP, weightT0,
      // weightT1) flat arrays from CalculateLimitWeights (Phase 1b.13d).
      // Offsets array of size N+1; flat arrays sized to offsets[N].
      limitWeightsPerWedge: subdiv.limitWeightsPerWedge || null,
      // buildEdgeLookupInputs -- the exact inputs passed into
      // BuildEdgeLookup (read from builder.m_topo0 at dump time).
      // Phase 1b.2 gate consumes these so it isolates the
      // BuildEdgeLookup port from the not-yet-ported
      // MakeSubdivTopoFromMeshVtx / BuildHardTangentEdgesFromMesh
      // upstream.
      buildEdgeLookupInputs: subdiv.buildEdgeLookupInputs || null,
      patches:              subdiv.patches || [],
      // Per-rendered-triangle (patchIter, uvBasis) read from amolite's
      // VtMeshData::m_topoData. Surfaced into the viewer's builtData
      // at scene load.
      triPatchMap:          subdiv.triPatchMap || [],
    },
    frame: {
      limitPos:   toF32(frame.limitPos,  'frame.limitPos'),
      limitNrm:   toF32(frame.limitNrm,  'frame.limitNrm'),
      wedgeIters: frame.wedgeIters || [],
    },
    samples: {
      count:    okSamples.length,
      patchIter: sampleIter,
      uv:        sampleUv,
      P:         samplePos,
      N:         sampleNrm,
      // Skipped sentinels (amolite's known multi-semisharp limitation).
      // Phase 4 patch-eval JS port either reproduces this limitation or
      // does better. Either way the diff treats these as "no oracle
      // available here" rather than "data missing".
      skipped:   skippedSamples,
    },
    raw,    // escape hatch for cases that need the unwrapped JSON
  };
}

// Browser + Node export. In the browser, both functions land on
// globalThis when this file is loaded via <script>. In Node, callers
// require('./ccref_loader') and destructure.
if (typeof module !== 'undefined' && module.exports) {
  module.exports = { CCREF_SCHEMA_VERSION, loadCcReference, wrapCcReference };
}
