import * as WebIFC from 'web-ifc';
import { IFCSPACE, IFCOPENINGELEMENT, IFCPRODUCTDEFINITIONSHAPE, IFCRELAGGREGATES, IFCRELCONTAINEDINSPATIALSTRUCTURE, IFCRELDEFINESBYPROPERTIES, IFCRELASSOCIATESMATERIAL, IFCRELDEFINESBYTYPE, IFCPROJECT, IFCBUILDING } from 'web-ifc';
import { Mesh, Color, MeshLambertMaterial, DoubleSide, Matrix4, BufferGeometry, BufferAttribute, Loader, FileLoader } from 'three';
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';

const nullIfcManagerErrorMessage = 'IfcManager is null!';

class IFCModel extends Mesh {

  constructor() {
    super(...arguments);
    this.modelID = IFCModel.modelIdCounter++;
    this.ifcManager = null;
    this.mesh = this;
  }

  static dispose() {
    IFCModel.modelIdCounter = 0;
  }

  setIFCManager(manager) {
    this.ifcManager = manager;
  }

  setWasmPath(path, absolute = false) {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    this.ifcManager.setWasmPath(path, absolute);
  }

  close(scene) {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    this.ifcManager.close(this.modelID, scene);
  }

  getExpressId(geometry, faceIndex) {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    return this.ifcManager.getExpressId(geometry, faceIndex);
  }

  getAllItemsOfType(type, verbose) {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    return this.ifcManager.getAllItemsOfType(this.modelID, type, verbose);
  }

  getItemProperties(id, recursive = false) {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    return this.ifcManager.getItemProperties(this.modelID, id, recursive);
  }

  getPropertySets(id, recursive = false) {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    return this.ifcManager.getPropertySets(this.modelID, id, recursive);
  }

  getTypeProperties(id, recursive = false) {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    return this.ifcManager.getTypeProperties(this.modelID, id, recursive);
  }

  getIfcType(id) {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    return this.ifcManager.getIfcType(this.modelID, id);
  }

  getSpatialStructure() {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    return this.ifcManager.getSpatialStructure(this.modelID);
  }

  getSubset(material) {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    return this.ifcManager.getSubset(this.modelID, material);
  }

  removeSubset(material, customID) {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    this.ifcManager.removeSubset(this.modelID, material, customID);
  }

  createSubset(config) {
    if (this.ifcManager === null)
      throw new Error(nullIfcManagerErrorMessage);
    const modelConfig = {
      ...config,
      modelID: this.modelID
    };
    return this.ifcManager.createSubset(modelConfig);
  }

}

IFCModel.modelIdCounter = 0;

class IFCParser {

  constructor(state, BVH) {
    this.state = state;
    this.BVH = BVH;
    this.loadedModels = 0;
    this.optionalCategories = {
      [IFCSPACE]: true,
      [IFCOPENINGELEMENT]: false
    };
    this.geometriesByMaterials = {};
    this.loadingState = {
      total: 0,
      current: 0,
      step: 0.1
    };
    this.currentWebIfcID = -1;
    this.currentModelID = -1;
  }

  async setupOptionalCategories(config) {
    this.optionalCategories = config;
  }

  async parse(buffer, coordinationMatrix) {
    if (this.state.api.wasmModule === undefined)
      await this.state.api.Init();
    await this.newIfcModel(buffer);
    this.loadedModels++;
    if (coordinationMatrix) {
      await this.state.api.SetGeometryTransformation(this.currentWebIfcID, coordinationMatrix);
    }
    return this.loadAllGeometry(this.currentWebIfcID);
  }

  getAndClearErrors(_modelId) {}

  notifyProgress(loaded, total) {
    if (this.state.onProgress)
      this.state.onProgress({
        loaded,
        total
      });
  }

  async newIfcModel(buffer) {
    const data = new Uint8Array(buffer);
    this.currentWebIfcID = await this.state.api.OpenModel(data, this.state.webIfcSettings);
    this.currentModelID = this.state.useJSON ? this.loadedModels : this.currentWebIfcID;
    this.state.models[this.currentModelID] = {
      modelID: this.currentModelID,
      mesh: {},
      types: {},
      jsonData: {}
    };
  }

  async loadAllGeometry(modelID) {
    this.addOptionalCategories(modelID);
    await this.initializeLoadingState(modelID);
    this.state.api.StreamAllMeshes(modelID, (mesh) => {
      this.updateLoadingState();
      this.streamMesh(modelID, mesh);
    });
    this.notifyLoadingEnded();
    const geometries = [];
    const materials = [];
    Object.keys(this.geometriesByMaterials).forEach((key) => {
      const geometriesByMaterial = this.geometriesByMaterials[key].geometries;
      const merged = mergeGeometries(geometriesByMaterial);
      materials.push(this.geometriesByMaterials[key].material);
      geometries.push(merged);
    });
    const combinedGeometry = mergeGeometries(geometries, true);
    this.cleanUpGeometryMemory(geometries);
    if (this.BVH)
      this.BVH.applyThreeMeshBVH(combinedGeometry);
    const model = new IFCModel(combinedGeometry, materials);
    this.state.models[this.currentModelID].mesh = model;
    return model;
  }

  async initializeLoadingState(modelID) {
    const shapes = await this.state.api.GetLineIDsWithType(modelID, IFCPRODUCTDEFINITIONSHAPE);
    this.loadingState.total = shapes.size();
    this.loadingState.current = 0;
    this.loadingState.step = 0.1;
  }

  notifyLoadingEnded() {
    this.notifyProgress(this.loadingState.total, this.loadingState.total);
  }

  updateLoadingState() {
    const realCurrentItem = Math.min(this.loadingState.current++, this.loadingState.total);
    if (realCurrentItem / this.loadingState.total >= this.loadingState.step) {
      const currentProgress = Math.ceil(this.loadingState.total * this.loadingState.step);
      this.notifyProgress(currentProgress, this.loadingState.total);
      this.loadingState.step += 0.1;
    }
  }

  addOptionalCategories(modelID) {
    const optionalTypes = [];
    for (let key in this.optionalCategories) {
      if (this.optionalCategories.hasOwnProperty(key)) {
        const category = parseInt(key);
        if (this.optionalCategories[category])
          optionalTypes.push(category);
      }
    }
    this.state.api.StreamAllMeshesWithTypes(this.currentWebIfcID, optionalTypes, (mesh) => {
      this.streamMesh(modelID, mesh);
    });
  }

  streamMesh(modelID, mesh) {
    const placedGeometries = mesh.geometries;
    const size = placedGeometries.size();
    for (let i = 0; i < size; i++) {
      const placedGeometry = placedGeometries.get(i);
      let itemMesh = this.getPlacedGeometry(modelID, mesh.expressID, placedGeometry);
      let geom = itemMesh.geometry.applyMatrix4(itemMesh.matrix);
      this.storeGeometryByMaterial(placedGeometry.color, geom);
    }
  }

  getPlacedGeometry(modelID, expressID, placedGeometry) {
    const geometry = this.getBufferGeometry(modelID, expressID, placedGeometry);
    const mesh = new Mesh(geometry);
    mesh.matrix = this.getMeshMatrix(placedGeometry.flatTransformation);
    mesh.matrixAutoUpdate = false;
    return mesh;
  }

  getBufferGeometry(modelID, expressID, placedGeometry) {
    const geometry = this.state.api.GetGeometry(modelID, placedGeometry.geometryExpressID);
    const verts = this.state.api.GetVertexArray(geometry.GetVertexData(), geometry.GetVertexDataSize());
    const indices = this.state.api.GetIndexArray(geometry.GetIndexData(), geometry.GetIndexDataSize());
    const buffer = this.ifcGeometryToBuffer(expressID, verts, indices);
    geometry.delete();
    return buffer;
  }

  storeGeometryByMaterial(color, geometry) {
    let colID = `${color.x}${color.y}${color.z}${color.w}`;
    if (this.geometriesByMaterials[colID]) {
      this.geometriesByMaterials[colID].geometries.push(geometry);
      return;
    }
    const col = new Color().setRGB(color.x, color.y, color.z, 'srgb');
    const material = new MeshLambertMaterial({
      color: col,
      side: DoubleSide
    });
    material.transparent = color.w !== 1;
    if (material.transparent)
      material.opacity = color.w;
    this.geometriesByMaterials[colID] = {
      material,
      geometries: [geometry]
    };
  }

  getMeshMatrix(matrix) {
    const mat = new Matrix4();
    mat.fromArray(matrix);
    return mat;
  }

  ifcGeometryToBuffer(expressID, vertexData, indexData) {
    const geometry = new BufferGeometry();
    const posFloats = new Float32Array(vertexData.length / 2);
    const normFloats = new Float32Array(vertexData.length / 2);
    const idAttribute = new Uint32Array(vertexData.length / 6);
    for (let i = 0; i < vertexData.length; i += 6) {
      posFloats[i / 2] = vertexData[i];
      posFloats[i / 2 + 1] = vertexData[i + 1];
      posFloats[i / 2 + 2] = vertexData[i + 2];
      normFloats[i / 2] = vertexData[i + 3];
      normFloats[i / 2 + 1] = vertexData[i + 4];
      normFloats[i / 2 + 2] = vertexData[i + 5];
      idAttribute[i / 6] = expressID;
    }
    geometry.setAttribute('position', new BufferAttribute(posFloats, 3));
    geometry.setAttribute('normal', new BufferAttribute(normFloats, 3));
    geometry.setAttribute('expressID', new BufferAttribute(idAttribute, 1));
    geometry.setIndex(new BufferAttribute(indexData, 1));
    return geometry;
  }

  cleanUpGeometryMemory(geometries) {
    geometries.forEach(geometry => geometry.dispose());
    Object.keys(this.geometriesByMaterials).forEach((materialID) => {
      const geometriesByMaterial = this.geometriesByMaterials[materialID];
      geometriesByMaterial.geometries.forEach(geometry => geometry.dispose());
      geometriesByMaterial.geometries = [];
      geometriesByMaterial.material = null;
    });
    this.geometriesByMaterials = {};
  }

}

class ItemsMap {

  constructor(state) {
    this.state = state;
    this.map = {};
  }

  generateGeometryIndexMap(modelID) {
    if (this.map[modelID])
      return;
    const geometry = this.getGeometry(modelID);
    const items = this.newItemsMap(modelID, geometry);
    for (const group of geometry.groups) {
      this.fillItemsWithGroupInfo(group, geometry, items);
    }
  }

  getSubsetID(modelID, material, customID = 'DEFAULT') {
    const baseID = modelID;
    const materialID = material ? material.uuid : 'DEFAULT';
    return `${baseID} - ${materialID} - ${customID}`;
  }

  dispose() {
    Object.values(this.map).forEach(model => {
      model.indexCache = null;
      model.map = null;
    });
    this.map = null;
  }

  getGeometry(modelID) {
    const geometry = this.state.models[modelID].mesh.geometry;
    if (!geometry)
      throw new Error('Model without geometry.');
    if (!geometry.index)
      throw new Error('Geometry must be indexed');
    return geometry;
  }

  newItemsMap(modelID, geometry) {
    const startIndices = geometry.index.array;
    this.map[modelID] = {
      indexCache: startIndices.slice(0, geometry.index.array.length),
      map: new Map()
    };
    return this.map[modelID];
  }

  fillItemsWithGroupInfo(group, geometry, items) {
    let prevExpressID = -1;
    const materialIndex = group.materialIndex;
    const materialStart = group.start;
    const materialEnd = materialStart + group.count - 1;
    let objectStart = -1;
    let objectEnd = -1;
    for (let i = materialStart; i <= materialEnd; i++) {
      const index = geometry.index.array[i];
      const bufferAttr = geometry.attributes.expressID;
      const expressID = bufferAttr.array[index];
      if (prevExpressID === -1) {
        prevExpressID = expressID;
        objectStart = i;
      }
      const isEndOfMaterial = i === materialEnd;
      if (isEndOfMaterial) {
        const store = this.getMaterialStore(items.map, expressID, materialIndex);
        store.push(objectStart, materialEnd);
        break;
      }
      if (prevExpressID === expressID)
        continue;
      const store = this.getMaterialStore(items.map, prevExpressID, materialIndex);
      objectEnd = i - 1;
      store.push(objectStart, objectEnd);
      prevExpressID = expressID;
      objectStart = i;
    }
  }

  getMaterialStore(map, id, matIndex) {
    if (map.get(id) === undefined) {
      map.set(id, {});
    }
    const storedIfcItem = map.get(id);
    if (storedIfcItem === undefined)
      throw new Error('Geometry map generation error');
    if (storedIfcItem[matIndex] === undefined) {
      storedIfcItem[matIndex] = [];
    }
    return storedIfcItem[matIndex];
  }

}

class SubsetUtils {

  static getAllIndicesOfGroup(modelID, ids, materialIndex, items, flatten = true) {
    const indicesByGroup = [];
    for (const expressID of ids) {
      const entry = items.map.get(expressID);
      if (!entry)
        continue;
      const value = entry[materialIndex];
      if (!value)
        continue;
      SubsetUtils.getIndexChunk(value, indicesByGroup, materialIndex, items, flatten);
    }
    return indicesByGroup;
  }

  static getIndexChunk(value, indicesByGroup, materialIndex, items, flatten) {
    const pairs = value.length / 2;
    for (let pair = 0; pair < pairs; pair++) {
      const pairIndex = pair * 2;
      const start = value[pairIndex];
      const end = value[pairIndex + 1];
      for (let j = start; j <= end; j++) {
        if (flatten)
          indicesByGroup.push(items.indexCache[j]);
        else {
          if (!indicesByGroup[materialIndex])
            indicesByGroup[materialIndex] = [];
          indicesByGroup[materialIndex].push(items.indexCache[j]);
        }
      }
    }
  }

}

class SubsetCreator {

  constructor(state, items, subsets, BVH) {
    this.state = state;
    this.items = items;
    this.subsets = subsets;
    this.BVH = BVH;
    this.tempIndex = [];
  }

  createSubset(config, subsetID) {
    if (!this.items.map[config.modelID])
      this.items.generateGeometryIndexMap(config.modelID);
    if (!this.subsets[subsetID])
      this.initializeSubset(config, subsetID);
    this.filterIndices(config, subsetID);
    this.constructSubsetByMaterial(config, subsetID);
    config.ids.forEach(id => this.subsets[subsetID].ids.add(id));
    this.subsets[subsetID].mesh.geometry.setIndex(this.tempIndex);
    this.tempIndex.length = 0;
    const subset = this.subsets[subsetID].mesh;
    if (config.applyBVH)
      this.BVH.applyThreeMeshBVH(subset.geometry);
    if (config.scene)
      config.scene.add(subset);
    return this.subsets[subsetID].mesh;
  }

  dispose() {
    this.tempIndex = [];
  }

  initializeSubset(config, subsetID) {
    const model = this.state.models[config.modelID].mesh;
    const subsetGeom = new BufferGeometry();
    this.initializeSubsetAttributes(subsetGeom, model);
    if (!config.material)
      this.initializeSubsetGroups(subsetGeom, model);
    const mesh = new Mesh(subsetGeom, config.material || model.material);
    mesh.modelID = config.modelID;
    const bvh = Boolean(config.applyBVH);
    this.subsets[subsetID] = {
      ids: new Set(),
      mesh,
      bvh
    };
    model.add(mesh);
  }

  initializeSubsetAttributes(subsetGeom, model) {
    subsetGeom.setAttribute('position', model.geometry.attributes.position);
    subsetGeom.setAttribute('normal', model.geometry.attributes.normal);
    subsetGeom.setAttribute('expressID', model.geometry.attributes.expressID);
    subsetGeom.setIndex([]);
  }

  initializeSubsetGroups(subsetGeom, model) {
    subsetGeom.groups = JSON.parse(JSON.stringify(model.geometry.groups));
    this.resetGroups(subsetGeom);
  }

  filterIndices(config, subsetID) {
    const geometry = this.subsets[subsetID].mesh.geometry;
    if (config.removePrevious) {
      geometry.setIndex([]);
      this.resetGroups(geometry);
      return;
    }
    const previousIndices = geometry.index.array;
    const previousIDs = this.subsets[subsetID].ids;
    config.ids = config.ids.filter(id => !previousIDs.has(id));
    this.tempIndex = Array.from(previousIndices);
  }

  constructSubsetByMaterial(config, subsetID) {
    const model = this.state.models[config.modelID].mesh;
    const newIndices = {
      count: 0
    };
    for (let i = 0; i < model.geometry.groups.length; i++) {
      this.insertNewIndices(config, subsetID, i, newIndices);
    }
  }

  insertNewIndices(config, subsetID, materialIndex, newIndices) {
    const items = this.items.map[config.modelID];
    const indicesOfOneMaterial = SubsetUtils.getAllIndicesOfGroup(config.modelID, config.ids, materialIndex, items);
    if (!config.material) {
      this.insertIndicesAtGroup(subsetID, indicesOfOneMaterial, materialIndex, newIndices);
    } else {
      indicesOfOneMaterial.forEach(index => this.tempIndex.push(index));
    }
  }

  insertIndicesAtGroup(subsetID, indicesByGroup, index, newIndices) {
    const currentGroup = this.getCurrentGroup(subsetID, index);
    currentGroup.start += newIndices.count;
    let newIndicesPosition = currentGroup.start + currentGroup.count;
    newIndices.count += indicesByGroup.length;
    if (indicesByGroup.length > 0) {
      let position = newIndicesPosition;
      const start = this.tempIndex.slice(0, position);
      const end = this.tempIndex.slice(position);
      this.tempIndex = Array.prototype.concat.apply([], [start, indicesByGroup, end]);
      currentGroup.count += indicesByGroup.length;
    }
  }

  getCurrentGroup(subsetID, groupIndex) {
    const geometry = this.subsets[subsetID].mesh.geometry;
    return geometry.groups[groupIndex];
  }

  resetGroups(geometry) {
    geometry.groups.forEach((group) => {
      group.start = 0;
      group.count = 0;
    });
  }

}

class SubsetManager {

  constructor(state, BVH) {
    this.subsets = {};
    this.state = state;
    this.items = new ItemsMap(state);
    this.BVH = BVH;
    this.subsetCreator = new SubsetCreator(state, this.items, this.subsets, this.BVH);
  }

  getAllSubsets() {
    return this.subsets;
  }

  getSubset(modelID, material, customId) {
    const subsetID = this.getSubsetID(modelID, material, customId);
    return this.subsets[subsetID].mesh;
  }

  removeSubset(modelID, material, customID) {
    const subsetID = this.getSubsetID(modelID, material, customID);
    const subset = this.subsets[subsetID];
    if (!subset)
      return;
    if (subset.mesh.parent)
      subset.mesh.removeFromParent();
    subset.mesh.geometry.attributes = {};
    subset.mesh.geometry.index = null;
    subset.mesh.geometry.dispose();
    subset.mesh.geometry = null;
    delete this.subsets[subsetID];
  }

  createSubset(config) {
    const subsetID = this.getSubsetID(config.modelID, config.material, config.customID);
    return this.subsetCreator.createSubset(config, subsetID);
  }

  removeFromSubset(modelID, ids, customID, material) {
    const subsetID = this.getSubsetID(modelID, material, customID);
    if (!this.subsets[subsetID])
      return;
    const previousIDs = this.subsets[subsetID].ids;
    ids.forEach((id) => {
      if (previousIDs.has(id))
        previousIDs.delete(id);
    });
    return this.createSubset({
      modelID,
      removePrevious: true,
      material,
      customID,
      applyBVH: this.subsets[subsetID].bvh,
      ids: Array.from(previousIDs),
      scene: this.subsets[subsetID].mesh.parent
    });
  }

  clearSubset(modelID, customID, material) {
    const subsetID = this.getSubsetID(modelID, material, customID);
    if (!this.subsets[subsetID])
      return;
    this.subsets[subsetID].ids.clear();
    const subset = this.getSubset(modelID, material, customID);
    subset.geometry.setIndex([]);
  }

  dispose() {
    this.items.dispose();
    this.subsetCreator.dispose();
    Object.values(this.subsets).forEach(subset => {
      subset.ids = null;
      subset.mesh.removeFromParent();
      const mats = subset.mesh.material;
      if (Array.isArray(mats))
        mats.forEach(mat => mat.dispose());
      else
        mats.dispose();
      subset.mesh.geometry.index = null;
      subset.mesh.geometry.dispose();
      const geom = subset.mesh.geometry;
      if (geom.disposeBoundsTree)
        geom.disposeBoundsTree();
      subset.mesh = null;
    });
    this.subsets = null;
  }

  getSubsetID(modelID, material, customID = 'DEFAULT') {
    const baseID = modelID;
    const materialID = material ? material.uuid : 'DEFAULT';
    return `${baseID} - ${materialID} - ${customID}`;
  }

}

const IdAttrName = 'expressID';
const PropsNames = {
  aggregates: {
    name: IFCRELAGGREGATES,
    relating: 'RelatingObject',
    related: 'RelatedObjects',
    key: 'children'
  },
  spatial: {
    name: IFCRELCONTAINEDINSPATIALSTRUCTURE,
    relating: 'RelatingStructure',
    related: 'RelatedElements',
    key: 'children'
  },
  psets: {
    name: IFCRELDEFINESBYPROPERTIES,
    relating: 'RelatingPropertyDefinition',
    related: 'RelatedObjects',
    key: 'hasPsets'
  },
  materials: {
    name: IFCRELASSOCIATESMATERIAL,
    relating: 'RelatingMaterial',
    related: 'RelatedObjects',
    key: 'hasMaterial'
  },
  type: {
    name: IFCRELDEFINESBYTYPE,
    relating: 'RelatingType',
    related: 'RelatedObjects',
    key: 'hasType'
  }
};

class BasePropertyManager {

  constructor(state) {
    this.state = state;
  }

  async getPropertySets(modelID, elementID, recursive = false) {
    return await this.getProperty(modelID, elementID, recursive, PropsNames.psets);
  }

  async getTypeProperties(modelID, elementID, recursive = false) {
    return await this.getProperty(modelID, elementID, recursive, PropsNames.type);
  }

  async getMaterialsProperties(modelID, elementID, recursive = false) {
    return await this.getProperty(modelID, elementID, recursive, PropsNames.materials);
  }

  async getSpatialNode(modelID, node, treeChunks, includeProperties) {
    await this.getChildren(modelID, node, treeChunks, PropsNames.aggregates, includeProperties);
    await this.getChildren(modelID, node, treeChunks, PropsNames.spatial, includeProperties);
  }

  async getChildren(modelID, node, treeChunks, propNames, includeProperties) {
    const children = treeChunks[node.expressID];
    if (children == undefined)
      return;
    const prop = propNames.key;
    const nodes = [];
    for (let i = 0; i < children.length; i++) {
      const child = children[i];
      let node = this.newNode(modelID, child);
      if (includeProperties) {
        const properties = await this.getItemProperties(modelID, node.expressID);
        node = {
          ...properties, ...node
        };
      }
      await this.getSpatialNode(modelID, node, treeChunks, includeProperties);
      nodes.push(node);
    }
    node[prop] = nodes;
  }

  newNode(modelID, id) {
    const typeName = this.getNodeType(modelID, id);
    return {
      expressID: id,
      type: typeName,
      children: []
    };
  }

  async getSpatialTreeChunks(modelID) {
    const treeChunks = {};
    await this.getChunks(modelID, treeChunks, PropsNames.aggregates);
    await this.getChunks(modelID, treeChunks, PropsNames.spatial);
    return treeChunks;
  }

  saveChunk(chunks, propNames, rel) {
    const relating = rel[propNames.relating].value;
    const related = rel[propNames.related].map((r) => r.value);
    if (chunks[relating] == undefined) {
      chunks[relating] = related;
    } else {
      chunks[relating] = chunks[relating].concat(related);
    }
  }

  getRelated(rel, propNames, IDs) {
    const element = rel[propNames.relating];
    if (!element) {
      return console.warn(`The object with ID ${rel.expressID} has a broken reference.`);
    }
    if (!Array.isArray(element))
      IDs.push(element.value);
    else
      element.forEach((ele) => IDs.push(ele.value));
  }

  static isRelated(id, rel, propNames) {
    const relatedItems = rel[propNames.related];
    if (Array.isArray(relatedItems)) {
      const values = relatedItems.map((item) => item.value);
      return values.includes(id);
    }
    return relatedItems.value === id;
  }

  static newIfcProject(id) {
    return {
      expressID: id,
      type: 'IFCPROJECT',
      children: []
    };
  }

  async getProperty(modelID, elementID, recursive = false, propName) {}

  async getChunks(modelID, chunks, propNames) {}

  async getItemProperties(modelID, expressID, recursive = false) {}

  getNodeType(modelID, id) {}

}

class WebIfcPropertyManager extends BasePropertyManager {

  async getItemProperties(modelID, id, recursive = false) {
    return this.state.api.GetLine(modelID, id, recursive);
  }

  async getHeaderLine(modelID, headerType) {
    return this.state.api.GetHeaderLine(modelID, headerType);
  }

  async getSpatialStructure(modelID, includeProperties) {
    const chunks = await this.getSpatialTreeChunks(modelID);
    const allLines = await this.state.api.GetLineIDsWithType(modelID, IFCPROJECT);
    const projectID = allLines.get(0);
    const project = WebIfcPropertyManager.newIfcProject(projectID);
    await this.getSpatialNode(modelID, project, chunks, includeProperties);
    return project;
  }

  async getAllItemsOfType(modelID, type, verbose) {
    let items = [];
    const lines = await this.state.api.GetLineIDsWithType(modelID, type);
    for (let i = 0; i < lines.size(); i++)
      items.push(lines.get(i));
    if (!verbose)
      return items;
    const result = [];
    for (let i = 0; i < items.length; i++) {
      result.push(await this.state.api.GetLine(modelID, items[i]));
    }
    return result;
  }

  async getProperty(modelID, elementID, recursive = false, propName) {
    const propSetIds = await this.getAllRelatedItemsOfType(modelID, elementID, propName);
    const result = [];
    for (let i = 0; i < propSetIds.length; i++) {
      result.push(await this.state.api.GetLine(modelID, propSetIds[i], recursive));
    }
    return result;
  }

  getNodeType(modelID, id) {
    const typeID = this.state.models[modelID].types[id];
    return this.state.api.GetNameFromTypeCode(typeID);
  }

  async getChunks(modelID, chunks, propNames) {
    const relation = await this.state.api.GetLineIDsWithType(modelID, propNames.name);
    for (let i = 0; i < relation.size(); i++) {
      const rel = await this.state.api.GetLine(modelID, relation.get(i), false);
      this.saveChunk(chunks, propNames, rel);
    }
  }

  async getAllRelatedItemsOfType(modelID, id, propNames) {
    const lines = await this.state.api.GetLineIDsWithType(modelID, propNames.name);
    const IDs = [];
    for (let i = 0; i < lines.size(); i++) {
      const rel = await this.state.api.GetLine(modelID, lines.get(i));
      const isRelated = BasePropertyManager.isRelated(id, rel, propNames);
      if (isRelated)
        this.getRelated(rel, propNames, IDs);
    }
    return IDs;
  }

}

class JSONPropertyManager extends BasePropertyManager {

  async getItemProperties(modelID, id, recursive = false) {
    return {
      ...this.state.models[modelID].jsonData[id]
    };
  }

  async getHeaderLine(modelID) {
    return {};
  }

  async getSpatialStructure(modelID, includeProperties) {
    const chunks = await this.getSpatialTreeChunks(modelID);
    const projectsIDs = await this.getAllItemsOfType(modelID, IFCPROJECT, false);
    const projectID = projectsIDs[0];
    const project = JSONPropertyManager.newIfcProject(projectID);
    await this.getSpatialNode(modelID, project, chunks, includeProperties);
    return {
      ...project
    };
  }

  async getAllItemsOfType(modelID, type, verbose) {
    const data = this.state.models[modelID].jsonData;
    const typeName = await this.state.api.GetNameFromTypeCode(type);
    if (!typeName) {
      throw new Error(`Type not found: ${type}`);
    }
    return this.filterItemsByType(data, typeName, verbose);
  }

  async getProperty(modelID, elementID, recursive = false, propName) {
    const resultIDs = await this.getAllRelatedItemsOfType(modelID, elementID, propName);
    const result = this.getItemsByID(modelID, resultIDs);
    if (recursive) {
      result.forEach(result => this.getReferencesRecursively(modelID, result));
    }
    return result;
  }

  getNodeType(modelID, id) {
    return this.state.models[modelID].jsonData[id].type;
  }

  async getChunks(modelID, chunks, propNames) {
    const relation = await this.getAllItemsOfType(modelID, propNames.name, true);
    relation.forEach(rel => {
      this.saveChunk(chunks, propNames, rel);
    });
  }

  filterItemsByType(data, typeName, verbose) {
    const result = [];
    Object.keys(data).forEach(key => {
      const numKey = parseInt(key);
      if (data[numKey].type.toUpperCase() === typeName) {
        result.push(verbose ? {
          ...data[numKey]
        } : numKey);
      }
    });
    return result;
  }

  async getAllRelatedItemsOfType(modelID, id, propNames) {
    const lines = await this.getAllItemsOfType(modelID, propNames.name, true);
    const IDs = [];
    lines.forEach(line => {
      const isRelated = JSONPropertyManager.isRelated(id, line, propNames);
      if (isRelated)
        this.getRelated(line, propNames, IDs);
    });
    return IDs;
  }

  getItemsByID(modelID, ids) {
    const data = this.state.models[modelID].jsonData;
    const result = [];
    ids.forEach(id => result.push({
      ...data[id]
    }));
    return result;
  }

  getReferencesRecursively(modelID, jsonObject) {
    if (jsonObject == undefined)
      return;
    const keys = Object.keys(jsonObject);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      this.getJSONItem(modelID, jsonObject, key);
    }
  }

  getJSONItem(modelID, jsonObject, key) {
    if (Array.isArray(jsonObject[key])) {
      return this.getMultipleJSONItems(modelID, jsonObject, key);
    }
    if (jsonObject[key] && jsonObject[key].type === 5) {
      jsonObject[key] = this.getItemsByID(modelID, [jsonObject[key].value])[0];
      this.getReferencesRecursively(modelID, jsonObject[key]);
    }
  }

  getMultipleJSONItems(modelID, jsonObject, key) {
    jsonObject[key] = jsonObject[key].map((item) => {
      if (item.type === 5) {
        item = this.getItemsByID(modelID, [item.value])[0];
        this.getReferencesRecursively(modelID, item);
      }
      return item;
    });
  }

}

const geometryTypes = new Set([
  1123145078, 574549367, 1675464909, 2059837836, 3798115385, 32440307, 3125803723, 3207858831,
  2740243338, 2624227202, 4240577450, 3615266464, 3724593414, 220341763, 477187591, 1878645084,
  1300840506, 3303107099, 1607154358, 1878645084, 846575682, 1351298697, 2417041796, 3049322572,
  3331915920, 1416205885, 776857604, 3285139300, 3958052878, 2827736869, 2732653382, 673634403,
  3448662350, 4142052618, 2924175390, 803316827, 2556980723, 1809719519, 2205249479, 807026263,
  3737207727, 1660063152, 2347385850, 3940055652, 2705031697, 3732776249, 2485617015, 2611217952,
  1704287377, 2937912522, 2770003689, 1281925730, 1484403080, 3448662350, 4142052618, 3800577675,
  4006246654, 3590301190, 1383045692, 2775532180, 2047409740, 370225590, 3593883385, 2665983363,
  4124623270, 812098782, 3649129432, 987898635, 1105321065, 3510044353, 1635779807, 2603310189,
  3406155212, 1310608509, 4261334040, 2736907675, 3649129432, 1136057603, 1260505505, 4182860854,
  2713105998, 2898889636, 59481748, 3749851601, 3486308946, 3150382593, 1062206242, 3264961684,
  15328376, 1485152156, 370225590, 1981873012, 2859738748, 45288368, 2614616156, 2732653382,
  775493141, 2147822146, 2601014836, 2629017746, 1186437898, 2367409068, 1213902940, 3632507154,
  3900360178, 476780140, 1472233963, 2804161546, 3008276851, 738692330, 374418227, 315944413,
  3905492369, 3570813810, 2571569899, 178912537, 2294589976, 1437953363, 2133299955, 572779678,
  3092502836, 388784114, 2624227202, 1425443689, 3057273783, 2347385850, 1682466193, 2519244187,
  2839578677, 3958567839, 2513912981, 2830218821, 427810014
]);

class PropertySerializer {

  constructor(webIfc) {
    this.webIfc = webIfc;
  }

  dispose() {
    this.webIfc = null;
  }

  async serializeAllProperties(modelID, maxSize, event) {
    const blobs = [];
    await this.getPropertiesAsBlobs(modelID, blobs, maxSize, event);
    return blobs;
  }

  async getPropertiesAsBlobs(modelID, blobs, maxSize, event) {
    const geometriesIDs = await this.getAllGeometriesIDs(modelID);
    let properties = await this.initializePropertiesObject(modelID);
    const allLinesIDs = await this.webIfc.GetAllLines(modelID);
    const linesCount = allLinesIDs.size();
    let lastEvent = 0.1;
    let counter = 0;
    for (let i = 0; i < linesCount; i++) {
      const id = allLinesIDs.get(i);
      if (!geometriesIDs.has(id)) {
        await this.getItemProperty(modelID, id, properties);
        counter++;
      }
      if (maxSize && counter > maxSize) {
        blobs.push(new Blob([JSON.stringify(properties)], {
          type: 'application/json'
        }));
        properties = {};
        counter = 0;
      }
      if (event && i / linesCount > lastEvent) {
        event(i, linesCount);
        lastEvent += 0.1;
      }
    }
    blobs.push(new Blob([JSON.stringify(properties)], {
      type: 'application/json'
    }));
  }

  async getItemProperty(modelID, id, properties) {
    try {
      const props = await this.webIfc.GetLine(modelID, id);
      if (props.type) {
        props.type = this.webIfc.GetNameFromTypeCode(props.type);
      }
      this.formatItemProperties(props);
      properties[id] = props;
    } catch (e) {
      console.log(`There was a problem getting the properties of the item with ID ${id}`);
    }
  }

  formatItemProperties(props) {
    Object.keys(props).forEach((key) => {
      const value = props[key];
      if (value && value.value !== undefined)
        props[key] = value.value;
      else if (Array.isArray(value))
        props[key] = value.map((item) => {
          if (item && item.value)
            return item.value;
          return item;
        });
    });
  }

  async initializePropertiesObject(modelID) {
    return {
      coordinationMatrix: await this.webIfc.GetCoordinationMatrix(modelID),
      globalHeight: await this.getBuildingHeight(modelID)
    };
  }

  async getBuildingHeight(modelID) {
    const building = await this.getBuilding(modelID);
    let placement;
    const siteReference = building.ObjectPlacement.PlacementRelTo;
    if (siteReference)
      placement = siteReference.RelativePlacement.Location;
    else
      placement = building.ObjectPlacement.RelativePlacement.Location;
    const transform = placement.Coordinates.map((coord) => coord.value);
    return transform[2];
  }

  async getBuilding(modelID) {
    const allBuildingsIDs = await this.webIfc.GetLineIDsWithType(modelID, IFCBUILDING);
    const buildingID = allBuildingsIDs.get(0);
    return this.webIfc.GetLine(modelID, buildingID, true);
  }

  async getAllGeometriesIDs(modelID) {
    const geometriesIDs = new Set();
    const geomTypesArray = Array.from(geometryTypes);
    for (let i = 0; i < geomTypesArray.length; i++) {
      const category = geomTypesArray[i];
      const ids = await this.webIfc.GetLineIDsWithType(modelID, category);
      const idsSize = ids.size();
      for (let j = 0; j < idsSize; j++) {
        geometriesIDs.add(ids.get(j));
      }
    }
    return geometriesIDs;
  }

}

class PropertyManager {

  constructor(state) {
    this.state = state;
    this.webIfcProps = new WebIfcPropertyManager(state);
    this.jsonProps = new JSONPropertyManager(state);
    this.currentProps = this.webIfcProps;
    this.serializer = new PropertySerializer(this.state.api);
  }

  getExpressId(geometry, faceIndex) {
    if (!geometry.index)
      throw new Error('Geometry does not have index information.');
    const geoIndex = geometry.index.array;
    const bufferAttr = geometry.attributes[IdAttrName];
    return bufferAttr.getX(geoIndex[3 * faceIndex]);
  }

  async getHeaderLine(modelID, headerType) {
    this.updateCurrentProps();
    return this.currentProps.getHeaderLine(modelID, headerType);
  }

  async getItemProperties(modelID, elementID, recursive = false) {
    this.updateCurrentProps();
    return this.currentProps.getItemProperties(modelID, elementID, recursive);
  }

  async getAllItemsOfType(modelID, type, verbose) {
    this.updateCurrentProps();
    return this.currentProps.getAllItemsOfType(modelID, type, verbose);
  }

  async getPropertySets(modelID, elementID, recursive = false) {
    this.updateCurrentProps();
    return this.currentProps.getPropertySets(modelID, elementID, recursive);
  }

  async getTypeProperties(modelID, elementID, recursive = false) {
    this.updateCurrentProps();
    return this.currentProps.getTypeProperties(modelID, elementID, recursive);
  }

  async getMaterialsProperties(modelID, elementID, recursive = false) {
    this.updateCurrentProps();
    return this.currentProps.getMaterialsProperties(modelID, elementID, recursive);
  }

  async getSpatialStructure(modelID, includeProperties) {
    this.updateCurrentProps();
    if (!this.state.useJSON && includeProperties) {
      console.warn('Including properties in getSpatialStructure with the JSON workflow disabled can lead to poor performance.');
    }
    return await this.currentProps.getSpatialStructure(modelID, includeProperties);
  }

  updateCurrentProps() {
    this.currentProps = this.state.useJSON ? this.jsonProps : this.webIfcProps;
  }

}

class TypeManager {

  constructor(state) {
    this.state = state;
    this.state = state;
  }

  async getAllTypes(worker) {
    for (let modelID in this.state.models) {
      if (this.state.models.hasOwnProperty(modelID)) {
        const types = this.state.models[modelID].types;
        if (Object.keys(types).length == 0) {
          await this.getAllTypesOfModel(parseInt(modelID), worker);
        }
      }
    }
  }

  async getAllTypesOfModel(modelID, worker) {
    const result = {};
    const elements = await this.state.api.GetIfcEntityList(modelID);
    for (let i = 0; i < elements.length; i++) {
      const element = elements[i];
      const lines = await this.state.api.GetLineIDsWithType(modelID, element);
      const size = lines.size();
      for (let i = 0; i < size; i++)
        result[lines.get(i)] = element;
    }
    if (this.state.worker.active && worker) {
      await worker.workerState.updateModelStateTypes(modelID, result);
    }
    this.state.models[modelID].types = result;
  }

}

class BvhManager {

  initializeMeshBVH(computeBoundsTree, disposeBoundsTree, acceleratedRaycast) {
    this.computeBoundsTree = computeBoundsTree;
    this.disposeBoundsTree = disposeBoundsTree;
    this.acceleratedRaycast = acceleratedRaycast;
    this.setupThreeMeshBVH();
  }

  applyThreeMeshBVH(geometry) {
    if (this.computeBoundsTree)
      geometry.computeBoundsTree();
  }

  setupThreeMeshBVH() {
    if (!this.computeBoundsTree || !this.disposeBoundsTree || !this.acceleratedRaycast)
      return;
    BufferGeometry.prototype.computeBoundsTree = this.computeBoundsTree;
    BufferGeometry.prototype.disposeBoundsTree = this.disposeBoundsTree;
    Mesh.prototype.raycast = this.acceleratedRaycast;
  }

}

var WorkerActions;
(function(WorkerActions) {
  WorkerActions["updateStateUseJson"] = "updateStateUseJson";
  WorkerActions["updateStateWebIfcSettings"] = "updateStateWebIfcSettings";
  WorkerActions["updateModelStateTypes"] = "updateModelStateTypes";
  WorkerActions["updateModelStateJsonData"] = "updateModelStateJsonData";
  WorkerActions["loadJsonDataFromWorker"] = "loadJsonDataFromWorker";
  WorkerActions["dispose"] = "dispose";
  WorkerActions["Close"] = "Close";
  WorkerActions["DisposeWebIfc"] = "DisposeWebIfc";
  WorkerActions["Init"] = "Init";
  WorkerActions["OpenModel"] = "OpenModel";
  WorkerActions["CreateModel"] = "CreateModel";
  WorkerActions["ExportFileAsIFC"] = "ExportFileAsIFC";
  WorkerActions["GetGeometry"] = "GetGeometry";
  WorkerActions["GetLine"] = "GetLine";
  WorkerActions["GetAndClearErrors"] = "GetAndClearErrors";
  WorkerActions["WriteLine"] = "WriteLine";
  WorkerActions["FlattenLine"] = "FlattenLine";
  WorkerActions["GetRawLineData"] = "GetRawLineData";
  WorkerActions["WriteRawLineData"] = "WriteRawLineData";
  WorkerActions["GetLineIDsWithType"] = "GetLineIDsWithType";
  WorkerActions["GetAllLines"] = "GetAllLines";
  WorkerActions["SetGeometryTransformation"] = "SetGeometryTransformation";
  WorkerActions["GetCoordinationMatrix"] = "GetCoordinationMatrix";
  WorkerActions["GetVertexArray"] = "GetVertexArray";
  WorkerActions["GetIndexArray"] = "GetIndexArray";
  WorkerActions["getSubArray"] = "getSubArray";
  WorkerActions["CloseModel"] = "CloseModel";
  WorkerActions["StreamAllMeshes"] = "StreamAllMeshes";
  WorkerActions["StreamAllMeshesWithTypes"] = "StreamAllMeshesWithTypes";
  WorkerActions["IsModelOpen"] = "IsModelOpen";
  WorkerActions["LoadAllGeometry"] = "LoadAllGeometry";
  WorkerActions["GetFlatMesh"] = "GetFlatMesh";
  WorkerActions["SetWasmPath"] = "SetWasmPath";
  WorkerActions["GetNameFromTypeCode"] = "GetNameFromTypeCode";
  WorkerActions["GetIfcEntityList"] = "GetIfcEntityList";
  WorkerActions["GetTypeCodeFromName"] = "GetTypeCodeFromName";
  WorkerActions["parse"] = "parse";
  WorkerActions["setupOptionalCategories"] = "setupOptionalCategories";
  WorkerActions["getExpressId"] = "getExpressId";
  WorkerActions["initializeProperties"] = "initializeProperties";
  WorkerActions["getAllItemsOfType"] = "getAllItemsOfType";
  WorkerActions["getItemProperties"] = "getItemProperties";
  WorkerActions["getMaterialsProperties"] = "getMaterialsProperties";
  WorkerActions["getPropertySets"] = "getPropertySets";
  WorkerActions["getSpatialStructure"] = "getSpatialStructure";
  WorkerActions["getTypeProperties"] = "getTypeProperties";
  WorkerActions["getHeaderLine"] = "getHeaderLine";
})(WorkerActions || (WorkerActions = {}));
var WorkerAPIs;
(function(WorkerAPIs) {
  WorkerAPIs["workerState"] = "workerState";
  WorkerAPIs["webIfc"] = "webIfc";
  WorkerAPIs["properties"] = "properties";
  WorkerAPIs["parser"] = "parser";
})(WorkerAPIs || (WorkerAPIs = {}));

class Vector {

  constructor(vector) {
    this._data = {};
    this._size = vector.size;
    const keys = Object.keys(vector).filter((key) => key.indexOf('size') === -1).map(key => parseInt(key));
    keys.forEach((key) => this._data[key] = vector[key]);
  }

  size() {
    return this._size;
  }

  get(index) {
    return this._data[index];
  }

}

class IfcGeometry {

  constructor(vector) {
    this._GetVertexData = vector.GetVertexData;
    this._GetVertexDataSize = vector.GetVertexDataSize;
    this._GetIndexData = vector.GetIndexData;
    this._GetIndexDataSize = vector.GetIndexDataSize;
  }

  GetVertexData() {
    return this._GetVertexData;
  }

  GetVertexDataSize() {
    return this._GetVertexDataSize;
  }

  GetIndexData() {
    return this._GetIndexData;
  }

  GetIndexDataSize() {
    return this._GetIndexDataSize;
  }

}

class FlatMesh {

  constructor(serializer, flatMesh) {
    this.expressID = flatMesh.expressID;
    this.geometries = serializer.reconstructVector(flatMesh.geometries);
  }

}

class FlatMeshVector {

  constructor(serializer, vector) {
    this._data = {};
    this._size = vector.size;
    const keys = Object.keys(vector).filter((key) => key.indexOf('size') === -1).map(key => parseInt(key));
    keys.forEach(key => this._data[key] = serializer.reconstructFlatMesh(vector[key]));
  }

  size() {
    return this._size;
  }

  get(index) {
    return this._data[index];
  }

}

class SerializedMaterial {

  constructor(material) {
    this.color = [material.color.r, material.color.g, material.color.b];
    this.opacity = material.opacity;
    this.transparent = material.transparent;
  }

}

class MaterialReconstructor {

  static new(material) {
    return new MeshLambertMaterial({
      color: new Color(material.color[0], material.color[1], material.color[2]),
      opacity: material.opacity,
      transparent: material.transparent,
      side: DoubleSide
    });
  }

}

class SerializedGeometry {

  constructor(geometry) {
    var _a,
      _b,
      _c,
      _d;
    this.position = ((_a = geometry.attributes.position) === null || _a === void 0 ? void 0 : _a.array) || [];
    this.normal = ((_b = geometry.attributes.normal) === null || _b === void 0 ? void 0 : _b.array) || [];
    this.expressID = ((_c = geometry.attributes.expressID) === null || _c === void 0 ? void 0 : _c.array) || [];
    this.index = ((_d = geometry.index) === null || _d === void 0 ? void 0 : _d.array) || [];
    this.groups = geometry.groups;
  }

}

class GeometryReconstructor {

  static new(serialized) {
    const geom = new BufferGeometry();
    GeometryReconstructor.set(geom, 'expressID', new Uint32Array(serialized.expressID), 1);
    GeometryReconstructor.set(geom, 'position', new Float32Array(serialized.position), 3);
    GeometryReconstructor.set(geom, 'normal', new Float32Array(serialized.normal), 3);
    geom.setIndex(Array. from (serialized.index));
    geom.groups = serialized.groups;
    return geom;
  }

  static set(geom, name, data, size) {
    if (data.length > 0) {
      geom.setAttribute(name, new BufferAttribute(data, size));
    }
  }

}

class SerializedMesh {

  constructor(model) {
    this.materials = [];
    this.modelID = model.modelID;
    this.geometry = new SerializedGeometry(model.geometry);
    if (Array.isArray(model.material)) {
      model.material.forEach(mat => {
        this.materials.push(new SerializedMaterial(mat));
      });
    } else {
      this.materials.push(new SerializedMaterial(model.material));
    }
  }

}

class MeshReconstructor {

  static new(serialized) {
    const model = new IFCModel();
    model.modelID = serialized.modelID;
    model.geometry = GeometryReconstructor.new(serialized.geometry);
    MeshReconstructor.getMaterials(serialized, model);
    return model;
  }

  static getMaterials(serialized, model) {
    model.material = [];
    const mats = model.material;
    serialized.materials.forEach(mat => {
      mats.push(MaterialReconstructor.new(mat));
    });
  }

}

class Serializer {

  serializeVector(vector) {
    const size = vector.size();
    const serialized = {
      size
    };
    for (let i = 0; i < size; i++) {
      serialized[i] = vector.get(i);
    }
    return serialized;
  }

  reconstructVector(vector) {
    return new Vector(vector);
  }

  serializeIfcGeometry(geometry) {
    const GetVertexData = geometry.GetVertexData();
    const GetVertexDataSize = geometry.GetVertexDataSize();
    const GetIndexData = geometry.GetIndexData();
    const GetIndexDataSize = geometry.GetIndexDataSize();
    return {
      GetVertexData,
      GetVertexDataSize,
      GetIndexData,
      GetIndexDataSize
    };
  }

  reconstructIfcGeometry(geometry) {
    return new IfcGeometry(geometry);
  }

  serializeFlatMesh(flatMesh) {
    return {
      expressID: flatMesh.expressID,
      geometries: this.serializeVector(flatMesh.geometries)
    };
  }

  reconstructFlatMesh(flatMesh) {
    return new FlatMesh(this, flatMesh);
  }

  serializeFlatMeshVector(vector) {
    const size = vector.size();
    const serialized = {
      size
    };
    for (let i = 0; i < size; i++) {
      const flatMesh = vector.get(i);
      serialized[i] = this.serializeFlatMesh(flatMesh);
    }
    return serialized;
  }

  reconstructFlatMeshVector(vector) {
    return new FlatMeshVector(this, vector);
  }

  serializeIfcModel(model) {
    return new SerializedMesh(model);
  }

  reconstructIfcModel(model) {
    return MeshReconstructor.new(model);
  }

}

class PropertyHandler {

  constructor(handler) {
    this.handler = handler;
    this.API = WorkerAPIs.properties;
  }

  getExpressId(geometry, faceIndex) {
    if (!geometry.index)
      throw new Error('Geometry does not have index information.');
    const geoIndex = geometry.index.array;
    const bufferAttr = geometry.attributes[IdAttrName];
    return bufferAttr.getX(geoIndex[3 * faceIndex]);
  }

  getHeaderLine(modelID, headerType) {
    return this.handler.request(this.API, WorkerActions.getHeaderLine, {
      modelID,
      headerType
    });
  }

  getAllItemsOfType(modelID, type, verbose) {
    return this.handler.request(this.API, WorkerActions.getAllItemsOfType, {
      modelID,
      type,
      verbose
    });
  }

  getItemProperties(modelID, elementID, recursive) {
    return this.handler.request(this.API, WorkerActions.getItemProperties, {
      modelID,
      elementID,
      recursive
    });
  }

  getMaterialsProperties(modelID, elementID, recursive) {
    return this.handler.request(this.API, WorkerActions.getMaterialsProperties, {
      modelID,
      elementID,
      recursive
    });
  }

  getPropertySets(modelID, elementID, recursive) {
    return this.handler.request(this.API, WorkerActions.getPropertySets, {
      modelID,
      elementID,
      recursive
    });
  }

  getTypeProperties(modelID, elementID, recursive) {
    return this.handler.request(this.API, WorkerActions.getTypeProperties, {
      modelID,
      elementID,
      recursive
    });
  }

  getSpatialStructure(modelID, includeProperties) {
    return this.handler.request(this.API, WorkerActions.getSpatialStructure, {
      modelID,
      includeProperties
    });
  }

}

class WebIfcHandler {

  constructor(handler, serializer) {
    this.handler = handler;
    this.serializer = serializer;
    this.API = WorkerAPIs.webIfc;
  }

  async Init() {
    this.wasmModule = true;
    return this.handler.request(this.API, WorkerActions.Init);
  }

  async OpenModel(data, settings) {
    return this.handler.request(this.API, WorkerActions.OpenModel, {
      data,
      settings
    });
  }

  async CreateModel(model, settings) {
    return this.handler.request(this.API, WorkerActions.CreateModel, {
      model,
      settings
    });
  }

  async ExportFileAsIFC(modelID) {
    return this.handler.request(this.API, WorkerActions.ExportFileAsIFC, {
      modelID
    });
  }

  async GetHeaderLine(modelID, headerType) {
    return this.handler.request(this.API, WorkerActions.getHeaderLine, {
      modelID,
      headerType
    });
  }

  async GetGeometry(modelID, geometryExpressID) {
    this.handler.serializeHandlers[this.handler.requestID] = (geom) => {
      return this.serializer.reconstructIfcGeometry(geom);
    };
    return this.handler.request(this.API, WorkerActions.GetGeometry, {
      modelID,
      geometryExpressID
    });
  }

  async GetLine(modelID, expressID, flatten) {
    return this.handler.request(this.API, WorkerActions.GetLine, {
      modelID,
      expressID,
      flatten
    });
  }

  async GetAndClearErrors(modelID) {
    this.handler.serializeHandlers[this.handler.requestID] = (vector) => {
      return this.serializer.reconstructVector(vector);
    };
    return this.handler.request(this.API, WorkerActions.GetAndClearErrors, {
      modelID
    });
  }

  async GetNameFromTypeCode(type) {
    return this.handler.request(this.API, WorkerActions.GetNameFromTypeCode, {
      type
    });
  }

  async GetIfcEntityList(modelID) {
    return this.handler.request(this.API, WorkerActions.GetIfcEntityList, {
      modelID
    });
  }

  async GetTypeCodeFromName(modelID, typeName) {
    return this.handler.request(this.API, WorkerActions.GetTypeCodeFromName, {
      modelID,
      typeName
    });
  }

  async WriteLine(modelID, lineObject) {
    return this.handler.request(this.API, WorkerActions.WriteLine, {
      modelID,
      lineObject
    });
  }

  async FlattenLine(modelID, line) {
    return this.handler.request(this.API, WorkerActions.FlattenLine, {
      modelID,
      line
    });
  }

  async GetRawLineData(modelID, expressID) {
    return this.handler.request(this.API, WorkerActions.GetRawLineData, {
      modelID,
      expressID
    });
  }

  async WriteRawLineData(modelID, data) {
    return this.handler.request(this.API, WorkerActions.WriteRawLineData, {
      modelID,
      data
    });
  }

  async GetLineIDsWithType(modelID, type) {
    this.handler.serializeHandlers[this.handler.requestID] = (vector) => {
      return this.serializer.reconstructVector(vector);
    };
    return this.handler.request(this.API, WorkerActions.GetLineIDsWithType, {
      modelID,
      type
    });
  }

  async GetAllLines(modelID) {
    this.handler.serializeHandlers[this.handler.requestID] = (vector) => {
      return this.serializer.reconstructVector(vector);
    };
    return this.handler.request(this.API, WorkerActions.GetAllLines, {
      modelID
    });
  }

  async SetGeometryTransformation(modelID, transformationMatrix) {
    return this.handler.request(this.API, WorkerActions.SetGeometryTransformation, {
      modelID,
      transformationMatrix
    });
  }

  async GetCoordinationMatrix(modelID) {
    return this.handler.request(this.API, WorkerActions.GetCoordinationMatrix, {
      modelID
    });
  }

  async GetVertexArray(ptr, size) {
    return this.handler.request(this.API, WorkerActions.GetVertexArray, {
      ptr,
      size
    });
  }

  async GetIndexArray(ptr, size) {
    return this.handler.request(this.API, WorkerActions.GetIndexArray, {
      ptr,
      size
    });
  }

  async getSubArray(heap, startPtr, sizeBytes) {
    return this.handler.request(this.API, WorkerActions.getSubArray, {
      heap,
      startPtr,
      sizeBytes
    });
  }

  async CloseModel(modelID) {
    return this.handler.request(this.API, WorkerActions.CloseModel, {
      modelID
    });
  }

  async StreamAllMeshes(modelID, meshCallback) {
    this.handler.callbackHandlers[this.handler.requestID] = {
      action: meshCallback,
      serializer: this.serializer.reconstructFlatMesh
    };
    return this.handler.request(this.API, WorkerActions.StreamAllMeshes, {
      modelID
    });
  }

  async StreamAllMeshesWithTypes(modelID, types, meshCallback) {
    this.handler.callbackHandlers[this.handler.requestID] = {
      action: meshCallback,
      serializer: this.serializer.reconstructFlatMesh
    };
    return this.handler.request(this.API, WorkerActions.StreamAllMeshesWithTypes, {
      modelID,
      types
    });
  }

  async IsModelOpen(modelID) {
    return this.handler.request(this.API, WorkerActions.IsModelOpen, {
      modelID
    });
  }

  async LoadAllGeometry(modelID) {
    this.handler.serializeHandlers[this.handler.requestID] = (vector) => {
      return this.serializer.reconstructFlatMeshVector(vector);
    };
    return this.handler.request(this.API, WorkerActions.LoadAllGeometry, {
      modelID
    });
  }

  async GetFlatMesh(modelID, expressID) {
    this.handler.serializeHandlers[this.handler.requestID] = (flatMesh) => {
      return this.serializer.reconstructFlatMesh(flatMesh);
    };
    return this.handler.request(this.API, WorkerActions.GetFlatMesh, {
      modelID,
      expressID
    });
  }

  async SetWasmPath(path) {
    return this.handler.request(this.API, WorkerActions.SetWasmPath, {
      path
    });
  }

}

class WorkerStateHandler {

  constructor(handler) {
    this.handler = handler;
    this.API = WorkerAPIs.workerState;
    this.state = this.handler.state;
  }

  async updateStateUseJson() {
    const useJson = this.state.useJSON;
    return this.handler.request(this.API, WorkerActions.updateStateUseJson, {
      useJson
    });
  }

  async updateStateWebIfcSettings() {
    const webIfcSettings = this.state.webIfcSettings;
    return this.handler.request(this.API, WorkerActions.updateStateWebIfcSettings, {
      webIfcSettings
    });
  }

  async updateModelStateTypes(modelID, types) {
    return this.handler.request(this.API, WorkerActions.updateModelStateTypes, {
      modelID,
      types
    });
  }

  async updateModelStateJsonData(modelID, jsonData) {
    return this.handler.request(this.API, WorkerActions.updateModelStateJsonData, {
      modelID,
      jsonData
    });
  }

  async loadJsonDataFromWorker(modelID, path) {
    return this.handler.request(this.API, WorkerActions.loadJsonDataFromWorker, {
      modelID,
      path
    });
  }

}

var DBOperation;
(function(DBOperation) {
  DBOperation[DBOperation["transferIfcModel"] = 0] = "transferIfcModel";
  DBOperation[DBOperation["transferIndividualItems"] = 1] = "transferIndividualItems";
})(DBOperation || (DBOperation = {}));

class IndexedDatabase {

  async save(item, id) {
    const open = IndexedDatabase.openOrCreateDB(id);
    this.createSchema(open, id);
    return new Promise((resolve, reject) => {
      open.onsuccess = () => this.saveItem(item, open, id, resolve);
    });
  }

  async load(id) {
    const open = IndexedDatabase.openOrCreateDB(id);
    return new Promise((resolve, reject) => {
      open.onsuccess = () => this.loadItem(open, id, resolve);
    });
  }

  createSchema(open, id) {
    open.onupgradeneeded = function() {
      const db = open.result;
      db.createObjectStore(id.toString(), {
        keyPath: "id"
      });
    };
  }

  saveItem(item, open, id, resolve) {
    const {db, tx, store} = IndexedDatabase.getDBItems(open, id);
    item.id = id;
    store.put(item);
    tx.oncomplete = () => IndexedDatabase.closeDB(db, tx, resolve);
  }

  loadItem(open, id, resolve) {
    const {db, tx, store} = IndexedDatabase.getDBItems(open, id);
    const item = store.get(id);
    const callback = () => {
      delete item.result.id;
      resolve(item.result);
    };
    tx.oncomplete = () => IndexedDatabase.closeDB(db, tx, callback);
  }

  static getDBItems(open, id) {
    const db = open.result;
    const tx = db.transaction(id.toString(), "readwrite");
    const store = tx.objectStore(id.toString());
    return {
      db,
      tx,
      store
    };
  }

  static openOrCreateDB(id) {
    return indexedDB.open(id.toString(), 1);
  }

  static closeDB(db, tx, resolve) {
    db.close();
    resolve("success");
  }

}

class ParserHandler {

  constructor(handler, serializer, BVH, IDB) {
    this.handler = handler;
    this.serializer = serializer;
    this.BVH = BVH;
    this.IDB = IDB;
    this.optionalCategories = {
      [IFCSPACE]: true,
      [IFCOPENINGELEMENT]: false
    };
    this.API = WorkerAPIs.parser;
  }

  async setupOptionalCategories(config) {
    this.optionalCategories = config;
    return this.handler.request(this.API, WorkerActions.setupOptionalCategories, {
      config
    });
  }

  async parse(buffer, coordinationMatrix) {
    this.handler.onprogressHandlers[this.handler.requestID] = (progress) => {
      if (this.handler.state.onProgress)
        this.handler.state.onProgress(progress);
    };
    this.handler.serializeHandlers[this.handler.requestID] = async (result) => {
      this.updateState(result.modelID);
      return this.getModel();
    };
    return this.handler.request(this.API, WorkerActions.parse, {
      buffer,
      coordinationMatrix
    });
  }

  getAndClearErrors(_modelId) {}

  updateState(modelID) {
    this.handler.state.models[modelID] = {
      modelID: modelID,
      mesh: {},
      types: {},
      jsonData: {}
    };
  }

  async getModel() {
    const serializedModel = await this.IDB.load(DBOperation.transferIfcModel);
    const model = this.serializer.reconstructIfcModel(serializedModel);
    this.BVH.applyThreeMeshBVH(model.geometry);
    this.handler.state.models[model.modelID].mesh = model;
    return model;
  }

}

class IFCWorkerHandler {

  constructor(state, BVH) {
    this.state = state;
    this.BVH = BVH;
    this.requestID = 0;
    this.rejectHandlers = {};
    this.resolveHandlers = {};
    this.serializeHandlers = {};
    this.callbackHandlers = {};
    this.onprogressHandlers = {};
    this.serializer = new Serializer();
    this.IDB = new IndexedDatabase();
    this.workerPath = this.state.worker.path;
    this.ifcWorker = new Worker(this.workerPath);
    this.ifcWorker.onmessage = (data) => this.handleResponse(data);
    this.properties = new PropertyHandler(this);
    this.parser = new ParserHandler(this, this.serializer, this.BVH, this.IDB);
    this.webIfc = new WebIfcHandler(this, this.serializer);
    this.workerState = new WorkerStateHandler(this);
  }

  request(worker, action, args) {
    const data = {
      worker,
      action,
      args,
      id: this.requestID,
      result: undefined,
      onProgress: false
    };
    return new Promise((resolve, reject) => {
      this.resolveHandlers[this.requestID] = resolve;
      this.rejectHandlers[this.requestID] = reject;
      this.requestID++;
      this.ifcWorker.postMessage(data);
    });
  }

  async terminate() {
    await this.request(WorkerAPIs.workerState, WorkerActions.dispose);
    await this.request(WorkerAPIs.webIfc, WorkerActions.DisposeWebIfc);
    this.ifcWorker.terminate();
  }

  async Close() {
    await this.request(WorkerAPIs.webIfc, WorkerActions.Close);
  }

  handleResponse(event) {
    const data = event.data;
    if (data.onProgress) {
      this.resolveOnProgress(data);
      return;
    }
    this.callHandlers(data);
    delete this.resolveHandlers[data.id];
    delete this.rejectHandlers[data.id];
    delete this.onprogressHandlers[data.id];
  }

  callHandlers(data) {
    try {
      this.resolveSerializations(data);
      this.resolveCallbacks(data);
      this.resolveHandlers[data.id](data.result);
    } catch (error) {
      this.rejectHandlers[data.id](error);
    }
  }

  resolveOnProgress(data) {
    if (this.onprogressHandlers[data.id]) {
      data.result = this.onprogressHandlers[data.id](data.result);
    }
  }

  resolveSerializations(data) {
    if (this.serializeHandlers[data.id]) {
      data.result = this.serializeHandlers[data.id](data.result);
      delete this.serializeHandlers[data.id];
    }
  }

  resolveCallbacks(data) {
    if (this.callbackHandlers[data.id]) {
      let callbackParameter = data.result;
      if (this.callbackHandlers[data.id].serializer) {
        callbackParameter = this.callbackHandlers[data.id].serializer(data.result);
      }
      this.callbackHandlers[data.id].action(callbackParameter);
    }
  }

}

class MemoryCleaner {

  constructor(state) {
    this.state = state;
  }

  async dispose() {
    Object.keys(this.state.models).forEach(modelID => {
      const model = this.state.models[parseInt(modelID, 10)];
      model.mesh.removeFromParent();
      const geom = model.mesh.geometry;
      if (geom.disposeBoundsTree)
        geom.disposeBoundsTree();
      geom.dispose();
      if (!Array.isArray(model.mesh.material))
        model.mesh.material.dispose();
      else
        model.mesh.material.forEach(mat => mat.dispose());
      model.mesh = null;
      model.types = null;
      model.jsonData = null;
    });
    this.state.api = null;
    this.state.models = null;
  }

}

class IFCUtils {

  constructor(state) {
    this.state = state;
    this.map = {};
  }

  isA(entity, entity_class) {
    var test = false;
    if (entity_class) {
      if (this.state.api.GetNameFromTypeCode(entity.type) === entity_class.toUpperCase()) {
        test = true;
      }
      return test;
    } else {
      return this.state.api.GetNameFromTypeCode(entity.type);
    }
  }

  async byId(modelID, id) {
    return this.state.api.GetLine(modelID, id);
  }

  async idsByType(modelID, entity_class) {
    let entities_ids = await this.state.api.GetLineIDsWithType(modelID, Number(this.state.api.GetTypeCodeFromName(modelID, entity_class.toUpperCase())));
    return entities_ids;
  }

  async byType(modelID, entity_class) {
    let entities_ids = await this.idsByType(modelID, entity_class);
    if (entities_ids !== null) {
      let items = [];
      for (let i = 0; i < entities_ids.size(); i++) {
        let entity = await this.byId(modelID, entities_ids.get(i));
        items.push(entity);
      }
      return items;
    }
  }

}

class Data {

  constructor(state) {
    this.state = state;
    this.isLoaded = false;
    this.workPlans = {};
    this.workSchedules = {};
    this.workCalendars = {};
    this.workTimes = {};
    this.recurrencePatterns = {};
    this.timePeriods = {};
    this.tasks = {};
    this.taskTimes = {};
    this.lagTimes = {};
    this.sequences = {};
    this.utils = new IFCUtils(this.state);
  }

  async load(modelID) {
    await this.loadTasks(modelID);
    await this.loadWorkSchedules(modelID);
    await this.loadWorkCalendars(modelID);
    await this.loadWorkTimes(modelID);
    await this.loadTimePeriods(modelID);
    this.isLoaded = true;
  }

  async loadWorkSchedules(modelID) {
    let workSchedules = await this.utils.byType(modelID, "IfcWorkSchedule");
    for (let i = 0; i < workSchedules.length; i++) {
      let workSchedule = workSchedules[i];
      this.workSchedules[workSchedule.expressID] = {
        "Id": workSchedule.expressID,
        "Name": workSchedule.Name.value,
        "Description": ((workSchedule.Description) ? workSchedule.Description.value : ""),
        "Creators": [],
        "CreationDate": ((workSchedule.CreationDate) ? workSchedule.CreationDate.value : ""),
        "StartTime": ((workSchedule.StartTime) ? workSchedule.StartTime.value : ""),
        "FinishTime": ((workSchedule.FinishTime) ? workSchedule.FinishTime.value : ""),
        "TotalFloat": ((workSchedule.TotalFloat) ? workSchedule.TotalFloat.value : ""),
        "RelatedObjects": [],
      };
    }
    this.loadWorkScheduleRelatedObjects(modelID);
  }

  async loadWorkScheduleRelatedObjects(modelID) {
    let relsControls = await this.utils.byType(modelID, "IfcRelAssignsToControl");
    for (let i = 0; i < relsControls.length; i++) {
      let relControls = relsControls[i];
      let relatingControl = await this.utils.byId(modelID, relControls.RelatingControl.value);
      let relatedObjects = relControls.RelatedObjects;
      if (this.utils.isA(relatingControl, "IfcWorkSchedule")) {
        for (var objectIndex = 0; objectIndex < relatedObjects.length; objectIndex++) {
          this.workSchedules[relatingControl.expressID]["RelatedObjects"].push(relatedObjects[objectIndex].value);
        }
      }
    }
  }

  async loadTasks(modelID) {
    let tasks = await this.utils.byType(modelID, "IfcTask");
    for (let i = 0; i < tasks.length; i++) {
      let task = tasks[i];
      this.tasks[task.expressID] = {
        "Id": task.expressID,
        "Name": ((task.Name) ? task.Name.value : ""),
        "PredefinedType": ((task.PredefinedType) ? task.PredefinedType.value : ""),
        "TaskTime": ((task.TaskTime) ? await this.utils.byId(modelID, task.TaskTime.value) : ""),
        "Identification": ((task.Identification) ? task.Identification.value : ""),
        "IsMilestone": ((task.IsMilestone) ? task.IsMilestone.value : ""),
        "IsPredecessorTo": [],
        "IsSucessorFrom": [],
        "Inputs": [],
        "Resources": [],
        "Outputs": [],
        "Controls": [],
        "Nests": [],
        "IsNestedBy": [],
        "OperatesOn": [],
        "HasAssignmentsWorkCalendars": [],
      };
    }
    await this.loadTaskSequence(modelID);
    await this.loadTaskOutputs(modelID);
    await this.loadTaskNesting(modelID);
    await this.loadTaskOperations(modelID);
    await this.loadAssignementsWorkCalendar(modelID);
  }

  async loadTaskSequence(modelID) {
    let relsSequence = await this.utils.idsByType(modelID, "IfcRelSequence");
    for (let i = 0; i < relsSequence.size(); i++) {
      let relSequenceId = relsSequence.get(i);
      if (relSequenceId !== 0) {
        let relSequence = await this.utils.byId(modelID, relSequenceId);
        let related_process = relSequence.RelatedProcess.value;
        let relatingProcess = relSequence.RelatingProcess.value;
        this.tasks[relatingProcess]["IsPredecessorTo"].push(relSequence.expressID);
        this.tasks[related_process]["IsSucessorFrom"].push(relSequence.expressID);
      }
    }
  }

  async loadTaskOutputs(modelID) {
    let rels_assigns_to_product = await this.utils.byType(modelID, "IfcRelAssignsToProduct");
    for (let i = 0; i < rels_assigns_to_product.length; i++) {
      let relAssignsToProduct = rels_assigns_to_product[i];
      let relatedObject = await this.utils.byId(modelID, relAssignsToProduct.RelatedObjects[0].value);
      if (this.utils.isA(relatedObject, "IfcTask")) {
        let relatingProduct = await this.utils.byId(modelID, relAssignsToProduct.RelatingProduct.value);
        this.tasks[relatedObject.expressID]["Outputs"].push(relatingProduct.expressID);
      }
    }
  }

  async loadTaskNesting(modelID) {
    let rels_nests = await this.utils.byType(modelID, "IfcRelNests");
    for (let i = 0; i < rels_nests.length; i++) {
      let relNests = rels_nests[i];
      let relating_object = await this.utils.byId(modelID, relNests.RelatingObject.value);
      if (this.utils.isA(relating_object, "IfcTask")) {
        let relatedObjects = relNests.RelatedObjects;
        for (var object_index = 0; object_index < relatedObjects.length; object_index++) {
          this.tasks[relating_object.expressID]["IsNestedBy"].push(relatedObjects[object_index].value);
          this.tasks[relatedObjects[object_index].value]["Nests"].push(relating_object.expressID);
        }
      }
    }
  }

  async loadTaskOperations(modelID) {
    let relsAssignsToProcess = await this.utils.byType(modelID, "IfcRelAssignsToProcess");
    for (let i = 0; i < relsAssignsToProcess.length; i++) {
      let relAssignToProcess = relsAssignsToProcess[i];
      let relatingProcess = await this.utils.byId(modelID, relAssignToProcess.RelatingProcess.value);
      if (this.utils.isA(relatingProcess, "IfcTask")) {
        let relatedObjects = relAssignToProcess.RelatedObjects;
        for (var object_index = 0; object_index < relatedObjects.length; object_index++) {
          this.tasks[relatingProcess.expressID]["OperatesOn"].push(relatedObjects[object_index].value);
        }
      }
    }
  }

  async loadAssignementsWorkCalendar(modelID) {
    let relsAssignsToControl = await this.utils.byType(modelID, "IfcRelAssignsToControl");
    for (let i = 0; i < relsAssignsToControl.length; i++) {
      let relAssignsToControl = relsAssignsToControl[i];
      let relatingControl = await this.utils.byId(modelID, relAssignsToControl.RelatingControl.value);
      if (this.utils.isA(relatingControl, "IfcWorkCalendar")) {
        let relatedObjects = relAssignsToControl.RelatedObjects;
        for (var object_index = 0; object_index < relatedObjects.length; object_index++) {
          this.tasks[relatedObjects[object_index].value]["HasAssignmentsWorkCalendars"].push(relatingControl.expressID);
        }
      }
    }
  }

  async loadWorkCalendars(modelID) {
    let workCalendars = await this.utils.byType(modelID, "IfcWorkCalendar");
    for (let i = 0; i < workCalendars.length; i++) {
      let workCalendar = workCalendars[i];
      let workCalenderData = {
        "Id": workCalendar.expressID,
        "Name": ((workCalendar.Name) ? workCalendar.Name.value : ""),
        "Description": ((workCalendar.Description) ? workCalendar.Description.value : ""),
        "WorkingTimes": ((workCalendar.WorkingTimes) ? workCalendar.WorkingTimes : []),
        "ExceptionTimes": ((workCalendar.ExceptionTimes) ? workCalendar.ExceptionTimes : []),
      };
      this.workCalendars[workCalendar.expressID] = workCalenderData;
    }
  }

  async loadWorkTimes(modelID) {
    let workTimes = await this.utils.byType(modelID, "IfcWorkTime");
    for (let i = 0; i < workTimes.length; i++) {
      let workTime = workTimes[i];
      let workTimeData = {
        "Name": ((workTime.Name) ? workTime.Name.value : ""),
        "RecurrencePattern": ((workTime.RecurrencePattern) ? await this.utils.byId(modelID, workTime.RecurrencePattern.value) : ""),
        "Start": ((workTime.Start) ? new Date(workTime.Start.value) : ""),
        "Finish": ((workTime.Finish) ? new Date(workTime.Finish.value) : ""),
      };
      this.workTimes[workTime.expressID] = workTimeData;
    }
  }

  async loadTimePeriods(modelID) {
    let timePeriods = await this.utils.byType(modelID, "IfcTimePeriod");
    for (let i = 0; i < timePeriods.length; i++) {
      let timePeriod = timePeriods[i];
      let workTimeData = {
        "StartTime": ((timePeriod.StartTime) ? new Date(timePeriod.StartTime.value) : ""),
        "EndTime": ((timePeriod.EndTime) ? new Date(timePeriod.EndTime.value) : ""),
      };
      this.timePeriods[timePeriod.expressID] = workTimeData;
    }
  }

}

class IFCManager {

  constructor() {
    this.state = {
      models: [],
      api: new WebIFC.IfcAPI(),
      wasmPath: '',
      wasmAbsolute: false,
      useJSON: false,
      worker: {
        active: false,
        path: ''
      }
    };
    this.BVH = new BvhManager();
    this.parser = new IFCParser(this.state, this.BVH);
    this.subsets = new SubsetManager(this.state, this.BVH);
    this.utils = new IFCUtils(this.state);
    this.sequenceData = new Data(this.state);
    this.properties = new PropertyManager(this.state);
    this.types = new TypeManager(this.state);
    this.useFragments = false;
    this.cleaner = new MemoryCleaner(this.state);
  }

  get ifcAPI() {
    return this.state.api;
  }

  async parse(buffer) {
    var _a;
    let model = await this.parser.parse(buffer, (_a = this.state.coordinationMatrix) === null || _a === void 0 ? void 0 : _a.toArray());
    model.setIFCManager(this);
    try {
      await this.types.getAllTypes(this.worker);
    } catch (e) {
      console.log("Could not get all types of model.");
    }
    return model;
  }

  async setWasmPath(path, absolute = false) {
    this.state.api.SetWasmPath(path, absolute);
    this.state.wasmPath = path;
    this.state.wasmAbsolute = absolute;
  }

  setupThreeMeshBVH(computeBoundsTree, disposeBoundsTree, acceleratedRaycast) {
    this.BVH.initializeMeshBVH(computeBoundsTree, disposeBoundsTree, acceleratedRaycast);
  }

  setOnProgress(onProgress) {
    this.state.onProgress = onProgress;
  }

  setupCoordinationMatrix(matrix) {
    this.state.coordinationMatrix = matrix;
  }

  clearCoordinationMatrix() {
    delete this.state.coordinationMatrix;
  }

  async applyWebIfcConfig(settings) {
    this.state.webIfcSettings = settings;
    if (this.state.worker.active && this.worker) {
      await this.worker.workerState.updateStateWebIfcSettings();
    }
  }

  async useWebWorkers(active, path) {
    if (this.state.worker.active === active)
      return;
    this.state.api = null;
    if (active) {
      if (!path)
        throw new Error('You must provide a path to the web worker.');
      this.state.worker.active = active;
      this.state.worker.path = path;
      await this.initializeWorkers();
      const wasm = this.state.wasmPath;
      if (wasm)
        await this.setWasmPath(wasm, this.state.wasmAbsolute);
    } else {
      this.state.api = new WebIFC.IfcAPI();
    }
  }

  async useJSONData(useJSON = true) {
    var _a;
    this.state.useJSON = useJSON;
    if (useJSON) {
      await ((_a = this.worker) === null || _a === void 0 ? void 0 : _a.workerState.updateStateUseJson());
    }
  }

  async addModelJSONData(modelID, data) {
    var _a;
    const model = this.state.models[modelID];
    if (!model)
      throw new Error('The specified model for the JSON data does not exist');
    if (this.state.worker.active) {
      await ((_a = this.worker) === null || _a === void 0 ? void 0 : _a.workerState.updateModelStateJsonData(modelID, data));
    } else {
      model.jsonData = data;
    }
  }

  async loadJsonDataFromWorker(modelID, path) {
    var _a;
    if (this.state.worker.active) {
      await ((_a = this.worker) === null || _a === void 0 ? void 0 : _a.workerState.loadJsonDataFromWorker(modelID, path));
    }
  }

  close(modelID, scene) {
    try {
      this.state.api.CloseModel(modelID);
      const mesh = this.state.models[modelID].mesh;
      const {geometry, material} = mesh;
      if (scene)
        scene.remove(mesh);
      geometry === null || geometry === void 0 ? void 0 : geometry.dispose();
      Array.isArray(material) ? material.forEach(m => m.dispose()) : material === null || material === void 0 ? void 0 : material.dispose();
      delete this.state.models[modelID];
    } catch (e) {
      console.warn(`Close IFCModel ${modelID} failed`);
    }
  }

  getExpressId(geometry, faceIndex) {
    return this.properties.getExpressId(geometry, faceIndex);
  }

  getAllItemsOfType(modelID, type, verbose) {
    return this.properties.getAllItemsOfType(modelID, type, verbose);
  }

  getItemProperties(modelID, id, recursive = false) {
    return this.properties.getItemProperties(modelID, id, recursive);
  }

  getPropertySets(modelID, id, recursive = false) {
    return this.properties.getPropertySets(modelID, id, recursive);
  }

  getTypeProperties(modelID, id, recursive = false) {
    return this.properties.getTypeProperties(modelID, id, recursive);
  }

  getMaterialsProperties(modelID, id, recursive = false) {
    return this.properties.getMaterialsProperties(modelID, id, recursive);
  }

  getIfcType(modelID, id) {
    const typeID = this.state.models[modelID].types[id];
    return this.state.api.GetNameFromTypeCode(typeID);
  }

  getSpatialStructure(modelID, includeProperties) {
    return this.properties.getSpatialStructure(modelID, includeProperties);
  }

  getSubset(modelID, material, customId) {
    return this.subsets.getSubset(modelID, material, customId);
  }

  removeSubset(modelID, material, customID) {
    this.subsets.removeSubset(modelID, material, customID);
  }

  createSubset(config) {
    return this.subsets.createSubset(config);
  }

  removeFromSubset(modelID, ids, customID, material) {
    return this.subsets.removeFromSubset(modelID, ids, customID, material);
  }

  clearSubset(modelID, customID, material) {
    return this.subsets.clearSubset(modelID, customID, material);
  }

  async isA(entity, entity_class) {
    return this.utils.isA(entity, entity_class);
  }

  async getSequenceData(modelID) {
    await this.sequenceData.load(modelID);
    return this.sequenceData;
  }

  async byType(modelID, entityClass) {
    return this.utils.byType(modelID, entityClass);
  }

  async byId(modelID, id) {
    return this.utils.byId(modelID, id);
  }

  async idsByType(modelID, entityClass) {
    return this.utils.idsByType(modelID, entityClass);
  }

  async dispose() {
    IFCModel.dispose();
    await this.cleaner.dispose();
    this.subsets.dispose();
    if (this.worker && this.state.worker.active)
      await this.worker.terminate();
    this.state = null;
  }

  async disposeMemory() {
    var _a;
    if (this.state.worker.active) {
      await ((_a = this.worker) === null || _a === void 0 ? void 0 : _a.Close());
    } else {
      this.state.api.Close();
      this.state.api = null;
      this.state.api = new WebIFC.IfcAPI();
    }
  }

  getAndClearErrors(modelID) {
    return this.parser.getAndClearErrors(modelID);
  }

  async initializeWorkers() {
    this.worker = new IFCWorkerHandler(this.state, this.BVH);
    this.state.api = this.worker.webIfc;
    this.properties = this.worker.properties;
    await this.worker.parser.setupOptionalCategories(this.parser.optionalCategories);
    this.parser = this.worker.parser;
    await this.worker.workerState.updateStateUseJson();
    await this.worker.workerState.updateStateWebIfcSettings();
  }

}

class IFCLoader extends Loader {

  constructor(manager) {
    super(manager);
    this.ifcManager = new IFCManager();
  }

  load(url, onLoad, onProgress, onError) {
    const scope = this;
    const loader = new FileLoader(scope.manager);
    this.onProgress = onProgress;
    loader.setPath(scope.path);
    loader.setResponseType('arraybuffer');
    loader.setRequestHeader(scope.requestHeader);
    loader.setWithCredentials(scope.withCredentials);
    loader.load(url, async function (buffer) {
      try {
        if (typeof buffer == 'string') {
          throw new Error('IFC files must be given as a buffer!');
        }
        onLoad(await scope.parse(buffer));
      } catch (e) {
        if (onError) {
          onError(e);
        } else {
          console.error(e);
        }
        scope.manager.itemError(url);
      }
    }, onProgress, onError);
  }

  parse(buffer) {
    return this.ifcManager.parse(buffer);
  }

}

export { IFCLoader };
//# sourceMappingURL=IFCLoader.js.map
