( function () {

	const _taskCache = new WeakMap();

	class DRACOLoader extends THREE.Loader {

		constructor( manager ) {

			super( manager );
			this.decoderPath = '';
			this.decoderConfig = {};
			this.decoderBinary = null;
			this.decoderPending = null;
			this.workerLimit = 4;
			this.workerPool = [];
			this.workerNextTaskID = 1;
			this.workerSourceURL = '';
			this.defaultAttributeIDs = {
				position: 'POSITION',
				normal: 'NORMAL',
				color: 'COLOR',
				uv: 'TEX_COORD'
			};
			this.defaultAttributeTypes = {
				position: 'Float32Array',
				normal: 'Float32Array',
				color: 'Float32Array',
				uv: 'Float32Array'
			};

		}

		setDecoderPath( path ) {

			this.decoderPath = path;
			return this;

		}

		setDecoderConfig( config ) {

			this.decoderConfig = config;
			return this;

		}

		setWorkerLimit( workerLimit ) {

			this.workerLimit = workerLimit;
			return this;

		}

		load( url, onLoad, onProgress, onError ) {

			const loader = new THREE.FileLoader( this.manager );
			loader.setPath( this.path );
			loader.setResponseType( 'arraybuffer' );
			loader.setRequestHeader( this.requestHeader );
			loader.setWithCredentials( this.withCredentials );
			loader.load( url, buffer => {

				const taskConfig = {
					attributeIDs: this.defaultAttributeIDs,
					attributeTypes: this.defaultAttributeTypes,
					useUniqueIDs: false
				};
				this.decodeGeometry( buffer, taskConfig ).then( onLoad ).catch( onError );

			}, onProgress, onError );

		}
		/** @deprecated Kept for backward-compatibility with previous DRACOLoader versions. */


		decodeDracoFile( buffer, callback, attributeIDs, attributeTypes ) {

			const taskConfig = {
				attributeIDs: attributeIDs || this.defaultAttributeIDs,
				attributeTypes: attributeTypes || this.defaultAttributeTypes,
				useUniqueIDs: !! attributeIDs
			};
			this.decodeGeometry( buffer, taskConfig ).then( callback );

		}

		decodeGeometry( buffer, taskConfig ) {

			// TODO: For backward-compatibility, support 'attributeTypes' objects containing
			// references (rather than names) to typed array constructors. These must be
			// serialized before sending them to the worker.
			for ( const attribute in taskConfig.attributeTypes ) {

				const type = taskConfig.attributeTypes[ attribute ];

				if ( type.BYTES_PER_ELEMENT !== undefined ) {

					taskConfig.attributeTypes[ attribute ] = type.name;

				}

			} //


			const taskKey = JSON.stringify( taskConfig ); // Check for an existing task using this buffer. A transferred buffer cannot be transferred
			// again from this thread.

			if ( _taskCache.has( buffer ) ) {

				const cachedTask = _taskCache.get( buffer );

				if ( cachedTask.key === taskKey ) {

					return cachedTask.promise;

				} else if ( buffer.byteLength === 0 ) {

					// Technically, it would be possible to wait for the previous task to complete,
					// transfer the buffer back, and decode again with the second configuration. That
					// is complex, and I don't know of any reason to decode a Draco buffer twice in
					// different ways, so this is left unimplemented.
					throw new Error( 'THREE.DRACOLoader: Unable to re-decode a buffer with different ' + 'settings. Buffer has already been transferred.' );

				}

			} //


			let worker;
			const taskID = this.workerNextTaskID ++;
			const taskCost = buffer.byteLength; // Obtain a worker and assign a task, and construct a geometry instance
			// when the task completes.

			const geometryPending = this._getWorker( taskID, taskCost ).then( _worker => {

				worker = _worker;
				return new Promise( ( resolve, reject ) => {

					worker._callbacks[ taskID ] = {
						resolve,
						reject
					};
					worker.postMessage( {
						type: 'decode',
						id: taskID,
						taskConfig,
						buffer
					}, [ buffer ] ); // this.debug();

				} );

			} ).then( message => this._createGeometry( message.geometry ) ); // Remove task from the task list.
			// Note: replaced '.finally()' with '.catch().then()' block - iOS 11 support (#19416)


			geometryPending.catch( () => true ).then( () => {

				if ( worker && taskID ) {

					this._releaseTask( worker, taskID ); // this.debug();

				}

			} ); // Cache the task result.

			_taskCache.set( buffer, {
				key: taskKey,
				promise: geometryPending
			} );

			return geometryPending;

		}

		_createGeometry( geometryData ) {

			const geometry = new THREE.BufferGeometry();

			if ( geometryData.index ) {

				geometry.setIndex( new THREE.BufferAttribute( geometryData.index.array, 1 ) );

			}

			for ( let i = 0; i < geometryData.attributes.length; i ++ ) {

				const attribute = geometryData.attributes[ i ];
				const name = attribute.name;
				const array = attribute.array;
				const itemSize = attribute.itemSize;
				geometry.setAttribute( name, new THREE.BufferAttribute( array, itemSize ) );

			}

			return geometry;

		}

		_loadLibrary( url, responseType ) {

			const loader = new THREE.FileLoader( this.manager );
			loader.setPath( this.decoderPath );
			loader.setResponseType( responseType );
			loader.setWithCredentials( this.withCredentials );
			return new Promise( ( resolve, reject ) => {

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

			} );

		}

		preload() {

			this._initDecoder();

			return this;

		}

		_initDecoder() {

			if ( this.decoderPending ) return this.decoderPending;
			const useJS = typeof WebAssembly !== 'object' || this.decoderConfig.type === 'js';
			const librariesPending = [];

			if ( useJS ) {

				librariesPending.push( this._loadLibrary( 'draco_decoder.js', 'text' ) );

			} else {

				librariesPending.push( this._loadLibrary( 'draco_wasm_wrapper.js', 'text' ) );
				librariesPending.push( this._loadLibrary( 'draco_decoder.wasm', 'arraybuffer' ) );

			}

			this.decoderPending = Promise.all( librariesPending ).then( libraries => {

				const jsContent = libraries[ 0 ];

				if ( ! useJS ) {

					this.decoderConfig.wasmBinary = libraries[ 1 ];

				}

				const fn = DRACOWorker.toString();
				const body = [ '/* draco decoder */', jsContent, '', '/* worker */', fn.substring( fn.indexOf( '{' ) + 1, fn.lastIndexOf( '}' ) ) ].join( '\n' );
				this.workerSourceURL = URL.createObjectURL( new Blob( [ body ] ) );

			} );
			return this.decoderPending;

		}

		_getWorker( taskID, taskCost ) {

			return this._initDecoder().then( () => {

				if ( this.workerPool.length < this.workerLimit ) {

					const worker = new Worker( this.workerSourceURL );
					worker._callbacks = {};
					worker._taskCosts = {};
					worker._taskLoad = 0;
					worker.postMessage( {
						type: 'init',
						decoderConfig: this.decoderConfig
					} );

					worker.onmessage = function ( e ) {

						const message = e.data;

						switch ( message.type ) {

							case 'decode':
								worker._callbacks[ message.id ].resolve( message );

								break;

							case 'error':
								worker._callbacks[ message.id ].reject( message );

								break;

							default:
								console.error( 'THREE.DRACOLoader: Unexpected message, "' + message.type + '"' );

						}

					};

					this.workerPool.push( worker );

				} else {

					this.workerPool.sort( function ( a, b ) {

						return a._taskLoad > b._taskLoad ? - 1 : 1;

					} );

				}

				const worker = this.workerPool[ this.workerPool.length - 1 ];
				worker._taskCosts[ taskID ] = taskCost;
				worker._taskLoad += taskCost;
				return worker;

			} );

		}

		_releaseTask( worker, taskID ) {

			worker._taskLoad -= worker._taskCosts[ taskID ];
			delete worker._callbacks[ taskID ];
			delete worker._taskCosts[ taskID ];

		}

		debug() {

			console.log( 'Task load: ', this.workerPool.map( worker => worker._taskLoad ) );

		}

		dispose() {

			for ( let i = 0; i < this.workerPool.length; ++ i ) {

				this.workerPool[ i ].terminate();

			}

			this.workerPool.length = 0;
			return this;

		}

	}
	/* WEB WORKER */


	function DRACOWorker() {

		let decoderConfig;
		let decoderPending;

		onmessage = function ( e ) {

			const message = e.data;

			switch ( message.type ) {

				case 'init':
					decoderConfig = message.decoderConfig;
					decoderPending = new Promise( function ( resolve
						/*, reject*/
					) {

						decoderConfig.onModuleLoaded = function ( draco ) {

							// Module is Promise-like. Wrap before resolving to avoid loop.
							resolve( {
								draco: draco
							} );

						};

						DracoDecoderModule( decoderConfig ); // eslint-disable-line no-undef

					} );
					break;

				case 'decode':
					const buffer = message.buffer;
					const taskConfig = message.taskConfig;
					decoderPending.then( module => {

						const draco = module.draco;
						const decoder = new draco.Decoder();
						const decoderBuffer = new draco.DecoderBuffer();
						decoderBuffer.Init( new Int8Array( buffer ), buffer.byteLength );

						try {

							const geometry = decodeGeometry( draco, decoder, decoderBuffer, taskConfig );
							const buffers = geometry.attributes.map( attr => attr.array.buffer );
							if ( geometry.index ) buffers.push( geometry.index.array.buffer );
							self.postMessage( {
								type: 'decode',
								id: message.id,
								geometry
							}, buffers );

						} catch ( error ) {

							console.error( error );
							self.postMessage( {
								type: 'error',
								id: message.id,
								error: error.message
							} );

						} finally {

							draco.destroy( decoderBuffer );
							draco.destroy( decoder );

						}

					} );
					break;

			}

		};

		function decodeGeometry( draco, decoder, decoderBuffer, taskConfig ) {

			const attributeIDs = taskConfig.attributeIDs;
			const attributeTypes = taskConfig.attributeTypes;
			let dracoGeometry;
			let decodingStatus;
			const geometryType = decoder.GetEncodedGeometryType( decoderBuffer );

			if ( geometryType === draco.TRIANGULAR_MESH ) {

				dracoGeometry = new draco.Mesh();
				decodingStatus = decoder.DecodeBufferToMesh( decoderBuffer, dracoGeometry );

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

				dracoGeometry = new draco.PointCloud();
				decodingStatus = decoder.DecodeBufferToPointCloud( decoderBuffer, dracoGeometry );

			} else {

				throw new Error( 'THREE.DRACOLoader: Unexpected geometry type.' );

			}

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

				throw new Error( 'THREE.DRACOLoader: Decoding failed: ' + decodingStatus.error_msg() );

			}

			const geometry = {
				index: null,
				attributes: []
			}; // Gather all vertex attributes.

			for ( const attributeName in attributeIDs ) {

				const attributeType = self[ attributeTypes[ attributeName ] ];
				let attribute;
				let attributeID; // A Draco file may be created with default vertex attributes, whose attribute IDs
				// are mapped 1:1 from their semantic name (POSITION, NORMAL, ...). Alternatively,
				// a Draco file may contain a custom set of attributes, identified by known unique
				// IDs. glTF files always do the latter, and `.drc` files typically do the former.

				if ( taskConfig.useUniqueIDs ) {

					attributeID = attributeIDs[ attributeName ];
					attribute = decoder.GetAttributeByUniqueId( dracoGeometry, attributeID );

				} else {

					attributeID = decoder.GetAttributeId( dracoGeometry, draco[ attributeIDs[ attributeName ] ] );
					if ( attributeID === - 1 ) continue;
					attribute = decoder.GetAttribute( dracoGeometry, attributeID );

				}

				geometry.attributes.push( decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) );

			} // Add index.


			if ( geometryType === draco.TRIANGULAR_MESH ) {

				geometry.index = decodeIndex( draco, decoder, dracoGeometry );

			}

			draco.destroy( dracoGeometry );
			return geometry;

		}

		function decodeIndex( draco, decoder, dracoGeometry ) {

			const numFaces = dracoGeometry.num_faces();
			const numIndices = numFaces * 3;
			const byteLength = numIndices * 4;

			const ptr = draco._malloc( byteLength );

			decoder.GetTrianglesUInt32Array( dracoGeometry, byteLength, ptr );
			const index = new Uint32Array( draco.HEAPF32.buffer, ptr, numIndices ).slice();

			draco._free( ptr );

			return {
				array: index,
				itemSize: 1
			};

		}

		function decodeAttribute( draco, decoder, dracoGeometry, attributeName, attributeType, attribute ) {

			const numComponents = attribute.num_components();
			const numPoints = dracoGeometry.num_points();
			const numValues = numPoints * numComponents;
			const byteLength = numValues * attributeType.BYTES_PER_ELEMENT;
			const dataType = getDracoDataType( draco, attributeType );

			const ptr = draco._malloc( byteLength );

			decoder.GetAttributeDataArrayForAllPoints( dracoGeometry, attribute, dataType, byteLength, ptr );
			const array = new attributeType( draco.HEAPF32.buffer, ptr, numValues ).slice();

			draco._free( ptr );

			return {
				name: attributeName,
				array: array,
				itemSize: numComponents
			};

		}

		function getDracoDataType( draco, attributeType ) {

			switch ( attributeType ) {

				case Float32Array:
					return draco.DT_FLOAT32;

				case Int8Array:
					return draco.DT_INT8;

				case Int16Array:
					return draco.DT_INT16;

				case Int32Array:
					return draco.DT_INT32;

				case Uint8Array:
					return draco.DT_UINT8;

				case Uint16Array:
					return draco.DT_UINT16;

				case Uint32Array:
					return draco.DT_UINT32;

			}

		}

	}

	THREE.DRACOLoader = DRACOLoader;

} )();
