/**
 * Ghost track storage + sampling — the pure, headless-testable core shared by
 * the recorder (writes), the player (reads) and the title (reads best time).
 *
 * No three / no DOM here beyond guarded btoa/atob/localStorage, so the drift
 * verifier (verify-drift.ts) can bundle + run this exact code under `node`.
 *
 * Wire storage is int16 centimetres (position) + int16 milli-units (uniform
 * scale), base64 of the raw Int16Array inside a tiny JSON envelope in
 * localStorage key `blobbo:ghost`. A full 5-minute run is ~48KB base64, well
 * under the ~200KB budget.
 */

export const GHOST_KEY = 'blobbo:ghost'
export const RECORD_HZ = 15
export const MAX_SECONDS = 300 // ~5 min recording cap
export const MAX_SAMPLES = MAX_SECONDS * RECORD_HZ // 4500
export const FLOATS_PER_SAMPLE = 4 // x, y, z, uniformScale
export const POS_SCALE = 100 // metres -> int16 centimetres  (±327.67 m range)
export const SIZE_SCALE = 1000 // uniform scale -> int16 milli-units (±32.767)

export interface GhostTrack {
  time: number // recorded run time (seconds)
  hz: number // sample rate
  count: number // number of samples stored
  data: Int16Array // interleaved [x_cm, y_cm, z_cm, scale_milli] * count
}

export interface GhostSample {
  x: number
  y: number
  z: number
  scale: number
}

// clamp to int16 so one wild sample can never corrupt the buffer / overflow
const i16 = (n: number): number => Math.max(-32768, Math.min(32767, Math.round(n)))

/** Quantise the first `count` samples of a flat Float32 buffer into int16s. */
export function quantize(src: Float32Array, count: number): Int16Array {
  const out = new Int16Array(count * FLOATS_PER_SAMPLE)
  for (let i = 0; i < count; i++) {
    const o = i * FLOATS_PER_SAMPLE
    out[o] = i16(src[o] * POS_SCALE)
    out[o + 1] = i16(src[o + 1] * POS_SCALE)
    out[o + 2] = i16(src[o + 2] * POS_SCALE)
    out[o + 3] = i16(src[o + 3] * SIZE_SCALE)
  }
  return out
}

/**
 * Linear-interpolate the track at race-time `t` (seconds), writing into `out`
 * (reused to avoid per-frame allocation). Clamps at both ends: before the run
 * it sits at the start sample, after the run it freezes on the finish sample.
 */
export function sampleAt(track: GhostTrack, t: number, out: GhostSample): GhostSample {
  const { data, count, hz } = track
  if (count === 0) {
    out.x = out.y = out.z = 0
    out.scale = 1
    return out
  }
  const f = Math.max(0, Math.min(count - 1, t * hz))
  const i0 = Math.floor(f)
  const i1 = Math.min(count - 1, i0 + 1)
  const a = f - i0
  const o0 = i0 * FLOATS_PER_SAMPLE
  const o1 = i1 * FLOATS_PER_SAMPLE
  out.x = (data[o0] + (data[o1] - data[o0]) * a) / POS_SCALE
  out.y = (data[o0 + 1] + (data[o1 + 1] - data[o0 + 1]) * a) / POS_SCALE
  out.z = (data[o0 + 2] + (data[o1 + 2] - data[o0 + 2]) * a) / POS_SCALE
  out.scale = (data[o0 + 3] + (data[o1 + 3] - data[o0 + 3]) * a) / SIZE_SCALE
  return out
}

// ---- base64 <-> bytes (chunked so btoa never blows the arg limit) ----------
const B64_CHUNK = 0x8000

function bytesToB64(bytes: Uint8Array): string {
  let bin = ''
  for (let i = 0; i < bytes.length; i += B64_CHUNK) {
    bin += String.fromCharCode(...bytes.subarray(i, i + B64_CHUNK))
  }
  return btoa(bin)
}

function b64ToBytes(b64: string): Uint8Array {
  const bin = atob(b64)
  const bytes = new Uint8Array(bin.length)
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
  return bytes
}

// ---- envelope -------------------------------------------------------------
interface GhostPayload {
  v: number // schema version
  t: number // run time (s)
  hz: number // sample rate
  n: number // sample count
  b: string // base64 of the interleaved int16 buffer
}

export function encodeGhost(track: GhostTrack): string {
  const view = new Uint8Array(track.data.buffer, track.data.byteOffset, track.data.byteLength)
  const payload: GhostPayload = {
    v: 1,
    t: track.time,
    hz: track.hz,
    n: track.count,
    b: bytesToB64(view),
  }
  return JSON.stringify(payload)
}

export function decodeGhost(json: string): GhostTrack | null {
  try {
    const p = JSON.parse(json) as Partial<GhostPayload>
    if (!p || p.v !== 1 || typeof p.b !== 'string') return null
    const bytes = b64ToBytes(p.b)
    // ensure an even byte length for Int16Array
    const len16 = Math.floor(bytes.byteLength / 2)
    const data = new Int16Array(bytes.buffer, bytes.byteOffset, len16)
    const count = typeof p.n === 'number' ? p.n : Math.floor(len16 / FLOATS_PER_SAMPLE)
    return {
      time: typeof p.t === 'number' ? p.t : count / RECORD_HZ,
      hz: typeof p.hz === 'number' && p.hz > 0 ? p.hz : RECORD_HZ,
      count,
      data,
    }
  } catch {
    return null
  }
}

// ---- localStorage (guarded so this module stays runnable headless) --------
function store(): Storage | null {
  try {
    return typeof localStorage !== 'undefined' ? localStorage : null
  } catch {
    return null
  }
}

/** Persist a track. Returns false if storage is unavailable or the write threw. */
export function saveGhost(track: GhostTrack): boolean {
  const s = store()
  if (!s) return false
  try {
    s.setItem(GHOST_KEY, encodeGhost(track))
    return true
  } catch {
    return false
  }
}

export function loadGhost(): GhostTrack | null {
  const s = store()
  if (!s) return null
  const raw = s.getItem(GHOST_KEY)
  return raw ? decodeGhost(raw) : null
}

export function clearGhost(): void {
  store()?.removeItem(GHOST_KEY)
}

export function hasGhost(): boolean {
  return !!store()?.getItem(GHOST_KEY)
}

/** Cheap read of just the saved run time (for the title), without decoding samples. */
export function ghostTime(): number | null {
  const s = store()
  if (!s) return null
  const raw = s.getItem(GHOST_KEY)
  if (!raw) return null
  try {
    const p = JSON.parse(raw) as Partial<GhostPayload>
    return typeof p.t === 'number' ? p.t : null
  } catch {
    return null
  }
}
