/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

import type { MeshData } from '@ifc-lite/geometry';

/**
 * Minimal GLB parser for GLBs generated by IFC-Lite's GLTFExporter.
 * This is not a general-purpose glTF loader.
 */

type ParsedGlb = {
  json: any;
  bin: Uint8Array;
};

export function parseGLB(glb: Uint8Array): ParsedGlb {
  const dv = new DataView(glb.buffer, glb.byteOffset, glb.byteLength);
  const magic = dv.getUint32(0, true);
  if (magic !== 0x46546c67) throw new Error('Invalid GLB magic');
  const version = dv.getUint32(4, true);
  if (version !== 2) throw new Error(`Unsupported GLB version: ${version}`);
  const totalLen = dv.getUint32(8, true);
  if (totalLen > glb.byteLength) throw new Error('Invalid GLB length');

  let offset = 12;
  let json: any = null;
  let bin: Uint8Array | null = null;
  while (offset + 8 <= glb.byteLength) {
    const chunkLen = dv.getUint32(offset, true);
    const chunkType = dv.getUint32(offset + 4, true);
    offset += 8;
    const chunk = glb.subarray(offset, offset + chunkLen);
    offset += chunkLen;
    if (chunkType === 0x4e4f534a) {
      const txt = new TextDecoder().decode(chunk);
      json = JSON.parse(txt);
    } else if (chunkType === 0x004e4942) {
      bin = chunk;
    }
    // 4-byte alignment is already baked into chunkLen in GLB; offset is aligned.
  }

  if (!json || !bin) throw new Error('Invalid GLB: missing JSON or BIN chunk');
  return { json, bin };
}

function componentTypeSize(componentType: number): number {
  // Only types we emit
  if (componentType === 5126) return 4; // FLOAT
  if (componentType === 5125) return 4; // UNSIGNED_INT
  throw new Error(`Unsupported componentType: ${componentType}`);
}

function typeComponents(type: string): number {
  if (type === 'SCALAR') return 1;
  if (type === 'VEC2') return 2;
  if (type === 'VEC3') return 3;
  if (type === 'VEC4') return 4;
  throw new Error(`Unsupported accessor type: ${type}`);
}

export function extractGlbMapping(glb: Uint8Array): Record<string, { node: number; mesh: number }> {
  const { json } = parseGLB(glb);
  const out: Record<string, { node: number; mesh: number }> = {};
  const nodes: any[] = Array.isArray(json?.nodes) ? json.nodes : [];
  for (let i = 0; i < nodes.length; i++) {
    const n = nodes[i];
    const meshIdx = typeof n?.mesh === 'number' ? n.mesh : null;
    const expressId = n?.extras?.expressId;
    if (meshIdx !== null && typeof expressId === 'number') {
      out[String(expressId)] = { node: i, mesh: meshIdx };
    }
  }
  return out;
}

export function parseGLBToMeshData(glb: Uint8Array): MeshData[] {
  const { json, bin } = parseGLB(glb);
  const nodes: any[] = Array.isArray(json?.nodes) ? json.nodes : [];
  const meshes: any[] = Array.isArray(json?.meshes) ? json.meshes : [];
  const accessors: any[] = Array.isArray(json?.accessors) ? json.accessors : [];
  const bufferViews: any[] = Array.isArray(json?.bufferViews) ? json.bufferViews : [];
  const materials: any[] = Array.isArray(json?.materials) ? json.materials : [];

  const DEFAULT_COLOR: [number, number, number, number] = [0.8, 0.8, 0.8, 1];
  const resolveMaterialColor = (materialIdx: unknown): [number, number, number, number] => {
    if (typeof materialIdx !== 'number') return [...DEFAULT_COLOR];
    const factor = materials[materialIdx]?.pbrMetallicRoughness?.baseColorFactor;
    if (!Array.isArray(factor) || factor.length < 3) return [...DEFAULT_COLOR];
    const r = Number(factor[0]);
    const g = Number(factor[1]);
    const b = Number(factor[2]);
    const a = factor.length >= 4 ? Number(factor[3]) : 1;
    if (
      !Number.isFinite(r) ||
      !Number.isFinite(g) ||
      !Number.isFinite(b) ||
      !Number.isFinite(a)
    ) {
      return [...DEFAULT_COLOR];
    }
    return [r, g, b, a];
  };

  const readAccessor = (accessorIndex: number): { array: Float32Array | Uint32Array; count: number; components: number } => {
    const acc = accessors[accessorIndex];
    const bv = bufferViews[acc.bufferView];
    const byteOffset = (bv.byteOffset || 0) + (acc.byteOffset || 0);
    const count = Number(acc.count || 0);
    const comps = typeComponents(String(acc.type));
    const cSize = componentTypeSize(Number(acc.componentType));
    const byteLen = count * comps * cSize;
    if (byteOffset + byteLen > bin.byteLength) {
      throw new Error(`GLB accessor out of bounds: offset=${byteOffset} len=${byteLen} bufLen=${bin.byteLength}`);
    }
    const slice = bin.subarray(byteOffset, byteOffset + byteLen);
    // Ensure 4-byte alignment; copy to a new buffer if the slice is unaligned
    const aligned = (slice.byteOffset % 4 === 0)
      ? slice
      : new Uint8Array(slice);
    if (Number(acc.componentType) === 5126) {
      return { array: new Float32Array(aligned.buffer, aligned.byteOffset, aligned.byteLength / 4), count, components: comps };
    }
    if (Number(acc.componentType) === 5125) {
      return { array: new Uint32Array(aligned.buffer, aligned.byteOffset, aligned.byteLength / 4), count, components: comps };
    }
    throw new Error('Unsupported accessor component type');
  };

  const out: MeshData[] = [];
  for (const node of nodes) {
    const meshIdx = typeof node?.mesh === 'number' ? node.mesh : null;
    if (meshIdx === null) continue;
    const mesh = meshes[meshIdx];
    const prim = Array.isArray(mesh?.primitives) ? mesh.primitives[0] : null;
    if (!prim) continue;

    const posAcc = prim.attributes?.POSITION;
    const normAcc = prim.attributes?.NORMAL;
    const idxAcc = prim.indices;
    if (typeof posAcc !== 'number' || typeof normAcc !== 'number' || typeof idxAcc !== 'number') continue;

    const pos = readAccessor(posAcc).array as Float32Array;
    const nor = readAccessor(normAcc).array as Float32Array;
    const ind = readAccessor(idxAcc).array as Uint32Array;

    const expressId = typeof node?.extras?.expressId === 'number' ? node.extras.expressId : 0;
    out.push({
      expressId,
      positions: new Float32Array(pos),
      normals: new Float32Array(nor),
      indices: new Uint32Array(ind),
      color: resolveMaterialColor(prim.material),
      ifcType: 'IfcElement',
    });
  }
  return out;
}

