import * as THREE from 'three';
import * as WebIFC from 'web-ifc';
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';

const WASM_PATH = 'https://unpkg.com/web-ifc@0.0.57/';

/**
 * Convert an IFC file (ArrayBuffer) into a GLB Blob using web-ifc + GLTFExporter.
 */
export async function convertIFCtoGLB(
  buffer: ArrayBuffer,
  fileName: string
): Promise<{ blob: Blob; fileName: string; fileSize: number }> {
  const ifcApi = new WebIFC.IfcAPI();
  ifcApi.SetWasmPath(WASM_PATH, true);
  await ifcApi.Init();

  const data = new Uint8Array(buffer);
  const modelID = ifcApi.OpenModel(data);

  const scene = new THREE.Scene();
  const materials: Map<number, THREE.MeshStandardMaterial> = new Map();

  ifcApi.StreamAllMeshes(modelID, (mesh: WebIFC.FlatMesh) => {
    const placedGeometries = mesh.geometries;

    for (let i = 0; i < placedGeometries.size(); i++) {
      const placedGeometry = placedGeometries.get(i);
      const ifcGeometry = ifcApi.GetGeometry(modelID, placedGeometry.geometryExpressID);

      const verts = ifcApi.GetVertexArray(
        ifcGeometry.GetVertexData(),
        ifcGeometry.GetVertexDataSize()
      );
      const indices = ifcApi.GetIndexArray(
        ifcGeometry.GetIndexData(),
        ifcGeometry.GetIndexDataSize()
      );

      const geometry = new THREE.BufferGeometry();

      // web-ifc vertex format: x, y, z, nx, ny, nz (6 floats per vertex)
      const positionArray = new Float32Array(verts.length / 2);
      const normalArray = new Float32Array(verts.length / 2);

      for (let j = 0; j < verts.length; j += 6) {
        const idx = j / 6;
        positionArray[idx * 3] = verts[j];
        positionArray[idx * 3 + 1] = verts[j + 1];
        positionArray[idx * 3 + 2] = verts[j + 2];
        normalArray[idx * 3] = verts[j + 3];
        normalArray[idx * 3 + 1] = verts[j + 4];
        normalArray[idx * 3 + 2] = verts[j + 5];
      }

      geometry.setAttribute('position', new THREE.BufferAttribute(positionArray, 3));
      geometry.setAttribute('normal', new THREE.BufferAttribute(normalArray, 3));
      geometry.setIndex(new THREE.BufferAttribute(indices, 1));

      // Get or create material based on color
      const color = placedGeometry.color;
      const colorKey = (color.x * 255) << 16 | (color.y * 255) << 8 | (color.z * 255);

      let material = materials.get(colorKey);
      if (!material) {
        material = new THREE.MeshStandardMaterial({
          color: new THREE.Color(color.x, color.y, color.z),
          metalness: 0.2,
          roughness: 0.8,
          transparent: color.w < 1,
          opacity: color.w,
          side: THREE.DoubleSide,
        });
        materials.set(colorKey, material);
      }

      const mesh3 = new THREE.Mesh(geometry, material);

      // Apply placement transform
      const matrix = new THREE.Matrix4();
      matrix.fromArray(placedGeometry.flatTransformation);
      mesh3.applyMatrix4(matrix);

      scene.add(mesh3);

      ifcGeometry.delete();
    }
  });

  ifcApi.CloseModel(modelID);

  // Export to GLB
  const exporter = new GLTFExporter();
  const glb = await new Promise<ArrayBuffer>((resolve, reject) => {
    exporter.parse(
      scene,
      (result) => resolve(result as ArrayBuffer),
      (error) => reject(error),
      { binary: true }
    );
  });

  const glbFileName = fileName.replace(/\.ifc$/i, '.glb');
  const blob = new Blob([glb], { type: 'model/gltf-binary' });

  // Cleanup
  materials.forEach((m) => m.dispose());
  scene.traverse((child) => {
    if (child instanceof THREE.Mesh) {
      child.geometry.dispose();
    }
  });

  return { blob, fileName: glbFileName, fileSize: blob.size };
}
