// Precomputed-topology table for the table-driven tessellation path.
//
// For each pair (t0, t2) of token ids, store the interior topology of a
// type-2 source tri:
//   - per interior vertex: (rowIter : u8, col : u8)              -- 2 bytes
//   - per interior prim  : 3 x packed (vtype + vindex) u16       -- 6 bytes
//
// For each triple (t0, t1, t2), store the gap topology:
//   - per gap prim       : 3 x packed (vtype + vindex) u16       -- 6 bytes
//
// Packed corner encoding (16 bits):
//   bit 15 = 1 => vtype 6 (interior), vindex in low 15 bits (range 0..32767)
//   bit 15 = 0 => vtype 0..5 in bits 14..12, vindex in bits 11..0 (range 0..4095)
// vindex for vtype 0/1/2 (corners) is always 0; for vtype 3/4/5 (edges)
// up to numPts-1 (<= 255). Interior vindex can hit ~32K at max tess.
//
// The runtime shader uses (t0, t2) interior + (t0, t1, t2) gap to drive
// the index pass directly; for the bary pass, interior verts' (row, col)
// drive a fast (token_t0[1+row], token_t2[1+col]) lookup with the same
// gap clamp the analytical path applies. Edge / corner verts have their
// bary derived from token points via vtype + vindex (no table lookup
// needed for those).
//
// triType=0 (no tess) and triType=1 (low tess) handled inline in the
// shader; the table covers triType=2 only -- the dominant case at any
// non-degenerate tess factor.
//
// Blob layout (all little-endian):
//   [Header: 32 bytes]
//     u32 magic   = 'TS5T' (0x54_53_35_54)
//     u32 maxTokenId
//     u32 NT      (always 25)
//     u32 interiorOffsetTableByteOff
//     u32 gapOffsetTableByteOff
//     u32 dataSectionByteOff
//     u32 blobByteSize
//     u32 reserved
//   [Interior offset table: NT*NT entries x 16 bytes]
//     per (t0, t2) -> u32 vertByteOff, u32 vertCount, u32 primByteOff, u32 primCount
//   [Gap offset table: NT*NT*NT entries x 16 bytes]
//     per (t0, t1, t2) -> u32 primByteOff, u32 primCount, u32 _pad0, u32 _pad1
//   [Data section]
//     interior data (verts then prims per pair, contiguous)
//     gap data      (prims per triple, contiguous)

'use strict';

(function () {
  const NT = 25;
  const HEADER_BYTES               = 32;
  const INTERIOR_ENTRY_BYTES       = 16;  // (vertOff, vertCount, primOff, primCount) each u32
  const GAP_ENTRY_BYTES            = 16;
  const VERT_BYTES                 = 2;   // (rowIter:u8, col:u8)
  const PRIM_BYTES                 = 6;   // 3 x (vtype:u8, vindex:u8)
  const MAGIC                      = 0x54_35_53_54;  // 'TS5T'

  // Build a token "shape" object that the existing TessLookup functions
  // accept. They look at .m_table, .m_numPts, .m_log2Scale, .m_lerpT.
  // m_lerpT only affects bary VALUE calculations, not topology lookups
  // -- the table's job is topology only, so we leave it 0.
  function makeToken(tokenId) {
    const info = TessLookup.m_tokenInfo[tokenId];
    return {
      m_table:     tokenId,
      m_numPts:    info.m_ptsNum,
      m_log2Scale: info.m_log2Scale,
      m_lerpT:     0,
    };
  }

  // Pack a TopoVtx (returned by TessLookup.LookupStripNumPrimCorner /
  // LookupGapMatchIndexCorner) into a 16-bit value.
  // See header comment for encoding details.
  function packTopoVtx(tv) {
    const vtype = tv.m_type;
    if (vtype < 0 || vtype > 6) {
      throw new Error('TessTable: unexpected vtype ' + vtype);
    }
    let vindex = tv.m_index;
    if (vindex < 0) vindex = 0;
    if (vtype === 6) {
      if (vindex >= 0x8000) {
        throw new Error('TessTable: interior vindex ' + vindex + ' overflows 15 bits');
      }
      return 0x8000 | vindex;
    }
    if (vindex >= 0x1000) {
      throw new Error('TessTable: vindex ' + vindex + ' overflows 12 bits (vtype=' + vtype + ')');
    }
    return (vtype << 12) | vindex;
  }
  function writeTopoVtx(u16, halfOff, tv) {
    u16[halfOff] = packTopoVtx(tv);
  }

  // Build the blob. maxTokenId defaults to 24 (full coverage up to tess=255).
  function build(maxTokenId) {
    if (maxTokenId === undefined) maxTokenId = NT - 1;
    if (maxTokenId < 0 || maxTokenId >= NT) {
      throw new Error('TessTable.build: maxTokenId out of range');
    }

    // ---- Pass 1: count storage requirements ----
    const interiorEntries = new Array(NT * NT);
    const gapEntries      = new Array(NT * NT * NT);

    let dataCursor = 0;

    // Interior pairs (t0, t2). Always allocate for ALL pairs in the
    // [0..maxTokenId] range; pairs outside the range get count=0.
    for (let t2 = 0; t2 < NT; t2++) {
      for (let t0 = 0; t0 < NT; t0++) {
        const idx = t2 * NT + t0;
        if (t0 > maxTokenId || t2 > maxTokenId) {
          interiorEntries[idx] = { vertOff: 0, vertCount: 0, primOff: 0, primCount: 0 };
          continue;
        }
        const numVerts = TessLookup.LookupInteriorNumInteriorVtx(t0, t2);
        const numPrims = TessLookup.LookupInteriorNumPrims(t0, t2);
        const vertOff = dataCursor;
        dataCursor += numVerts * VERT_BYTES;
        const primOff = dataCursor;
        dataCursor += numPrims * PRIM_BYTES;
        interiorEntries[idx] = { vertOff, vertCount: numVerts, primOff, primCount: numPrims };
      }
    }

    // Gap triples (t0, t1, t2).
    const tokens = new Array(NT);
    for (let i = 0; i < NT; i++) tokens[i] = makeToken(i);

    for (let t2 = 0; t2 < NT; t2++) {
      for (let t1 = 0; t1 < NT; t1++) {
        for (let t0 = 0; t0 < NT; t0++) {
          const idx = t2 * NT * NT + t1 * NT + t0;
          if (t0 > maxTokenId || t1 > maxTokenId || t2 > maxTokenId) {
            gapEntries[idx] = { primOff: 0, primCount: 0 };
            continue;
          }
          const numPrims = TessLookup.GetGapNumPrims(tokens[t0], tokens[t1]);
          const primOff = dataCursor;
          dataCursor += numPrims * PRIM_BYTES;
          gapEntries[idx] = { primOff, primCount: numPrims };
        }
      }
    }

    // ---- Pass 2: allocate blob, write headers ----
    const interiorTableBytes = NT * NT * INTERIOR_ENTRY_BYTES;
    const gapTableBytes      = NT * NT * NT * GAP_ENTRY_BYTES;
    const dataBaseOff        = HEADER_BYTES + interiorTableBytes + gapTableBytes;
    const totalSize          = dataBaseOff + dataCursor;

    const buf = new ArrayBuffer(totalSize);
    const u32 = new Uint32Array(buf);
    const u16 = new Uint16Array(buf);
    const u8  = new Uint8Array(buf);

    // Header
    u32[0] = MAGIC;
    u32[1] = maxTokenId;
    u32[2] = NT;
    u32[3] = HEADER_BYTES;
    u32[4] = HEADER_BYTES + interiorTableBytes;
    u32[5] = dataBaseOff;
    u32[6] = totalSize;
    u32[7] = 0;

    // Interior offset table
    const interiorTableU32 = HEADER_BYTES / 4;
    for (let i = 0; i < NT * NT; i++) {
      const e = interiorEntries[i];
      const baseU32 = interiorTableU32 + i * 4;
      u32[baseU32 + 0] = e.vertCount > 0 ? dataBaseOff + e.vertOff : 0;
      u32[baseU32 + 1] = e.vertCount;
      u32[baseU32 + 2] = e.primCount > 0 ? dataBaseOff + e.primOff : 0;
      u32[baseU32 + 3] = e.primCount;
    }

    // Gap offset table
    const gapTableU32 = (HEADER_BYTES + interiorTableBytes) / 4;
    for (let i = 0; i < NT * NT * NT; i++) {
      const e = gapEntries[i];
      const baseU32 = gapTableU32 + i * 4;
      u32[baseU32 + 0] = e.primCount > 0 ? dataBaseOff + e.primOff : 0;
      u32[baseU32 + 1] = e.primCount;
      u32[baseU32 + 2] = 0;
      u32[baseU32 + 3] = 0;
    }

    // ---- Pass 3: write interior data ----
    for (let t2 = 0; t2 <= maxTokenId; t2++) {
      for (let t0 = 0; t0 <= maxTokenId; t0++) {
        const idx = t2 * NT + t0;
        const e = interiorEntries[idx];
        if (e.vertCount === 0 && e.primCount === 0) continue;

        const tk0 = tokens[t0];
        const tk2 = tokens[t2];

        // Interior verts: each interior bary index v -> (rowIter, col)
        // by walking the existing TessLookup row layout. Same arithmetic
        // as the WGSL `lookupBaryPos` interior branch:
        //   rowIter  = GetRowIndexForBary(tk0, tk2, v)
        //   rowStart = LookupInteriorRowVtxStart(t0, t2, rowIter)
        //   col      = v - rowStart
        const vertBase = dataBaseOff + e.vertOff;
        for (let v = 0; v < e.vertCount; v++) {
          const rowIter  = TessLookup.GetRowIndexForBary(tk0, tk2, v);
          const rowStart = TessLookup.LookupInteriorRowVtxStart(t0, t2, rowIter);
          const col      = v - rowStart;
          if (rowIter > 255 || col > 255 || col < 0) {
            throw new Error('TessTable interior vert (' + t0 + ',' + t2 + ') v=' + v +
                            ': row=' + rowIter + ' col=' + col + ' out of u8 range');
          }
          u8[vertBase + v * VERT_BYTES + 0] = rowIter;
          u8[vertBase + v * VERT_BYTES + 1] = col;
        }

        // Interior prims: each prim p has 3 corners. Walk via the strip
        // index, mirroring fullLookupCornerForPrim's interior branch.
        const primBase = dataBaseOff + e.primOff;
        const primHalfBase = primBase / 2;  // u16-index base
        for (let p = 0; p < e.primCount; p++) {
          const stripIter  = TessLookup.GetStripIndexForPrim(tk0, tk2, p);
          const stripStart = TessLookup.LookupStripPrimOffset(t0, t2, stripIter);
          const stripPrim  = p - stripStart;
          for (let c = 0; c < 3; c++) {
            const tv = TessLookup.LookupStripNumPrimCorner(t0, t2, stripIter, stripPrim, c);
            writeTopoVtx(u16, primHalfBase + p * 3 + c, tv);
          }
        }
      }
    }

    // ---- Pass 4: write gap data ----
    for (let t2 = 0; t2 <= maxTokenId; t2++) {
      for (let t1 = 0; t1 <= maxTokenId; t1++) {
        for (let t0 = 0; t0 <= maxTokenId; t0++) {
          const idx = t2 * NT * NT + t1 * NT + t0;
          const e = gapEntries[idx];
          if (e.primCount === 0) continue;

          const tk0 = tokens[t0];
          const tk1 = tokens[t1];
          const tk2 = tokens[t2];

          const primBase = dataBaseOff + e.primOff;
          const primHalfBase = primBase / 2;
          for (let p = 0; p < e.primCount; p++) {
            for (let c = 0; c < 3; c++) {
              const tv = TessLookup.LookupGapMatchIndexCorner(tk0, tk1, tk2, p, c);
              writeTopoVtx(u16, primHalfBase + p * 3 + c, tv);
            }
          }
        }
      }
    }

    return new Uint8Array(buf);
  }

  // Self-check: re-derive a few prim corners via TessLookup.FullLookupCornerForPrim
  // and verify they match what was packed into the blob. Returns true on
  // success, throws on mismatch.
  function unpackTopoVtx(packed) {
    if ((packed & 0x8000) !== 0) {
      return { vtype: 6, vindex: packed & 0x7FFF };
    }
    return { vtype: (packed >> 12) & 0xF, vindex: packed & 0xFFF };
  }

  function selfCheck(blob, maxTokenId) {
    if (maxTokenId === undefined) maxTokenId = NT - 1;
    const u32 = new Uint32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
    const u16 = new Uint16Array(blob.buffer, blob.byteOffset, blob.byteLength / 2);
    const u8  = new Uint8Array (blob.buffer, blob.byteOffset, blob.byteLength);

    if (u32[0] !== MAGIC)             throw new Error('TessTable.selfCheck: bad magic');
    if (u32[1] !== maxTokenId)        throw new Error('TessTable.selfCheck: maxTokenId mismatch');
    if (u32[2] !== NT)                throw new Error('TessTable.selfCheck: NT mismatch');
    if (u32[6] !== blob.byteLength)   throw new Error('TessTable.selfCheck: size mismatch');

    const interiorTableU32 = u32[3] / 4;
    const gapTableU32      = u32[4] / 4;

    const tokens = new Array(NT);
    for (let i = 0; i < NT; i++) tokens[i] = makeToken(i);

    let checkedInterior = 0;
    let checkedGap = 0;

    // Spot-check: every (t0, t2) pair, verify first prim's first corner.
    // Plus the LAST interior prim's LAST corner. Plus a vert sample.
    for (let t2 = 0; t2 <= maxTokenId; t2++) {
      for (let t0 = 0; t0 <= maxTokenId; t0++) {
        const idx = t2 * NT + t0;
        const baseU32 = interiorTableU32 + idx * 4;
        const vertCount = u32[baseU32 + 1];
        const primCount = u32[baseU32 + 3];
        if (primCount === 0) continue;

        const vertBase = u32[baseU32 + 0];
        const primBase = u32[baseU32 + 2];

        // Verify ALL interior prims for this pair (cheap enough for spot-check).
        const tk0 = tokens[t0], tk2 = tokens[t2];
        const primHalfBase = primBase / 2;
        for (let p = 0; p < primCount; p++) {
          const stripIter  = TessLookup.GetStripIndexForPrim(tk0, tk2, p);
          const stripStart = TessLookup.LookupStripPrimOffset(t0, t2, stripIter);
          const stripPrim  = p - stripStart;
          for (let c = 0; c < 3; c++) {
            const expected = TessLookup.LookupStripNumPrimCorner(t0, t2, stripIter, stripPrim, c);
            const got = unpackTopoVtx(u16[primHalfBase + p * 3 + c]);
            const expIndex = expected.m_index < 0 ? 0 : expected.m_index;
            if (got.vtype !== expected.m_type || got.vindex !== expIndex) {
              throw new Error('TessTable.selfCheck interior prim ' +
                  '(t0=' + t0 + ',t2=' + t2 + ',p=' + p + ',c=' + c + '): ' +
                  'expected (' + expected.m_type + ',' + expIndex + ') ' +
                  'got (' + got.vtype + ',' + got.vindex + ')');
            }
            checkedInterior++;
          }
        }

        // Verify interior verts' (rowIter, col) by recomputing from the
        // packed values and confirming the resulting bary index round-trips.
        for (let v = 0; v < vertCount; v++) {
          const off = vertBase + v * VERT_BYTES;
          const gotRow = u8[off + 0];
          const gotCol = u8[off + 1];
          const expRow = TessLookup.GetRowIndexForBary(tk0, tk2, v);
          const expCol = v - TessLookup.LookupInteriorRowVtxStart(t0, t2, expRow);
          if (gotRow !== expRow || gotCol !== expCol) {
            throw new Error('TessTable.selfCheck interior vert ' +
                '(t0=' + t0 + ',t2=' + t2 + ',v=' + v + '): ' +
                'expected (row=' + expRow + ',col=' + expCol + ') ' +
                'got (row=' + gotRow + ',col=' + gotCol + ')');
          }
        }
      }
    }

    // Verify ALL gap triples, ALL prims, ALL corners. Fast-ish; gap is small.
    for (let t2 = 0; t2 <= maxTokenId; t2++) {
      for (let t1 = 0; t1 <= maxTokenId; t1++) {
        for (let t0 = 0; t0 <= maxTokenId; t0++) {
          const idx = t2 * NT * NT + t1 * NT + t0;
          const baseU32 = gapTableU32 + idx * 4;
          const primCount = u32[baseU32 + 1];
          if (primCount === 0) continue;
          const primBase = u32[baseU32 + 0];
          const primHalfBase = primBase / 2;
          const tk0 = tokens[t0], tk1 = tokens[t1], tk2 = tokens[t2];
          for (let p = 0; p < primCount; p++) {
            for (let c = 0; c < 3; c++) {
              const expected = TessLookup.LookupGapMatchIndexCorner(tk0, tk1, tk2, p, c);
              const got = unpackTopoVtx(u16[primHalfBase + p * 3 + c]);
              const expIndex = expected.m_index < 0 ? 0 : expected.m_index;
              if (got.vtype !== expected.m_type || got.vindex !== expIndex) {
                throw new Error('TessTable.selfCheck gap prim ' +
                    '(t0=' + t0 + ',t1=' + t1 + ',t2=' + t2 + ',p=' + p + ',c=' + c + '): ' +
                    'expected (' + expected.m_type + ',' + expIndex + ') ' +
                    'got (' + got.vtype + ',' + got.vindex + ')');
              }
              checkedGap++;
            }
          }
        }
      }
    }

    return { checkedInterior, checkedGap };
  }

  window.TessTable = { build, selfCheck, NT, MAGIC,
    HEADER_BYTES, INTERIOR_ENTRY_BYTES, GAP_ENTRY_BYTES,
    VERT_BYTES, PRIM_BYTES };
})();
