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.
 */
function getMaterialName(ifcApi: WebIFC.IfcAPI, modelID: number, matRef: any): string | null {
  if (!matRef) return null;
  const matId = matRef.value;
  if (!matId) return null;
  try {
    const matLine = ifcApi.GetLine(modelID, matId);
    if (!matLine) return null;
    
    if (matLine.Name && matLine.Name.value) {
      return matLine.Name.value;
    }
    
    if (matLine.Materials) {
      for (const mRef of matLine.Materials) {
        const name = getMaterialName(ifcApi, modelID, mRef);
        if (name) return name;
      }
    }
    
    if (matLine.MaterialConstituents) {
      for (const mcRef of matLine.MaterialConstituents) {
        const mcLine = ifcApi.GetLine(modelID, mcRef.value);
        if (mcLine && mcLine.Material) {
          const name = getMaterialName(ifcApi, modelID, mcLine.Material);
          if (name) return name;
        }
      }
    }
    
    if (matLine.MaterialProfileSet) {
      return getMaterialName(ifcApi, modelID, matLine.MaterialProfileSet);
    }
    if (matLine.MaterialProfiles) {
      for (const mpRef of matLine.MaterialProfiles) {
        const mpLine = ifcApi.GetLine(modelID, mpRef.value);
        if (mpLine && mpLine.Material) {
          const name = getMaterialName(ifcApi, modelID, mpLine.Material);
          if (name) return name;
        }
      }
    }
  } catch (err) {
    // Silencia erros de atributos inexistentes
  }
  return null;
}

export async function parseIFCtoThree(buffer: ArrayBuffer): Promise<THREE.Scene> {
  const ifcApi = new WebIFC.IfcAPI();
  ifcApi.SetWasmPath(WASM_PATH, true);
  await ifcApi.Init();

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

  // Mapeamento de materiais
  const elementMaterialMap = new Map<number, string>();
  try {
    const rels = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELASSOCIATESMATERIAL);
    for (let i = 0; i < rels.size(); i++) {
      const relId = rels.get(i);
      const rel = ifcApi.GetLine(modelID, relId);
      if (rel && rel.RelatedObjects && rel.RelatingMaterial) {
        const matName = getMaterialName(ifcApi, modelID, rel.RelatingMaterial);
        if (matName) {
          for (const objRef of rel.RelatedObjects) {
            elementMaterialMap.set(objRef.value, matName);
          }
        }
      }
    }
  } catch (err) {
    console.warn('[IFC Parser] Falha ao mapear materiais:', err);
  }

  // Mapeamento de propriedades adicionais
  const elementPropertiesMap = new Map<number, Record<string, string>>();
  try {
    const relsProp = ifcApi.GetLineIDsWithType(modelID, WebIFC.IFCRELDEFINESBYPROPERTIES);
    for (let i = 0; i < relsProp.size(); i++) {
      const relId = relsProp.get(i);
      const rel = ifcApi.GetLine(modelID, relId);
      if (rel && rel.RelatedObjects && rel.RelatingPropertyDefinition) {
        const propDefId = rel.RelatingPropertyDefinition.value;
        const propDef = ifcApi.GetLine(modelID, propDefId);
        
        if (propDef) {
          const props: Record<string, string> = {};
          
          if (propDef.HasProperties) {
            for (const pRef of propDef.HasProperties) {
              const pLine = ifcApi.GetLine(modelID, pRef.value);
              if (pLine && pLine.Name) {
                const name = pLine.Name.value;
                let value = '';
                if (pLine.NominalValue) {
                  value = String(pLine.NominalValue.value);
                }
                if (name && value) {
                  props[name] = value;
                }
              }
            }
          }
          
          if (propDef.Quantities) {
            for (const qRef of propDef.Quantities) {
              const qLine = ifcApi.GetLine(modelID, qRef.value);
              if (qLine && qLine.Name) {
                const name = qLine.Name.value;
                let value = '';
                if (qLine.LengthValue !== undefined) value = `${qLine.LengthValue.toFixed(1)} mm`;
                else if (qLine.AreaValue !== undefined) value = `${qLine.AreaValue.toFixed(1)} m²`;
                else if (qLine.VolumeValue !== undefined) value = `${qLine.VolumeValue.toFixed(2)} m³`;
                else if (qLine.NominalValue) value = String(qLine.NominalValue.value);
                
                if (name && value) {
                  props[name] = value;
                }
              }
            }
          }
          
          if (Object.keys(props).length > 0) {
            for (const objRef of rel.RelatedObjects) {
              const existing = elementPropertiesMap.get(objRef.value) ?? {};
              elementPropertiesMap.set(objRef.value, { ...existing, ...props });
            }
          }
        }
      }
    }
  } catch (err) {
    console.warn('[IFC Parser] Falha ao mapear propriedades:', err);
  }

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

  ifcApi.StreamAllMeshes(modelID, (mesh: WebIFC.FlatMesh) => {
    const placedGeometries = mesh.geometries;
    const expressID = (mesh as unknown as { expressID?: number }).expressID ?? 0;

    let name = '';
    let tag = '';
    let objectType = '';
    let description = '';
    try {
      const elementLine = ifcApi.GetLine(modelID, expressID);
      if (elementLine) {
        name = elementLine.Name?.value ?? '';
        tag = elementLine.Tag?.value ?? '';
        objectType = elementLine.ObjectType?.value ?? '';
        description = elementLine.Description?.value ?? '';
      }
    } catch (e) {}

    const materialName = elementMaterialMap.get(expressID) ?? '';
    const extraProps = elementPropertiesMap.get(expressID) ?? {};

    const elementGroup = new THREE.Group();
    elementGroup.name = `ifc_${expressID}`;
    elementGroup.userData = {
      ifcElement: true,
      ifcId: expressID,
      materialName,
      properties: {
        name,
        tag,
        objectType,
        description,
        material: materialName,
        ...extraProps
      }
    };

    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();
      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));

      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);
      mesh3.userData = { ifcId: expressID };

      const matrix = new THREE.Matrix4();
      matrix.fromArray(placedGeometry.flatTransformation);
      mesh3.applyMatrix4(matrix);

      elementGroup.add(mesh3);
      ifcGeometry.delete();
    }

    if (elementGroup.children.length > 0) scene.add(elementGroup);
  });

  ifcApi.CloseModel(modelID);
  return scene;
}

export async function convertIFCtoGLB(
  buffer: ArrayBuffer,
  fileName: string
): Promise<{ blob: Blob; fileName: string; fileSize: number }> {
  const scene = await parseIFCtoThree(buffer);

  // 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
  scene.traverse((child) => {
    if (child instanceof THREE.Mesh) {
      child.geometry.dispose();
    }
  });

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