// The `cells('…')` payload literal used by `.scene2d.ts` tilemap blocks (and terrain material maps
// later): one opaque base64 string holding the grid dimensions + row-major Int32 cell values, so a
// whole map prints on one line of the scene file. Layout (little-endian Int32): [cols, rows,
// runLen₁, value₁, runLen₂, value₂, …] — run-length pairs, because tilemaps are dominated by long
// runs of the same tile (a 100×100 grass field is a handful of pairs, not 10k ints). The payload is
// versionless by design: cols is always > 0, so a future format can claim a negative sentinel.
//
// Self-contained on purpose — QuickJS has no atob/Buffer, and the editor keeps its own mirrored
// copy of this codec (the scene-doc/editor side must not depend on the SDK).

export type CellsData = {
  cols: number
  rows: number
  /** Row-major tile values (row 0 = the top row), length cols*rows. */
  data: Int32Array
}

const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
let B64_REV: Int8Array | null = null

const b64encode = (bytes: Uint8Array): string => {
  let out = ""
  for (let i = 0; i < bytes.length; i += 3) {
    const b0 = bytes[i]!, b1 = bytes[i + 1], b2 = bytes[i + 2]
    out += B64[b0 >> 2]! + B64[((b0 & 3) << 4) | ((b1 ?? 0) >> 4)]!
    out += b1 === undefined ? "=" : B64[((b1 & 15) << 2) | ((b2 ?? 0) >> 6)]!
    out += b2 === undefined ? "=" : B64[b2 & 63]!
  }
  return out
}

const b64decode = (s: string): Uint8Array => {
  if (!B64_REV) {
    B64_REV = new Int8Array(128).fill(-1)
    for (let i = 0; i < 64; i++) B64_REV[B64.charCodeAt(i)] = i
  }
  const clean = s.replace(/=+$/, "")
  const out = new Uint8Array(Math.floor(clean.length * 3 / 4))
  let acc = 0, bits = 0, w = 0
  for (let i = 0; i < clean.length; i++) {
    const v = B64_REV[clean.charCodeAt(i)] ?? -1
    if (v < 0) throw new Error("cells(): payload is not base64")
    acc = (acc << 6) | v
    bits += 6
    if (bits >= 8) { bits -= 8; out[w++] = (acc >> bits) & 0xff }
  }
  return out
}

/** Encode a grid into a `cells()` payload (the editor / tools side of the codec). */
export const encodeCells = (cols: number, rows: number, data: ArrayLike<number>): string => {
  if (data.length !== cols * rows) throw new Error(`encodeCells: expected ${cols * rows} values, got ${data.length}`)
  const ints: number[] = [ cols, rows ]
  for (let i = 0; i < data.length;) {
    const v = data[i]!
    let run = 1
    while (i + run < data.length && data[i + run] === v) run++
    ints.push(run, v)
    i += run
  }
  const bytes = new Uint8Array(ints.length * 4)
  const dv = new DataView(bytes.buffer)
  ints.forEach((n, i) => dv.setInt32(i * 4, n, true))
  return b64encode(bytes)
}

/**
 * Decode a `cells('…')` payload into the grid a tilemap block consumes. Scene files call this via
 * the injected global; the value is plain data, so hand-written code may pass any
 * `{ cols, rows, data }` of its own instead (procedural maps).
 */
export const cells = (payload: string): CellsData => {
  const bytes = b64decode(payload)
  if (bytes.length < 16 || bytes.length % 4 !== 0) throw new Error("cells(): malformed payload")
  const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
  const cols = dv.getInt32(0, true)
  const rows = dv.getInt32(4, true)
  const count = bytes.length / 4 - 2
  if (cols <= 0 || rows <= 0 || count % 2 !== 0) throw new Error("cells(): malformed payload")
  const data = new Int32Array(cols * rows)
  let w = 0
  for (let i = 0; i < count; i += 2) {
    const run = dv.getInt32(8 + i * 4, true)
    const value = dv.getInt32(12 + i * 4, true)
    if (run <= 0 || w + run > data.length) throw new Error("cells(): payload does not match its dimensions")
    data.fill(value, w, w + run)
    w += run
  }
  if (w !== data.length) throw new Error("cells(): payload does not match its dimensions")
  return { cols, rows, data }
}
