/**
 * @author AndrĂ© Storhaug <[email protected]>
 */

import autoBind from 'auto-bind';
import { Color, BufferGeometry, MeshPhongMaterial, BoxGeometry, Vector3, Mesh, Geometry, VertexColors } from 'three';
import { LoaderFactory } from "./loaders/LoaderFactory";
import { levelOfDetail } from './mixins/levelOfDetail';

export class VoxelLoader {
  /**
   * Create a VoxelLoader.
   * @classdesc Class for loading voxel data stored in various formats.
   * @param {LoadingManager} manager
   * @mixes levelOfDetail
   */
  constructor(manager) {
    autoBind(this);

    Object.assign(this, levelOfDetail);
    this.manager = manager;
    this.octree = null;
    this.material = null;
    this.voxelSize = null;

    this.setVoxelMaterial();
    this.setVoxelSize();
  }

  /**
   * Set the material used for all voxels.
   * Note that the {@link Material.vertexColors} will be set to {@link VertexColors}.
   * @param {Material} Material The wanted material.
   */
  setVoxelMaterial(material) {
    let defaultMaterial = new MeshPhongMaterial({
      color: 0xffffff
    });

    material = typeof material !== 'undefined' ? material : defaultMaterial;
    material.vertexColors = VertexColors
    this.material = material;
  }

  /**
   * Set the size of the cubes representing voxels generated in {@link VoxelLoader#generateMesh}.
   * @param {float} [voxelSize=1]
   */
  setVoxelSize(voxelSize = 1) {
    this.voxelSize = voxelSize;
  }

  /**
   * Update the internal data structures and settings.
	 * @return {Promise<PointOctree>} Promise with an updated octree.
   */
  update() {
    if (this.octree === null) {
      throw new Error('Octree is not built');
    }
    return this.parseData(this.octree, 'octree');
  }

  /**
	 * Loads and parses a 3D model file from a URL.
	 *
	 * @param {String} url - URL to the VOX file.
	 * @param {Function} [onLoad] - Callback invoked with the Mesh object.
	 * @param {Function} [onProgress] - Callback for download progress.
	 * @param {Function} [onError] - Callback for download errors.
	 */
  loadFile(url, onLoad, onProgress, onError) {
    let scope = this;
    let extension = url.split('.').pop().toLowerCase();
    let loaderFactory = new LoaderFactory(this.manager);

    let loader = loaderFactory.getLoader(extension);
    loader.setLOD(this.LOD.maxPoints, this.LOD.maxDepth);

    loader.load(url, function (octree) {
      scope.octree = octree;
      onLoad(scope.generateMesh(octree));
    }, onProgress, onError);
  }

  /**
   * Parses voxel data.
   * @param {PointOctree} octree Octree with voxel data stored as points in space.
	 * @return {Promise<PointOctree>} Promise with an octree filled with voxel data.
   */
  parseData(data, type) {
    let scope = this;
    let loaderFactory = new LoaderFactory(this.manager);

    let loader = loaderFactory.getLoader(type);
    loader.setLOD(this.LOD.maxPoints, this.LOD.maxDepth);

    return new Promise((resolve) => {
      loader.parse(data).then((octree) => {
        scope.octree = octree;
        resolve(octree);
      });
    });
  }

  /**
   * Generates a polygon mesh with cubes based on voxel data.
   * One cube for each voxel.
   * @param {PointOctree} octree Octree with voxel data stored as points in space.
   * @returns {Mesh} 3D mesh based on voxel data
   */
  generateMesh(octree) {

    let mergedGeometry = new Geometry();
    const material = this.material;

    for (const leaf of octree.leaves()) {
      if (leaf.points !== null) {
        const pos = new Vector3();
        var i;
        let min = { x: leaf.points[0].x, y: leaf.points[0].y, z: leaf.points[0].z };
        let max = { x: leaf.points[0].x, y: leaf.points[0].y, z: leaf.points[0].z };

        for (i = 0; i < leaf.points.length; i++) {
          const point = leaf.points[i];
          pos.add(point);
          min.x = Math.min(min.x, point.x);
          min.y = Math.min(min.y, point.y);
          min.z = Math.min(min.z, point.z);
          max.x = Math.max(max.x, point.x);
          max.y = Math.max(max.y, point.y);
          max.z = Math.max(max.z, point.z);
        }

        let width = Math.round((this.voxelSize + (max.x - min.x)) * 100) / 100;;
        let height = Math.round((this.voxelSize + (max.y - min.y)) * 100) / 100;;
        let depth = Math.round((this.voxelSize + (max.z - min.z)) * 100) / 100;

        let voxelGeometry = new BoxGeometry(width, height, depth);
        pos.divideScalar(i);

        const rgb = leaf.data[0].color;
        if (rgb != null) {
          const color = new Color().setRGB(rgb.r / 255, rgb.g / 255, rgb.b / 255);

          for (var i = 0; i < voxelGeometry.faces.length; i++) {
            let face = voxelGeometry.faces[i];
            face.color.set(color);
          }
        }

        voxelGeometry.translate(pos.x, pos.y, pos.z);
        mergedGeometry.merge(voxelGeometry);
        voxelGeometry.translate(-pos.x, -pos.y, -pos.z);
      }
    }

    let bufGeometry = new BufferGeometry().fromGeometry(mergedGeometry);
    bufGeometry.computeFaceNormals();
    bufGeometry.computeVertexNormals();

    var voxels = new Mesh(bufGeometry, material);

    return voxels;
  }
}