/** 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.
 */

/**
 * @param {THREE.LoadingManager} manager
 */
THREE.DRACOLoader = function ( manager ) {

	this.timeLoaded = 0;
	this.manager = manager || THREE.DefaultLoadingManager;
	this.materials = null;
	this.verbosity = 0;
	this.attributeOptions = {};
	this.drawMode = THREE.TrianglesDrawMode;
	// Native Draco attribute type to Three.JS attribute type.
	this.nativeAttributeMap = {
		position: "POSITION",
		normal: "NORMAL",
		color: "COLOR",
		uv: "TEX_COORD"
	};

};

THREE.DRACOLoader.prototype = {
	constructor: THREE.DRACOLoader,

	load: function ( url, onLoad, onProgress, onError ) {

		var scope = this;
		var loader = new THREE.FileLoader( scope.manager );
		loader.setPath( this.path );
		loader.setResponseType( "arraybuffer" );
		loader.load(
			url,
			function ( blob ) {

				scope.decodeDracoFile( blob, onLoad );

			},
			onProgress,
			onError
		);

	},

	setPath: function ( value ) {

		this.path = value;
		return this;

	},

	setVerbosity: function ( level ) {

		this.verbosity = level;
		return this;

	},

	/**
	 *  Sets desired mode for generated geometry indices.
	 *  Can be either:
	 *      THREE.TrianglesDrawMode
	 *      THREE.TriangleStripDrawMode
	 */
	setDrawMode: function ( drawMode ) {

		this.drawMode = drawMode;
		return this;

	},

	/**
	 * Skips dequantization for a specific attribute.
	 * |attributeName| is the THREE.js name of the given attribute type.
	 * The only currently supported |attributeName| is 'position', more may be
	 * added in future.
	 */
	setSkipDequantization: function ( attributeName, skip ) {

		var skipDequantization = true;
		if ( typeof skip !== "undefined" ) skipDequantization = skip;
		this.getAttributeOptions(
			attributeName
		).skipDequantization = skipDequantization;
		return this;

	},

	/**
	 * Decompresses a Draco buffer. Names of attributes (for ID and type maps)
	 * must be one of the supported three.js types, including: position, color,
	 * normal, uv, uv2, skinIndex, skinWeight.
	 *
	 * @param {ArrayBuffer} rawBuffer
	 * @param {Function} callback
	 * @param {Object|undefined} attributeUniqueIdMap Provides a pre-defined ID
	 *     for each attribute in the geometry to be decoded. If given,
	 *     `attributeTypeMap` is required and `nativeAttributeMap` will be
	 *     ignored.
	 * @param {Object|undefined} attributeTypeMap Provides a predefined data
	 *     type (as a typed array constructor) for each attribute in the
	 *     geometry to be decoded.
	 */
	decodeDracoFile: function (
		rawBuffer,
		callback,
		attributeUniqueIdMap,
		attributeTypeMap
	) {

		var scope = this;
		THREE.DRACOLoader.getDecoderModule().then( function ( module ) {

			scope.decodeDracoFileInternal(
				rawBuffer,
				module.decoder,
				callback,
				attributeUniqueIdMap,
				attributeTypeMap
			);

		} );

	},

	decodeDracoFileInternal: function (
		rawBuffer,
		dracoDecoder,
		callback,
		attributeUniqueIdMap,
		attributeTypeMap
	) {

		/*
		 * Here is how to use Draco Javascript decoder and get the geometry.
		 */
		var buffer = new dracoDecoder.DecoderBuffer();
		buffer.Init( new Int8Array( rawBuffer ), rawBuffer.byteLength );
		var decoder = new dracoDecoder.Decoder();

		/*
		 * Determine what type is this file: mesh or point cloud.
		 */
		var geometryType = decoder.GetEncodedGeometryType( buffer );
		if ( geometryType == dracoDecoder.TRIANGULAR_MESH ) {

			if ( this.verbosity > 0 ) {

				console.log( "Loaded a mesh." );

			}

		} else if ( geometryType == dracoDecoder.POINT_CLOUD ) {

			if ( this.verbosity > 0 ) {

				console.log( "Loaded a point cloud." );

			}

		} else {

			var errorMsg = "THREE.DRACOLoader: Unknown geometry type.";
			console.error( errorMsg );
			throw new Error( errorMsg );

		}
		callback(
			this.convertDracoGeometryTo3JS(
				dracoDecoder,
				decoder,
				geometryType,
				buffer,
				attributeUniqueIdMap,
				attributeTypeMap
			)
		);

	},

	addAttributeToGeometry: function (
		dracoDecoder,
		decoder,
		dracoGeometry,
		attributeName,
		attributeType,
		attribute,
		geometry,
		geometryBuffer
	) {

		if ( attribute.ptr === 0 ) {

			var errorMsg = "THREE.DRACOLoader: No attribute " + attributeName;
			console.error( errorMsg );
			throw new Error( errorMsg );

		}

		var numComponents = attribute.num_components();
		var numPoints = dracoGeometry.num_points();
		var numValues = numPoints * numComponents;
		var attributeData;
		var TypedBufferAttribute;

		switch ( attributeType ) {

			case Float32Array:
				attributeData = new dracoDecoder.DracoFloat32Array();
				decoder.GetAttributeFloatForAllPoints(
					dracoGeometry,
					attribute,
					attributeData
				);
				geometryBuffer[ attributeName ] = new Float32Array( numValues );
				TypedBufferAttribute = THREE.Float32BufferAttribute;
				break;

			case Int8Array:
				attributeData = new dracoDecoder.DracoInt8Array();
				decoder.GetAttributeInt8ForAllPoints(
					dracoGeometry,
					attribute,
					attributeData
				);
				geometryBuffer[ attributeName ] = new Int8Array( numValues );
				TypedBufferAttribute = THREE.Int8BufferAttribute;
				break;

			case Int16Array:
				attributeData = new dracoDecoder.DracoInt16Array();
				decoder.GetAttributeInt16ForAllPoints(
					dracoGeometry,
					attribute,
					attributeData
				);
				geometryBuffer[ attributeName ] = new Int16Array( numValues );
				TypedBufferAttribute = THREE.Int16BufferAttribute;
				break;

			case Int32Array:
				attributeData = new dracoDecoder.DracoInt32Array();
				decoder.GetAttributeInt32ForAllPoints(
					dracoGeometry,
					attribute,
					attributeData
				);
				geometryBuffer[ attributeName ] = new Int32Array( numValues );
				TypedBufferAttribute = THREE.Int32BufferAttribute;
				break;

			case Uint8Array:
				attributeData = new dracoDecoder.DracoUInt8Array();
				decoder.GetAttributeUInt8ForAllPoints(
					dracoGeometry,
					attribute,
					attributeData
				);
				geometryBuffer[ attributeName ] = new Uint8Array( numValues );
				TypedBufferAttribute = THREE.Uint8BufferAttribute;
				break;

			case Uint16Array:
				attributeData = new dracoDecoder.DracoUInt16Array();
				decoder.GetAttributeUInt16ForAllPoints(
					dracoGeometry,
					attribute,
					attributeData
				);
				geometryBuffer[ attributeName ] = new Uint16Array( numValues );
				TypedBufferAttribute = THREE.Uint16BufferAttribute;
				break;

			case Uint32Array:
				attributeData = new dracoDecoder.DracoUInt32Array();
				decoder.GetAttributeUInt32ForAllPoints(
					dracoGeometry,
					attribute,
					attributeData
				);
				geometryBuffer[ attributeName ] = new Uint32Array( numValues );
				TypedBufferAttribute = THREE.Uint32BufferAttribute;
				break;

			default:
				var errorMsg = "THREE.DRACOLoader: Unexpected attribute type.";
				console.error( errorMsg );
				throw new Error( errorMsg );

		}

		// Copy data from decoder.
		for ( var i = 0; i < numValues; i ++ ) {

			geometryBuffer[ attributeName ][ i ] = attributeData.GetValue( i );

		}
		// Add attribute to THREEJS geometry for rendering.
		geometry.addAttribute(
			attributeName,
			new TypedBufferAttribute( geometryBuffer[ attributeName ], numComponents )
		);
		dracoDecoder.destroy( attributeData );

	},

	convertDracoGeometryTo3JS: function (
		dracoDecoder,
		decoder,
		geometryType,
		buffer,
		attributeUniqueIdMap,
		attributeTypeMap
	) {

		// TODO: Should not assume native Draco attribute IDs apply.
		if ( this.getAttributeOptions( "position" ).skipDequantization === true ) {

			decoder.SkipAttributeTransform( dracoDecoder.POSITION );

		}
		var dracoGeometry;
		var decodingStatus;
		var start_time = performance.now();
		if ( geometryType === dracoDecoder.TRIANGULAR_MESH ) {

			dracoGeometry = new dracoDecoder.Mesh();
			decodingStatus = decoder.DecodeBufferToMesh( buffer, dracoGeometry );

		} else {

			dracoGeometry = new dracoDecoder.PointCloud();
			decodingStatus = decoder.DecodeBufferToPointCloud( buffer, dracoGeometry );

		}
		if ( ! decodingStatus.ok() || dracoGeometry.ptr == 0 ) {

			var errorMsg = "THREE.DRACOLoader: Decoding failed: ";
			errorMsg += decodingStatus.error_msg();
			console.error( errorMsg );
			dracoDecoder.destroy( decoder );
			dracoDecoder.destroy( dracoGeometry );
			throw new Error( errorMsg );

		}

		var decode_end = performance.now();
		dracoDecoder.destroy( buffer );
		/*
		 * Example on how to retrieve mesh and attributes.
		 */
		var numFaces;
		if ( geometryType == dracoDecoder.TRIANGULAR_MESH ) {

			numFaces = dracoGeometry.num_faces();
			if ( this.verbosity > 0 ) {

				console.log( "Number of faces loaded: " + numFaces.toString() );

			}

		} else {

			numFaces = 0;

		}

		var numPoints = dracoGeometry.num_points();
		var numAttributes = dracoGeometry.num_attributes();
		if ( this.verbosity > 0 ) {

			console.log( "Number of points loaded: " + numPoints.toString() );
			console.log( "Number of attributes loaded: " + numAttributes.toString() );

		}

		// Verify if there is position attribute.
		// TODO: Should not assume native Draco attribute IDs apply.
		var posAttId = decoder.GetAttributeId( dracoGeometry, dracoDecoder.POSITION );
		if ( posAttId == - 1 ) {

			var errorMsg = "THREE.DRACOLoader: No position attribute found.";
			console.error( errorMsg );
			dracoDecoder.destroy( decoder );
			dracoDecoder.destroy( dracoGeometry );
			throw new Error( errorMsg );

		}
		var posAttribute = decoder.GetAttribute( dracoGeometry, posAttId );

		// Structure for converting to THREEJS geometry later.
		var geometryBuffer = {};
		// Import data to Three JS geometry.
		var geometry = new THREE.BufferGeometry();

		// Do not use both the native attribute map and a provided (e.g. glTF) map.
		if ( attributeUniqueIdMap ) {

			// Add attributes of user specified unique id. E.g. GLTF models.
			for ( var attributeName in attributeUniqueIdMap ) {

				var attributeType = attributeTypeMap[ attributeName ];
				var attributeId = attributeUniqueIdMap[ attributeName ];
				var attribute = decoder.GetAttributeByUniqueId(
					dracoGeometry,
					attributeId
				);
				this.addAttributeToGeometry(
					dracoDecoder,
					decoder,
					dracoGeometry,
					attributeName,
					attributeType,
					attribute,
					geometry,
					geometryBuffer
				);

			}

		} else {

			// Add native Draco attribute type to geometry.
			for ( var attributeName in this.nativeAttributeMap ) {

				var attId = decoder.GetAttributeId(
					dracoGeometry,
					dracoDecoder[ this.nativeAttributeMap[ attributeName ] ]
				);
				if ( attId !== - 1 ) {

					if ( this.verbosity > 0 ) {

						console.log( "Loaded " + attributeName + " attribute." );

					}
					var attribute = decoder.GetAttribute( dracoGeometry, attId );
					this.addAttributeToGeometry(
						dracoDecoder,
						decoder,
						dracoGeometry,
						attributeName,
						Float32Array,
						attribute,
						geometry,
						geometryBuffer
					);

				}

			}

		}

		// For mesh, we need to generate the faces.
		if ( geometryType == dracoDecoder.TRIANGULAR_MESH ) {

			if ( this.drawMode === THREE.TriangleStripDrawMode ) {

				var stripsArray = new dracoDecoder.DracoInt32Array();
				decoder.GetTriangleStripsFromMesh(
					dracoGeometry,
					stripsArray
				);
				geometryBuffer.indices = new Uint32Array( stripsArray.size() );
				for ( var i = 0; i < stripsArray.size(); ++ i ) {

					geometryBuffer.indices[ i ] = stripsArray.GetValue( i );

				}
				dracoDecoder.destroy( stripsArray );

			} else {

				var numIndices = numFaces * 3;
				geometryBuffer.indices = new Uint32Array( numIndices );
				var ia = new dracoDecoder.DracoInt32Array();
				for ( var i = 0; i < numFaces; ++ i ) {

					decoder.GetFaceFromMesh( dracoGeometry, i, ia );
					var index = i * 3;
					geometryBuffer.indices[ index ] = ia.GetValue( 0 );
					geometryBuffer.indices[ index + 1 ] = ia.GetValue( 1 );
					geometryBuffer.indices[ index + 2 ] = ia.GetValue( 2 );

				}
				dracoDecoder.destroy( ia );

			}

		}

		geometry.drawMode = this.drawMode;
		if ( geometryType == dracoDecoder.TRIANGULAR_MESH ) {

			geometry.setIndex(
				new ( geometryBuffer.indices.length > 65535
					? THREE.Uint32BufferAttribute
					: THREE.Uint16BufferAttribute )( geometryBuffer.indices, 1 )
			);

		}

		// TODO: Should not assume native Draco attribute IDs apply.
		// TODO: Can other attribute types be quantized?
		var posTransform = new dracoDecoder.AttributeQuantizationTransform();
		if ( posTransform.InitFromAttribute( posAttribute ) ) {

			// Quantized attribute. Store the quantization parameters into the
			// THREE.js attribute.
			geometry.attributes[ "position" ].isQuantized = true;
			geometry.attributes[ "position" ].maxRange = posTransform.range();
			geometry.attributes[
				"position"
			].numQuantizationBits = posTransform.quantization_bits();
			geometry.attributes[ "position" ].minValues = new Float32Array( 3 );
			for ( var i = 0; i < 3; ++ i ) {

				geometry.attributes[ "position" ].minValues[ i ] = posTransform.min_value(
					i
				);

			}

		}
		dracoDecoder.destroy( posTransform );
		dracoDecoder.destroy( decoder );
		dracoDecoder.destroy( dracoGeometry );

		this.decode_time = decode_end - start_time;
		this.import_time = performance.now() - decode_end;

		if ( this.verbosity > 0 ) {

			console.log( "Decode time: " + this.decode_time );
			console.log( "Import time: " + this.import_time );

		}
		return geometry;

	},

	isVersionSupported: function ( version, callback ) {

		THREE.DRACOLoader.getDecoderModule().then( function ( module ) {

			callback( module.decoder.isVersionSupported( version ) );

		} );

	},

	getAttributeOptions: function ( attributeName ) {

		if ( typeof this.attributeOptions[ attributeName ] === "undefined" )
			this.attributeOptions[ attributeName ] = {};
		return this.attributeOptions[ attributeName ];

	}
};

THREE.DRACOLoader.decoderPath = "./";
THREE.DRACOLoader.decoderConfig = {};
THREE.DRACOLoader.decoderModulePromise = null;

/**
 * Sets the base path for decoder source files.
 * @param {string} path
 */
THREE.DRACOLoader.setDecoderPath = function ( path ) {

	THREE.DRACOLoader.decoderPath = path;

};

/**
 * Sets decoder configuration and releases singleton decoder module. Module
 * will be recreated with the next decoding call.
 * @param {Object} config
 */
THREE.DRACOLoader.setDecoderConfig = function ( config ) {

	var wasmBinary = THREE.DRACOLoader.decoderConfig.wasmBinary;
	THREE.DRACOLoader.decoderConfig = config || {};
	THREE.DRACOLoader.releaseDecoderModule();

	// Reuse WASM binary.
	if ( wasmBinary ) THREE.DRACOLoader.decoderConfig.wasmBinary = wasmBinary;

};

/**
 * Releases the singleton DracoDecoderModule instance. Module will be recreated
 * with the next decoding call.
 */
THREE.DRACOLoader.releaseDecoderModule = function () {

	THREE.DRACOLoader.decoderModulePromise = null;

};

/**
 * Gets WebAssembly or asm.js singleton instance of DracoDecoderModule
 * after testing for browser support. Returns Promise that resolves when
 * module is available.
 * @return {Promise<{decoder: DracoDecoderModule}>}
 */
THREE.DRACOLoader.getDecoderModule = function () {

	var scope = this;
	var path = THREE.DRACOLoader.decoderPath;
	var config = THREE.DRACOLoader.decoderConfig;
	var promise = THREE.DRACOLoader.decoderModulePromise;

	if ( promise ) return promise;

	// Load source files.
	if ( typeof DracoDecoderModule !== "undefined" ) {

		// Loaded externally.
		promise = Promise.resolve();

	} else if ( typeof WebAssembly !== "object" || config.type === "js" ) {

		// Load with asm.js.
		promise = THREE.DRACOLoader._loadScript( path + "draco_decoder.js" );

	} else {

		// Load with WebAssembly.
		config.wasmBinaryFile = path + "draco_decoder.wasm";
		promise = THREE.DRACOLoader._loadScript( path + "draco_wasm_wrapper.js" )
			.then( function () {

				return THREE.DRACOLoader._loadArrayBuffer( config.wasmBinaryFile );

			} )
			.then( function ( wasmBinary ) {

				config.wasmBinary = wasmBinary;

			} );

	}

	// Wait for source files, then create and return a decoder.
	promise = promise.then( function () {

		return new Promise( function ( resolve ) {

			config.onModuleLoaded = function ( decoder ) {

				scope.timeLoaded = performance.now();
				// Module is Promise-like. Wrap before resolving to avoid loop.
				resolve( { decoder: decoder } );

			};
			DracoDecoderModule( config );

		} );

	} );

	THREE.DRACOLoader.decoderModulePromise = promise;
	return promise;

};

/**
 * @param {string} src
 * @return {Promise}
 */
THREE.DRACOLoader._loadScript = function ( src ) {

	var prevScript = document.getElementById( "decoder_script" );
	if ( prevScript !== null ) {

		prevScript.parentNode.removeChild( prevScript );

	}
	var head = document.getElementsByTagName( "head" )[ 0 ];
	var script = document.createElement( "script" );
	script.id = "decoder_script";
	script.type = "text/javascript";
	script.src = src;
	return new Promise( function ( resolve ) {

		script.onload = resolve;
		head.appendChild( script );

	} );

};

/**
 * @param {string} src
 * @return {Promise}
 */
THREE.DRACOLoader._loadArrayBuffer = function ( src ) {

	var loader = new THREE.FileLoader();
	loader.setResponseType( "arraybuffer" );
	return new Promise( function ( resolve, reject ) {

		loader.load( src, resolve, undefined, reject );

	} );

};
