/**
 * Numeric acceptance for Lane H: ghost replay drift vs the recorded path must be
 * < one blob radius at all sample points. This mirrors the demo's deterministic
 * circle, records it at 15Hz through the REAL quantize(), then replays it
 * through the REAL sampleAt() and reports the worst-case position error.
 *
 * Not imported by any entry (headless-only). Run it with:
 *   node_modules/.bin/esbuild src/ghost/verify-drift.ts --bundle --platform=node \
 *     --format=esm --outfile=/tmp/verify.mjs && node /tmp/verify.mjs
 */
import {
  FLOATS_PER_SAMPLE,
  RECORD_HZ,
  quantize,
  sampleAt,
  type GhostSample,
  type GhostTrack,
} from './format'

// --- the exact same path the demo drives (ball on a circle) ---
const R = 8
const CY = 1.0
const RUN_SECONDS = 20
const OMEGA = (2 * Math.PI) / RUN_SECONDS
const BLOB_RADIUS = 0.5

function pathAt(t: number, out: GhostSample): GhostSample {
  const th = OMEGA * t - Math.PI / 2
  out.x = R * Math.cos(th)
  out.y = CY
  out.z = R * Math.sin(th)
  out.scale = 1 + 0.15 * Math.sin(2 * OMEGA * t)
  return out
}

// --- record at 15Hz exactly as GhostRecorder does (grid times i/hz) ---
const count = Math.floor(RUN_SECONDS * RECORD_HZ) + 1
const buf = new Float32Array(count * FLOATS_PER_SAMPLE)
const s: GhostSample = { x: 0, y: 0, z: 0, scale: 1 }
for (let i = 0; i < count; i++) {
  pathAt(i / RECORD_HZ, s)
  const o = i * FLOATS_PER_SAMPLE
  buf[o] = s.x
  buf[o + 1] = s.y
  buf[o + 2] = s.z
  buf[o + 3] = s.scale
}

const track: GhostTrack = { time: RUN_SECONDS, hz: RECORD_HZ, count, data: quantize(buf, count) }

// --- replay and measure drift ---
const truth: GhostSample = { x: 0, y: 0, z: 0, scale: 1 }
const ghost: GhostSample = { x: 0, y: 0, z: 0, scale: 1 }

function driftAt(t: number): number {
  pathAt(t, truth)
  sampleAt(track, t, ghost)
  return Math.hypot(truth.x - ghost.x, truth.y - ghost.y, truth.z - ghost.z)
}

// (a) exactly at every recorded sample point (the acceptance wording)
let maxAtSamples = 0
let worstSampleT = 0
for (let i = 0; i < count; i++) {
  const t = i / RECORD_HZ
  const d = driftAt(t)
  if (d > maxAtSamples) {
    maxAtSamples = d
    worstSampleT = t
  }
}

// (b) densely between samples too (interpolation error), 1ms resolution
let maxDense = 0
let worstDenseT = 0
for (let t = 0; t <= RUN_SECONDS; t += 0.001) {
  const d = driftAt(t)
  if (d > maxDense) {
    maxDense = d
    worstDenseT = t
  }
}

// scale error, for completeness
let maxScaleErr = 0
for (let t = 0; t <= RUN_SECONDS; t += 0.001) {
  pathAt(t, truth)
  sampleAt(track, t, ghost)
  maxScaleErr = Math.max(maxScaleErr, Math.abs(truth.scale - ghost.scale))
}

const rawBytes = track.data.byteLength
const b64Bytes = Math.ceil(rawBytes / 3) * 4

console.log('--- Lane H ghost drift verification ---')
console.log(`samples: ${count} @ ${RECORD_HZ}Hz   storage: ${rawBytes} B int16 → ~${b64Bytes} B base64`)
console.log(`blob radius (budget): ${BLOB_RADIUS} m`)
console.log(`max drift at sample points : ${maxAtSamples.toFixed(4)} m  (t=${worstSampleT.toFixed(3)}s)`)
console.log(`max drift densely (1ms)    : ${maxDense.toFixed(4)} m  (t=${worstDenseT.toFixed(3)}s)`)
console.log(`max scale error            : ${maxScaleErr.toFixed(5)}`)

const pass = maxDense < BLOB_RADIUS && maxAtSamples < BLOB_RADIUS
console.log(pass ? `PASS — drift < ${BLOB_RADIUS} m everywhere` : 'FAIL — drift exceeds one blob radius')
if (!pass) throw new Error('ghost drift exceeds one blob radius')
