// =====================================================================
// gpu_test_helpers.js -- WebGPU readback + byte-compare helpers used by
// the CC -> PASS_B consolidation tests. All functions are async; they
// stage a COPY_SRC -> MAP_READ buffer, await mapAsync, and operate on
// the mapped bytes.
//
// Why these live in their own file: the Phase A -> F consolidation
// plan needs per-step byte-equality gates (pack-into-topo verification,
// per-frame ccVec3Data upload verification, evalCC byte-equal-to-CCGPU
// validation). All gates share the same readback pattern; centralizing
// it here keeps the per-gate test code short and unambiguous.
// =====================================================================
'use strict';

// Read back `byteLen` bytes from `gpuBuf` starting at `byteOff`, return
// a Uint8Array copy. The returned array is detached from any WebGPU
// mapping so the caller can hold it across await points.
async function readbackBufferBytes(device, gpuBuf, byteOff, byteLen) {
  if (byteLen <= 0) return new Uint8Array(0);
  // mapAsync requires offset+size aligned to 4. Our inputs aren't
  // always aligned (e.g. tail bytes of an unpadded section). Round
  // the staging window to a 4-byte multiple and slice the result.
  const alignedOff  = byteOff & ~3;
  const headSkip    = byteOff - alignedOff;
  const alignedLen  = (headSkip + byteLen + 3) & ~3;
  const stage = device.createBuffer({
    size: alignedLen,
    usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
  });
  const enc = device.createCommandEncoder();
  enc.copyBufferToBuffer(gpuBuf, alignedOff, stage, 0, alignedLen);
  device.queue.submit([enc.finish()]);
  await stage.mapAsync(GPUMapMode.READ);
  const mapped = new Uint8Array(stage.getMappedRange());
  const out = new Uint8Array(byteLen);
  out.set(mapped.subarray(headSkip, headSkip + byteLen));
  stage.unmap();
  stage.destroy();
  return out;
}

// Byte-compare two Uint8Array (or anything with .byteLength + indexable
// bytes). Returns:
//   { ok: true,  size: N }                              if equal
//   { ok: false, size: N, mismatchIndex: i,
//     a: aByte, b: bByte, firstDiffWindow: [..16 hex..] } if not equal
// Names "a" / "b" let callers print which side is which.
function compareBytes(a, b, label) {
  if (a.byteLength !== b.byteLength) {
    return {
      ok: false,
      label: label || '',
      size: -1,
      mismatchIndex: -1,
      msg: 'size mismatch a=' + a.byteLength + ' b=' + b.byteLength,
    };
  }
  const N = a.byteLength;
  const u8a = (a instanceof Uint8Array) ? a : new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
  const u8b = (b instanceof Uint8Array) ? b : new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
  for (let i = 0; i < N; i++) {
    if (u8a[i] !== u8b[i]) {
      const w0 = Math.max(0, i - 4);
      const w1 = Math.min(N, i + 12);
      const hex = (arr) => Array.from(arr.subarray(w0, w1))
        .map(x => x.toString(16).padStart(2, '0')).join(' ');
      return {
        ok: false,
        label: label || '',
        size: N,
        mismatchIndex: i,
        a: u8a[i],
        b: u8b[i],
        aHex: hex(u8a),
        bHex: hex(u8b),
      };
    }
  }
  return { ok: true, label: label || '', size: N };
}

// Convenience: readback a buffer's first `byteLen` bytes and compare
// against `expectedBytes` (any TypedArray or ArrayBuffer). Returns a
// compareBytes-shaped result.
async function readbackAndCompare(device, gpuBuf, expectedBytes, label) {
  const expU8 = (expectedBytes instanceof ArrayBuffer)
    ? new Uint8Array(expectedBytes)
    : (expectedBytes instanceof Uint8Array)
      ? expectedBytes
      : new Uint8Array(expectedBytes.buffer, expectedBytes.byteOffset, expectedBytes.byteLength);
  const got = await readbackBufferBytes(device, gpuBuf, 0, expU8.byteLength);
  return compareBytes(got, expU8, label);
}

if (typeof module !== 'undefined' && module.exports) {
  module.exports = { readbackBufferBytes, compareBytes, readbackAndCompare };
}
