export const SNAPSHOT = Object.freeze({
  MAGIC: 0x504e5359, BLOB_MAGIC: 0x32534452, VERSION: 1, BYTES: 256,
  SLOTS: 3, SLOT_BYTES: 64, SCHEMA: 2, INIT: 0, OPEN: 1, FAILED: 2,
  CLOSED: 3, FREE: 0, WRITING: 1, READY: 2, READING: 3,
});
export const STREAM_NAMES = ["meshSlot", "meshGeneration", "meshLocalMin", "meshLocalMax", "instanceSlot", "instanceGeneration", "instanceMeshSlot", "instanceMeshGeneration", "instanceModel", "instanceWorldMin", "instanceWorldMax", "instanceType"];
const COMPONENTS = [1, 1, 3, 3, 1, 1, 1, 1, 16, 3, 3, 16];
const SCALARS = [1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1];
const STRIDES = COMPONENTS.map(n => n * 4);

export class SnapshotProtocolError extends Error {
  constructor(code) { super(code); this.code = code; this.name = "SnapshotProtocolError"; }
}

const bad = code => { throw new SnapshotProtocolError(code); };
const add = (a, b) => { const n = a + b; if (!Number.isSafeInteger(n) || n > 0xffffffff) bad("BAD_RANGE"); return n; };
const mul = (a, b) => { const n = a * b; if (!Number.isSafeInteger(n) || n > 0xffffffff) bad("BAD_RANGE"); return n; };

export class SnapshotReader {
  constructor(memory, controlPtr) {
    if (!memory || !(memory.buffer instanceof SharedArrayBuffer)) bad("BAD_MEMORY");
    if (!Number.isInteger(controlPtr) || controlPtr < 0 || controlPtr % 64 || add(controlPtr, 256) > memory.buffer.byteLength) bad("BAD_CONTROL_POINTER");
    this.memory = memory; this.controlPtr = controlPtr; this.buffer = null;
    this.refresh(); this.validateControl();
  }
  refresh() {
    if (this.buffer === this.memory.buffer) return;
    this.buffer = this.memory.buffer;
    if (add(this.controlPtr, 256) > this.buffer.byteLength) bad("BAD_CONTROL_POINTER");
    this.control = new Int32Array(this.buffer, this.controlPtr, 64);
  }
  validateControl() {
    const h = this.control;
    if ((Atomics.load(h, 0) >>> 0) !== SNAPSHOT.MAGIC) bad("BAD_MAGIC");
    if ((Atomics.load(h, 1) >>> 0) !== SNAPSHOT.VERSION) bad("BAD_VERSION");
    if ((Atomics.load(h, 2) >>> 0) !== SNAPSHOT.BYTES || (Atomics.load(h, 3) >>> 0) !== SNAPSHOT.SLOTS || (Atomics.load(h, 4) >>> 0) !== SNAPSHOT.SLOT_BYTES || (Atomics.load(h, 5) >>> 0) !== SNAPSHOT.SCHEMA) bad("BAD_LAYOUT");
    const lifecycle = Atomics.load(h, 6) >>> 0;
    if (lifecycle > SNAPSHOT.CLOSED) bad("BAD_LIFECYCLE");
    if ((Atomics.load(h, 15) >>> 0) !== 0) bad("BAD_RESERVED");
  }
  latest() {
    this.refresh(); this.validateControl();
    for (let tries = 0; tries < 16; tries++) {
      const a = Atomics.load(this.control, 7) >>> 0;
      if (a & 1) continue;
      const lifecycle = Atomics.load(this.control, 6) >>> 0;
      const value = { lifecycle, epoch: Atomics.load(this.control, 8) >>> 0, slot: Atomics.load(this.control, 9) >>> 0, revisionLo: Atomics.load(this.control, 10) >>> 0, revisionHi: Atomics.load(this.control, 11) >>> 0, wasmPages: Atomics.load(this.control, 12) >>> 0, layoutEpoch: Atomics.load(this.control, 13) >>> 0, error: Atomics.load(this.control, 14) >>> 0 };
      const b = Atomics.load(this.control, 7) >>> 0;
      if (a === b && !(b & 1)) {
        if (lifecycle === SNAPSHOT.FAILED) bad("SNAPSHOT_FAILED");
        if (lifecycle === SNAPSHOT.CLOSED) bad("SNAPSHOT_CLOSED");
        if (lifecycle !== SNAPSHOT.INIT && lifecycle !== SNAPSHOT.OPEN) bad("BAD_LIFECYCLE");
        if (value.wasmPages && value.wasmPages > this.buffer.byteLength / 65536) bad("BAD_WASM_PAGES");
        return value;
      }
    }
    bad("UNSTABLE_CONTROL");
  }
  transaction(fn, expectedEpoch = 0) {
    this.refresh();
    const latest = this.latest();
    if (!latest.epoch || latest.slot >= SNAPSHOT.SLOTS || (expectedEpoch && latest.epoch !== expectedEpoch)) return null;
    let control = this.control;
    const base = 16 + latest.slot * 16;
    if (Atomics.compareExchange(control, base, SNAPSHOT.READY, SNAPSHOT.READING) !== SNAPSHOT.READY) return null;
    try {
      // memory.grow replaces memory.buffer even after the slot has been pinned.
      this.refresh(); control = this.control;
      const slot = Array.from({length: 16}, (_, i) => Atomics.load(control, base + i) >>> 0);
      if (slot[0] !== SNAPSHOT.READING || slot[1] !== latest.epoch || slot[2] !== latest.layoutEpoch || slot[5] !== latest.revisionLo || slot[6] !== latest.revisionHi || slot[9] !== SNAPSHOT.SCHEMA || slot[10] !== 64) return null;
      if (slot.slice(11).some(Boolean)) bad("BAD_SLOT_RESERVED");
      const ptr = slot[3], bytes = slot[4];
      if (ptr % 16 || bytes < 448 || bytes % 16 || add(ptr, bytes) > this.buffer.byteLength) bad("BAD_SLOT");
      const u32 = new Uint32Array(this.buffer, ptr, bytes / 4);
      if (u32[0] !== SNAPSHOT.BLOB_MAGIC || u32[1] !== SNAPSHOT.SCHEMA || u32[2] !== 64 || u32[3] !== bytes || u32[4] !== slot[1] || u32[5] !== slot[5] || u32[6] !== slot[6] || u32[7] !== 12 || u32[8] !== 64 || u32[9] !== 32 || u32[10] !== slot[7] || u32[11] !== slot[8] || u32[12] !== 0x01020304 || u32[13] !== 3) bad("BAD_BLOB");
      if (u32[14] || u32[15]) bad("BAD_BLOB_RESERVED");
      const ranges = [], streams = {};
      for (let i = 0; i < 12; i++) {
        const d = 16 + i * 8, semantic = u32[d], scalar = u32[d + 1], offset = u32[d + 2], count = u32[d + 3], components = u32[d + 4], stride = u32[d + 5], width = u32[d + 6], reserved = u32[d + 7];
        const want = i < 4 ? slot[7] : slot[8];
        if (semantic !== i + 1 || scalar !== SCALARS[i] || count !== want || components !== COMPONENTS[i] || stride !== STRIDES[i] || width !== 4 || reserved || offset < 448 || offset % 16) bad("BAD_DESCRIPTOR");
        const end = add(offset, mul(stride, count));
        if (end > bytes) bad("BAD_DESCRIPTOR_RANGE");
        if (count) ranges.push([offset, end]);
        const Type = scalar === 2 ? Float32Array : Uint32Array;
        streams[STREAM_NAMES[i]] = new Type(this.buffer, add(ptr, offset), mul(count, components));
      }
      ranges.sort((a, b) => a[0] - b[0]);
      for (let i = 1; i < ranges.length; i++) if (ranges[i][0] < ranges[i - 1][1]) bad("OVERLAPPING_STREAMS");
      return fn(Object.freeze({epoch: slot[1], revisionLo: slot[5], revisionHi: slot[6], meshCount: slot[7], instanceCount: slot[8], streams: Object.freeze(streams)}));
    } finally {
      Atomics.store(control, base, SNAPSHOT.FREE); Atomics.notify(control, base);
    }
  }
}
