// Defaults → wire layout (docs/multiplayer-plan.md §4 "input: defaults object = layout"). The hot
// stream — inputs and snapshot states — is binary and its shape comes from a plain defaults object:
//   number    → f32
//   boolean   → 1 bit (packed after the floats)
//   number[]  → f32 × length, fixed
// Nothing else: no strings, no nesting, no optionals — snapshot fields are flat and fixed-size by
// design. Both sides run the same bundle, so the layout is derived, never transmitted; the `sig`
// feeds the schema hash the handshake compares.

export type FieldKind = 0 | 1 | 2   // f32 · bool · f32[]
export type Field = { key: string; kind: FieldKind; len: number }
export type Layout = { fields: Field[]; floats: number; bools: number; bytes: number; sig: string }

const describe = (v: unknown): string => Array.isArray(v) ? `array of ${v.length}` : v === null ? "null" : typeof v

export const makeLayout = (defaults: Record<string, unknown>, what: string): Layout => {
  const fields: Field[] = []
  let floats = 0
  let bools = 0
  for (const key of Object.keys(defaults)) {
    const v = defaults[key]
    if (typeof v === "number") { fields.push({ key, kind: 0, len: 1 }); floats++ }
    else if (typeof v === "boolean") { fields.push({ key, kind: 1, len: 1 }); bools++ }
    else if (Array.isArray(v) && v.length > 0 && v.length <= 64 && v.every((n) => typeof n === "number")) {
      fields.push({ key, kind: 2, len: v.length }); floats += v.length
    } else {
      throw new Error(`${what}: field '${key}' must be a number, a boolean or a fixed-length number[] (got ${describe(v)})`)
    }
  }
  const sig = fields.map((f) => `${f.key}:${f.kind}${f.len > 1 ? f.len : ""}`).join(",")
  return { fields, floats, bools, bytes: floats * 4 + Math.ceil(bools / 8), sig }
}

/** A fresh state object shaped like the defaults (arrays copied, never shared). */
export const cloneDefaults = <T extends object>(defaults: T): T => {
  const out: Record<string, unknown> = {}
  for (const key of Object.keys(defaults)) {
    const v = (defaults as Record<string, unknown>)[key]
    out[key] = Array.isArray(v) ? v.slice() : v
  }
  return out as T
}

/** Copy the layout's fields from `from` into `into` (array elements copied in place; unknown keys
 *  ignored — a JSON spawn state or a partial `setInput`). */
export const assignState = (layout: Layout, into: any, from: any): void => {
  if (!from) return
  for (const f of layout.fields) {
    const v = from[f.key]
    if (v === undefined) continue
    if (f.kind === 2) {
      const a = into[f.key] as number[]
      for (let i = 0; i < f.len; i++) a[i] = +(v[i] ?? 0)
    } else if (f.kind === 1) into[f.key] = !!v
    else into[f.key] = +v
  }
}

export const packInto = (view: DataView, offset: number, layout: Layout, s: any): number => {
  let o = offset
  for (const f of layout.fields) {
    if (f.kind === 0) { view.setFloat32(o, s[f.key], true); o += 4 }
    else if (f.kind === 2) {
      const a = s[f.key] as number[]
      for (let i = 0; i < f.len; i++) { view.setFloat32(o, a[i] ?? 0, true); o += 4 }
    }
  }
  let bits = 0
  let n = 0
  for (const f of layout.fields) {
    if (f.kind !== 1) continue
    if (s[f.key]) bits |= 1 << (n & 7)
    n++
    if ((n & 7) === 0) { view.setUint8(o++, bits); bits = 0 }
  }
  if (n & 7) view.setUint8(o++, bits)
  return o
}

export const unpackFrom = (view: DataView, offset: number, layout: Layout, into: any): number => {
  let o = offset
  for (const f of layout.fields) {
    if (f.kind === 0) { into[f.key] = view.getFloat32(o, true); o += 4 }
    else if (f.kind === 2) {
      const a = into[f.key] as number[]
      for (let i = 0; i < f.len; i++) { a[i] = view.getFloat32(o, true); o += 4 }
    }
  }
  let bits = 0
  let n = 0
  for (const f of layout.fields) {
    if (f.kind !== 1) continue
    if ((n & 7) === 0) bits = view.getUint8(o++)
    into[f.key] = (bits & (1 << (n & 7))) !== 0
    n++
  }
  return o
}

/** `out = a + (b − a) · t` for the numeric fields; booleans take `b`'s. */
export const lerpState = (layout: Layout, a: any, b: any, t: number, out: any): void => {
  for (const f of layout.fields) {
    if (f.kind === 0) out[f.key] = a[f.key] + (b[f.key] - a[f.key]) * t
    else if (f.kind === 2) {
      const pa = a[f.key] as number[], pb = b[f.key] as number[], po = out[f.key] as number[]
      for (let i = 0; i < f.len; i++) po[i] = pa[i] + (pb[i] - pa[i]) * t
    } else out[f.key] = b[f.key]
  }
}

/** FNV-1a over a string → 8 hex chars. Enough to tell two bundles' schemas apart. */
export const hashString = (s: string): string => {
  let h = 0x811c9dc5
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i)
    h = Math.imul(h, 0x01000193)
  }
  return (h >>> 0).toString(16).padStart(8, "0")
}
