/**
 * Lane J — a valid, tiny GLB built in memory.
 *
 * Exists so the drop pipeline can be exercised without a 13 MB farm asset:
 * `assets/` is NOT part of the vite build graph today (nothing copies it into
 * dist/), so the demo can't count on a real .glb being fetchable. This produces
 * a two-triangle, single-material, 0–1-UV quad — the shape a paintability check
 * should be happy with — as raw GLB bytes.
 *
 * Pure ArrayBuffer maths: no THREE, no DOM, runs under node.
 */

const MAGIC = 0x46546c67 // 'glTF'
const CHUNK_JSON = 0x4e4f534a // 'JSON'
const CHUNK_BIN = 0x004e4942 // 'BIN\0'

const pad4 = (n: number): number => (n + 3) & ~3

export interface TinyGlbOptions {
  /** Quad half-size in metres. Default 0.5 (a 1u-wide plate). */
  half?: number
  /** Material base colour as [r,g,b,a] in 0..1. Default a workshop orange. */
  color?: [number, number, number, number]
  /** Omit UVs to produce a deliberately unpaintable asset (for testing warnings). */
  withUv?: boolean
  /**
   * Omit the mesh entirely: a valid GLB whose scene holds one empty node. This
   * is what a Blender export with the mesh on a hidden collection looks like,
   * and the drop path has to refuse it rather than hide the original and put
   * nothing in its place.
   */
  withMesh?: boolean
}

export function buildTinyGlb(opts: TinyGlbOptions = {}): ArrayBuffer {
  const h = opts.half ?? 0.5
  const color = opts.color ?? [1, 0.58, 0, 1]
  const withUv = opts.withUv !== false
  const withMesh = opts.withMesh !== false

  const positions = new Float32Array([
    -h, 0, -h,
    h, 0, -h,
    h, 0, h,
    -h, 0, h,
  ])
  const uvs = new Float32Array([0, 0, 1, 0, 1, 1, 0, 1])
  const indices = new Uint16Array([0, 1, 2, 0, 2, 3])

  const posBytes = positions.byteLength // 48
  const uvBytes = withUv ? uvs.byteLength : 0 // 32
  const idxBytes = indices.byteLength // 12

  const posOffset = 0
  const uvOffset = posOffset + posBytes
  const idxOffset = pad4(uvOffset + uvBytes)
  const binLength = idxOffset + idxBytes

  const bin = new ArrayBuffer(pad4(binLength))
  new Uint8Array(bin, posOffset, posBytes).set(new Uint8Array(positions.buffer))
  if (withUv) new Uint8Array(bin, uvOffset, uvBytes).set(new Uint8Array(uvs.buffer))
  new Uint8Array(bin, idxOffset, idxBytes).set(new Uint8Array(indices.buffer))

  const bufferViews: unknown[] = [
    { buffer: 0, byteOffset: posOffset, byteLength: posBytes, target: 34962 },
  ]
  const accessors: unknown[] = [
    {
      bufferView: 0, componentType: 5126, count: 4, type: 'VEC3',
      min: [-h, 0, -h], max: [h, 0, h],
    },
  ]
  const attributes: Record<string, number> = { POSITION: 0 }
  if (withUv) {
    bufferViews.push({ buffer: 0, byteOffset: uvOffset, byteLength: uvBytes, target: 34962 })
    accessors.push({
      bufferView: bufferViews.length - 1, componentType: 5126, count: 4, type: 'VEC2',
      min: [0, 0], max: [1, 1],
    })
    attributes.TEXCOORD_0 = accessors.length - 1
  }
  bufferViews.push({ buffer: 0, byteOffset: idxOffset, byteLength: idxBytes, target: 34963 })
  accessors.push({
    bufferView: bufferViews.length - 1, componentType: 5123, count: 6, type: 'SCALAR',
    min: [0], max: [3],
  })
  const indexAccessor = accessors.length - 1

  const gltf = {
    asset: { version: '2.0', generator: 'BLOBBO workshop tiny-glb' },
    scene: 0,
    scenes: [{ nodes: [0] }],
    nodes: withMesh ? [{ mesh: 0, name: 'TinyQuad' }] : [{ name: 'EmptyNode' }],
    // glTF forbids an empty `meshes` array, so the key is dropped entirely.
    ...(withMesh
      ? {
        meshes: [{
          name: 'TinyQuad',
          primitives: [{ attributes, indices: indexAccessor, material: 0, mode: 4 }],
        }],
      }
      : {}),
    materials: [{
      name: 'TinyMat',
      pbrMetallicRoughness: { baseColorFactor: color, metallicFactor: 0, roughnessFactor: 0.6 },
    }],
    accessors,
    bufferViews,
    buffers: [{ byteLength: bin.byteLength }],
  }

  const jsonText = JSON.stringify(gltf)
  const jsonBytes = new TextEncoder().encode(jsonText)
  const jsonPadded = pad4(jsonBytes.length)

  const total = 12 + 8 + jsonPadded + 8 + bin.byteLength
  const out = new ArrayBuffer(total)
  const view = new DataView(out)
  const bytes = new Uint8Array(out)

  view.setUint32(0, MAGIC, true)
  view.setUint32(4, 2, true)
  view.setUint32(8, total, true)

  view.setUint32(12, jsonPadded, true)
  view.setUint32(16, CHUNK_JSON, true)
  bytes.set(jsonBytes, 20)
  // glTF requires JSON chunk padding to be spaces, BIN padding to be zeroes.
  for (let i = jsonBytes.length; i < jsonPadded; i++) bytes[20 + i] = 0x20

  const binChunkStart = 20 + jsonPadded
  view.setUint32(binChunkStart, bin.byteLength, true)
  view.setUint32(binChunkStart + 4, CHUNK_BIN, true)
  bytes.set(new Uint8Array(bin), binChunkStart + 8)

  return out
}

/** Cheap structural validation — the header checks GLTFLoader does first. */
export function isGlb(buffer: ArrayBuffer): boolean {
  if (buffer.byteLength < 20) return false
  const view = new DataView(buffer)
  return view.getUint32(0, true) === MAGIC &&
    view.getUint32(8, true) === buffer.byteLength
}
