// Copyright 2016 The Draco Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
'use strict';

THREE.DRACOLoader = function(manager) {
    this.manager = (manager !== undefined) ? manager :
        THREE.DefaultLoadingManager;
    this.materials = null;
};


THREE.DRACOLoader.prototype = {

    constructor: THREE.DRACOLoader,

    load: function(url, onLoad, onProgress, onError) {
        const scope = this;
        const loader = new THREE.FileLoader(scope.manager);
        loader.setPath(this.path);
        loader.setResponseType('arraybuffer');
        loader.load(url, function(blob) {
            onLoad(scope.decodeDracoFile(blob));
        }, onProgress, onError);
    },

    setPath: function(value) {
        this.path = value;
    },

    decodeDracoFile: function(rawBuffer) {
        const scope = this;
        /*
         * Here is how to use Draco Javascript decoder and get the geometry.
         */
        const buffer = new DracoModule.DecoderBuffer();
        buffer.Init(new Int8Array(rawBuffer), rawBuffer.byteLength);
        const wrapper = new DracoModule.WebIDLWrapper();

        /*
         * Determine what type is this file, mesh or point cloud.
         */
        const geometryType = wrapper.GetEncodedGeometryType(buffer);
        if (geometryType == DracoModule.TRIANGULAR_MESH) {
          fileDisplayArea.innerText = "Loaded a mesh.\n";
        } else if (geometryType == DracoModule.POINT_CLOUD) {
          fileDisplayArea.innerText = "Loaded a point cloud.\n";
        } else {
          const errorMsg = "Error: Unknown geometry type.";
          fileDisplayArea.innerText = errorMsg;
          throw new Error(errorMsg);
        }
        return scope.convertDracoGeometryTo3JS(wrapper, geometryType, buffer);
    },

    convertDracoGeometryTo3JS: function(wrapper, geometryType, buffer) {
        let dracoGeometry;
        if (geometryType == DracoModule.TRIANGULAR_MESH) {
          dracoGeometry = wrapper.DecodeMeshFromBuffer(buffer);
        } else {
          dracoGeometry = wrapper.DecodePointCloudFromBuffer(buffer);
        }
        DracoModule.destroy(buffer);
        /*
         * Example on how to retrieve mesh and attributes.
         */
        let numFaces, numPoints, numVertexCoordinates, numTextureCoordinates, numAttributes;
        // For output basic geometry information.
        let geometryInfoStr;
        if (geometryType == DracoModule.TRIANGULAR_MESH) {
          numFaces = dracoGeometry.num_faces();
          geometryInfoStr += "Number of faces loaded: " + numFaces.toString()
              + ".\n";
        } else {
          numFaces = 0;
        }
        numPoints = dracoGeometry.num_points();
        numVertexCoordinates = numPoints * 3;
        numTextureCoordinates = numPoints * 2;
        numAttributes = dracoGeometry.num_attributes();
        geometryInfoStr = "Number of points loaded: " + numPoints.toString()
            + ".\n";
        geometryInfoStr += "Number of attributes loaded: " +
            numAttributes.toString() + ".\n";

        // Get position attribute. Must exists.
        const posAttId = wrapper.GetAttributeId(dracoGeometry,
                                                Module.POSITION);
        if (posAttId == -1) {
          const errorMsg = "No position attribute found in the mesh.";
          fileDisplayArea.innerText = errorMsg;
          DracoModule.destroy(wrapper);
          DracoModule.destroy(dracoGeometry);
          throw new Error(errorMsg);
        }
        const posAttribute = wrapper.GetAttribute(dracoGeometry, posAttId);
        const posAttributeData = new DracoModule.DracoFloat32Array();
        wrapper.GetAttributeFloatForAllPoints(
            dracoGeometry, posAttribute, posAttributeData);
        // Get color attributes if exists.
        const colorAttId = wrapper.GetAttributeId(dracoGeometry, Module.COLOR);
        let colAttributeData;
        if (colorAttId != -1) {
          geometryInfoStr += "\nLoaded color attribute.\n";
          const colAttribute = wrapper.GetAttribute(dracoGeometry, colorAttId);
          colAttributeData = new DracoModule.DracoFloat32Array();
          wrapper.GetAttributeFloatForAllPoints(dracoGeometry, colAttribute,
                                                colAttributeData);
        }

        // Get normal attributes if exists.
        const normalAttId =
            wrapper.GetAttributeId(dracoGeometry, Module.NORMAL);
        let norAttributeData;
        if (normalAttId != -1) {
          geometryInfoStr += "\nLoaded normal attribute.\n";
          const norAttribute = wrapper.GetAttribute(dracoGeometry, normalAttId);
          norAttributeData = new DracoModule.DracoFloat32Array();
          wrapper.GetAttributeFloatForAllPoints(dracoGeometry, norAttribute,
                                                norAttributeData);
        }

        // Get texture coord attributes if exists.
        const texCoordAttId =
            wrapper.GetAttributeId(dracoGeometry, Module.TEX_COORD);
        let textCoordAttributeData;
        if (texCoordAttId != -1) {
          geometryInfoStr += "\nLoaded texture coordinate attribute.\n";
          const texCoordAttribute = wrapper.GetAttribute(dracoGeometry, texCoordAttId);
          textCoordAttributeData = new DracoModule.DracoFloat32Array();
          wrapper.GetAttributeFloatForAllPoints(dracoGeometry, texCoordAttribute,
                                                textCoordAttributeData);
        }

        // Structure for converting to THREEJS geometry later.
        const geometryBuffer = {
            indices: [],
            vertices: [],
            normals: [],
            uvs: [],
            colors: []
        };
        for (let i = 0; i < numVertexCoordinates; i += 3) {
            geometryBuffer.vertices.push(
                posAttributeData.GetValue(i),
                posAttributeData.GetValue(i + 1),
                posAttributeData.GetValue(i + 2));
            // Add color.
            if (colorAttId != -1) {
              geometryBuffer.colors.push(
                  colAttributeData.GetValue(i),
                  colAttributeData.GetValue(i + 1),
                  colAttributeData.GetValue(i + 2));
            } else {
              // Default is white.
              geometryBuffer.colors.push(1.0, 1.0, 1.0);
            }
            // Add normal.
            if (normalAttId != -1) {
              geometryBuffer.normals.push(
                  norAttributeData.GetValue(i),
                  norAttributeData.GetValue(i + 1),
                  norAttributeData.GetValue(i + 2));
            }
        }

        // Add texture coordinates.
        if (texCoordAttId != -1) {
          for (let i = 0; i < numTextureCoordinates; i += 2) {
            geometryBuffer.uvs.push(
                textCoordAttributeData.GetValue(i),
                textCoordAttributeData.GetValue(i + 1));
          }
        }

        DracoModule.destroy(posAttributeData);
        if (colorAttId != -1)
          DracoModule.destroy(colAttributeData);
        if (normalAttId != -1)
          DracoModule.destroy(norAttributeData);
        if (texCoordAttId != -1)
          DracoModule.destroy(textCoordAttributeData);
        
        // For mesh, we need to generate the faces.
        if (geometryType == DracoModule.TRIANGULAR_MESH) {
          const numIndices = numFaces * 3;
          const ia = new DracoInt32Array();
          for (let i = 0; i < numFaces; ++i) {
            wrapper.GetFaceFromMesh(dracoGeometry, i, ia);
            geometryBuffer.indices.push(
                ia.GetValue(0), ia.GetValue(1), ia.GetValue(2));
          }
          DracoModule.destroy(ia);
        }
        DracoModule.destroy(wrapper);
        DracoModule.destroy(dracoGeometry);

        fileDisplayArea.innerText += geometryInfoStr;

        // Import data to Three JS geometry.
        const geometry = new THREE.BufferGeometry();
        if (geometryType == DracoModule.TRIANGULAR_MESH) {
          geometry.setIndex(new(geometryBuffer.indices.length > 65535 ?
                THREE.Uint32BufferAttribute : THREE.Uint16BufferAttribute)
              (geometryBuffer.indices, 1));
        }
        geometry.addAttribute('position',
            new THREE.Float32BufferAttribute(geometryBuffer.vertices, 3));
        geometry.addAttribute('color',
            new THREE.Float32BufferAttribute(geometryBuffer.colors, 3));
        if (normalAttId != -1) {
          geometry.addAttribute('normal',
              new THREE.Float32BufferAttribute(geometryBuffer.normals, 3));
        }
        if (texCoordAttId != -1) {
          geometry.addAttribute('uv',
              new THREE.Float32BufferAttribute(geometryBuffer.uvs, 2));
        }
        return geometry;
    }
};
