/**
 * Cesium - https://github.com/CesiumGS/cesium
 *
 * Copyright 2011-2020 Cesium Contributors
 *
 * 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.
 *
 * Columbus View (Pat. Pend.)
 *
 * Portions licensed separately.
 * See https://github.com/CesiumGS/cesium/blob/master/LICENSE.md for full licensing details.
 */
define(['./when-8d13db60', './Check-70bec281', './Math-61ede240', './RuntimeError-ba10bc3e', './WebGLConstants-4c11ee5f', './ComponentDatatype-5862616f', './IndexDatatype-9435b55f', './createTaskProcessorWorker'], function (when, Check, _Math, RuntimeError, WebGLConstants, ComponentDatatype, IndexDatatype, createTaskProcessorWorker) { 'use strict';

    /* global require */

    var draco;

    function decodeIndexArray(dracoGeometry, dracoDecoder) {
        var numPoints = dracoGeometry.num_points();
        var numFaces = dracoGeometry.num_faces();
        var faceIndices = new draco.DracoInt32Array();
        var numIndices = numFaces * 3;
        var indexArray = IndexDatatype.IndexDatatype.createTypedArray(numPoints, numIndices);

        var offset = 0;
        for (var i = 0; i < numFaces; ++i) {
            dracoDecoder.GetFaceFromMesh(dracoGeometry, i, faceIndices);

            indexArray[offset + 0] = faceIndices.GetValue(0);
            indexArray[offset + 1] = faceIndices.GetValue(1);
            indexArray[offset + 2] = faceIndices.GetValue(2);
            offset += 3;
        }

        draco.destroy(faceIndices);

        return {
            typedArray : indexArray,
            numberOfIndices : numIndices
        };
    }

    function decodeQuantizedDracoTypedArray(dracoGeometry, dracoDecoder, dracoAttribute, quantization, vertexArrayLength) {
        var vertexArray;
        var attributeData;
        if (quantization.quantizationBits <= 8) {
            attributeData = new draco.DracoUInt8Array();
            vertexArray = new Uint8Array(vertexArrayLength);
            dracoDecoder.GetAttributeUInt8ForAllPoints(dracoGeometry, dracoAttribute, attributeData);
        } else {
            attributeData = new draco.DracoUInt16Array();
            vertexArray = new Uint16Array(vertexArrayLength);
            dracoDecoder.GetAttributeUInt16ForAllPoints(dracoGeometry, dracoAttribute, attributeData);
        }

        for (var i = 0; i < vertexArrayLength; ++i) {
            vertexArray[i] = attributeData.GetValue(i);
        }

        draco.destroy(attributeData);
        return vertexArray;
    }

    function decodeDracoTypedArray(dracoGeometry, dracoDecoder, dracoAttribute, vertexArrayLength) {
        var vertexArray;
        var attributeData;

        // Some attribute types are casted down to 32 bit since Draco only returns 32 bit values
        switch (dracoAttribute.data_type()) {
            case 1: case 11: // DT_INT8 or DT_BOOL
            attributeData = new draco.DracoInt8Array();
            vertexArray = new Int8Array(vertexArrayLength);
            dracoDecoder.GetAttributeInt8ForAllPoints(dracoGeometry, dracoAttribute, attributeData);
            break;
            case 2: // DT_UINT8
                attributeData = new draco.DracoUInt8Array();
                vertexArray = new Uint8Array(vertexArrayLength);
                dracoDecoder.GetAttributeUInt8ForAllPoints(dracoGeometry, dracoAttribute, attributeData);
                break;
            case 3: // DT_INT16
                attributeData = new draco.DracoInt16Array();
                vertexArray = new Int16Array(vertexArrayLength);
                dracoDecoder.GetAttributeInt16ForAllPoints(dracoGeometry, dracoAttribute, attributeData);
                break;
            case 4: // DT_UINT16
                attributeData = new draco.DracoUInt16Array();
                vertexArray = new Uint16Array(vertexArrayLength);
                dracoDecoder.GetAttributeUInt16ForAllPoints(dracoGeometry, dracoAttribute, attributeData);
                break;
            case 5: case 7: // DT_INT32 or DT_INT64
            attributeData = new draco.DracoInt32Array();
            vertexArray = new Int32Array(vertexArrayLength);
            dracoDecoder.GetAttributeInt32ForAllPoints(dracoGeometry, dracoAttribute, attributeData);
            break;
            case 6: case 8: // DT_UINT32 or DT_UINT64
            attributeData = new draco.DracoUInt32Array();
            vertexArray = new Uint32Array(vertexArrayLength);
            dracoDecoder.GetAttributeUInt32ForAllPoints(dracoGeometry, dracoAttribute, attributeData);
            break;
            case 9: case 10: // DT_FLOAT32 or DT_FLOAT64
            attributeData = new draco.DracoFloat32Array();
            vertexArray = new Float32Array(vertexArrayLength);
            dracoDecoder.GetAttributeFloatForAllPoints(dracoGeometry, dracoAttribute, attributeData);
            break;
        }

        for (var i = 0; i < vertexArrayLength; ++i) {
            vertexArray[i] = attributeData.GetValue(i);
        }

        draco.destroy(attributeData);
        return vertexArray;
    }

    function decodeAttribute(dracoGeometry, dracoDecoder, dracoAttribute) {
        var numPoints = dracoGeometry.num_points();
        var numComponents = dracoAttribute.num_components();

        var quantization;
        var transform = new draco.AttributeQuantizationTransform();
        if (transform.InitFromAttribute(dracoAttribute)) {
            var minValues = new Array(numComponents);
            for (var i = 0; i < numComponents; ++i) {
                minValues[i] = transform.min_value(i);
            }
            quantization = {
                quantizationBits : transform.quantization_bits(),
                minValues : minValues,
                range : transform.range(),
                octEncoded : false
            };
        }
        draco.destroy(transform);

        transform = new draco.AttributeOctahedronTransform();
        if (transform.InitFromAttribute(dracoAttribute)) {
            quantization = {
                quantizationBits : transform.quantization_bits(),
                octEncoded : true
            };
        }
        draco.destroy(transform);

        var vertexArrayLength = numPoints * numComponents;
        var vertexArray;
        if (when.defined(quantization)) {
            vertexArray = decodeQuantizedDracoTypedArray(dracoGeometry, dracoDecoder, dracoAttribute, quantization, vertexArrayLength);
        } else {
            vertexArray = decodeDracoTypedArray(dracoGeometry, dracoDecoder, dracoAttribute, vertexArrayLength);
        }

        var componentDatatype = ComponentDatatype.ComponentDatatype.fromTypedArray(vertexArray);

        return {
            array : vertexArray,
            data : {
                componentsPerAttribute : numComponents,
                componentDatatype : componentDatatype,
                byteOffset : dracoAttribute.byte_offset(),
                byteStride : ComponentDatatype.ComponentDatatype.getSizeInBytes(componentDatatype) * numComponents,
                normalized : dracoAttribute.normalized(),
                quantization : quantization
            }
        };
    }

    function decodePointCloud(parameters) {
        var dracoDecoder = new draco.Decoder();

        if (parameters.dequantizeInShader) {
            dracoDecoder.SkipAttributeTransform(draco.POSITION);
            dracoDecoder.SkipAttributeTransform(draco.NORMAL);
        }

        var buffer = new draco.DecoderBuffer();
        buffer.Init(parameters.buffer, parameters.buffer.length);

        var geometryType = dracoDecoder.GetEncodedGeometryType(buffer);
        if (geometryType !== draco.POINT_CLOUD) {
            throw new RuntimeError.RuntimeError('Draco geometry type must be POINT_CLOUD.');
        }

        var dracoPointCloud = new draco.PointCloud();
        var decodingStatus = dracoDecoder.DecodeBufferToPointCloud(buffer, dracoPointCloud);
        if (!decodingStatus.ok() || dracoPointCloud.ptr === 0) {
            throw new RuntimeError.RuntimeError('Error decoding draco point cloud: ' + decodingStatus.error_msg());
        }

        draco.destroy(buffer);

        var result = {};

        var properties = parameters.properties;
        for (var propertyName in properties) {
            if (properties.hasOwnProperty(propertyName)) {
                var attributeId = properties[propertyName];
                var dracoAttribute = dracoDecoder.GetAttributeByUniqueId(dracoPointCloud, attributeId);
                result[propertyName] = decodeAttribute(dracoPointCloud, dracoDecoder, dracoAttribute);
            }
        }

        draco.destroy(dracoPointCloud);
        draco.destroy(dracoDecoder);

        return result;
    }

    function decodePrimitive(parameters) {
        var dracoDecoder = new draco.Decoder();

        // Skip all parameter types except generic
        var attributesToSkip = ['POSITION', 'NORMAL', 'COLOR', 'TEX_COORD'];
        if (parameters.dequantizeInShader) {
            for (var i = 0; i < attributesToSkip.length; ++i) {
                dracoDecoder.SkipAttributeTransform(draco[attributesToSkip[i]]);
            }
        }

        var bufferView = parameters.bufferView;
        var buffer = new draco.DecoderBuffer();
        buffer.Init(parameters.array, bufferView.byteLength);

        var geometryType = dracoDecoder.GetEncodedGeometryType(buffer);
        if (geometryType !== draco.TRIANGULAR_MESH) {
            throw new RuntimeError.RuntimeError('Unsupported draco mesh geometry type.');
        }

        var dracoGeometry = new draco.Mesh();
        var decodingStatus = dracoDecoder.DecodeBufferToMesh(buffer, dracoGeometry);
        if (!decodingStatus.ok() || dracoGeometry.ptr === 0) {
            throw new RuntimeError.RuntimeError('Error decoding draco mesh geometry: ' + decodingStatus.error_msg());
        }

        draco.destroy(buffer);

        var attributeData = {};

        var compressedAttributes = parameters.compressedAttributes;
        for (var attributeName in compressedAttributes) {
            if (compressedAttributes.hasOwnProperty(attributeName)) {
                var compressedAttribute = compressedAttributes[attributeName];
                var dracoAttribute = dracoDecoder.GetAttributeByUniqueId(dracoGeometry, compressedAttribute);
                attributeData[attributeName] = decodeAttribute(dracoGeometry, dracoDecoder, dracoAttribute);
            }
        }

        var result = {
            indexArray : decodeIndexArray(dracoGeometry, dracoDecoder),
            attributeData : attributeData
        };

        draco.destroy(dracoGeometry);
        draco.destroy(dracoDecoder);

        return result;
    }

    function decode(parameters) {
        if (when.defined(parameters.primitive)) {
            return decodePrimitive(parameters);
        }
        return decodePointCloud(parameters);
    }

    function initWorker(dracoModule) {
        draco = dracoModule;
        self.onmessage = createTaskProcessorWorker(decode);
        self.postMessage(true);
    }

    function decodeDraco(event) {
        var data = event.data;

        // Expect the first message to be to load a web assembly module
        var wasmConfig = data.webAssemblyConfig;
        if (when.defined(wasmConfig)) {
            // Require and compile WebAssembly module, or use fallback if not supported
            return require([wasmConfig.modulePath], function(dracoModule) {
                if (when.defined(wasmConfig.wasmBinaryFile)) {
                    if (!when.defined(dracoModule)) {
                        dracoModule = self.DracoDecoderModule;
                    }

                    dracoModule(wasmConfig).then(function (compiledModule) {
                        initWorker(compiledModule);
                    });
                } else {
                    initWorker(dracoModule());
                }
            });
        }
    }

    return decodeDraco;

});
