import { EventDispatcher } from "../../2d/events";
import { GlobalLoader } from "../../2d/loader";
import { rgb2hex, getEnv } from "../../2d/utils";
import {  LightMaterial } from "../materials/LightMaterial";
import { GLTFCubicSplineInterpolant } from "./GLTFCubicSplineInterpolant";

var THREE: any
/* BINARY EXTENSION */

var BINARY_EXTENSION_BUFFER_NAME = 'binary_glTF';
var BINARY_EXTENSION_HEADER_MAGIC = 'glTF';
var BINARY_EXTENSION_HEADER_LENGTH = 12;
var BINARY_EXTENSION_CHUNK_TYPES = { JSON: 0x4E4F534A, BIN: 0x004E4942 };

/*********************************/
/********** EXTENSIONS ***********/
/*********************************/

var EXTENSIONS = {
	KHR_BINARY_GLTF: 'KHR_binary_glTF',
	KHR_DRACO_MESH_COMPRESSION: 'KHR_draco_mesh_compression',
	KHR_LIGHTS_PUNCTUAL: 'KHR_lights_punctual',
	KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS: 'KHR_materials_pbrSpecularGlossiness',
	KHR_MATERIALS_UNLIT: 'KHR_materials_unlit',
	MSFT_TEXTURE_DDS: 'MSFT_texture_dds'
};

/*********************************/
/********** INTERNALS ************/
/*********************************/

/* CONSTANTS */

var WEBGL_CONSTANTS = {
	FLOAT: 5126,
	//FLOAT_MAT2: 35674,
	FLOAT_MAT3: 35675,
	FLOAT_MAT4: 35676,
	FLOAT_VEC2: 35664,
	FLOAT_VEC3: 35665,
	FLOAT_VEC4: 35666,
	LINEAR: 9729,
	REPEAT: 10497,
	SAMPLER_2D: 35678,
	POINTS: 0,
	LINES: 1,
	LINE_LOOP: 2,
	LINE_STRIP: 3,
	TRIANGLES: 4,
	TRIANGLE_STRIP: 5,
	TRIANGLE_FAN: 6,
	UNSIGNED_BYTE: 5121,
	UNSIGNED_SHORT: 5123
};


var WEBGL_COMPONENT_TYPES = {
	5120: Int8Array,
	5121: Uint8Array,
	5122: Int16Array,
	5123: Uint16Array,
	5125: Uint32Array,
	5126: Float32Array
};


var WEBGL_TYPE_SIZES = {
	'SCALAR': 1,
	'VEC2': 2,
	'VEC3': 3,
	'VEC4': 4,
	'MAT2': 4,
	'MAT3': 9,
	'MAT4': 16
};

var ATTRIBUTES = {
	POSITION: 'position',
	NORMAL: 'normal',
	TEXCOORD_0: 'uv',
	TEXCOORD0: 'uv', // deprecated
	TEXCOORD: 'uv', // deprecated
	TEXCOORD_1: 'uv2',
	COLOR_0: 'color',
	COLOR0: 'color', // deprecated
	COLOR: 'color', // deprecated
	WEIGHTS_0: 'skinWeight',
	WEIGHT: 'skinWeight', // deprecated
	JOINTS_0: 'skinIndex',
	JOINT: 'skinIndex' // deprecated
};

var PATH_PROPERTIES = {
	scale: 'scale',
	translation: 'position',
	rotation: 'quaternion',
	weights: 'morphTargetInfluences',
	position: "position"
};

var INTERPOLATION = {
	CUBICSPLINE: THREE.InterpolateSmooth, // We use custom interpolation GLTFCubicSplineInterpolation for CUBICSPLINE.
	// KeyframeTrack.optimize() can't handle glTF Cubic Spline output values layout,
	// using THREE.InterpolateSmooth for KeyframeTrack instantiation to prevent optimization.
	// See KeyframeTrack.optimize() for the detail.
	LINEAR: THREE.InterpolateLinear,
	STEP: THREE.InterpolateDiscrete
};

class GLTFLoader extends EventDispatcher {
	constructor() {//anonymous
		super();
	}
	load(
		url: string,
		onLoad: (gltf) => void,
		onError: (err: any) => void
	) {
		var scope = this;

		let callback = (s: boolean, res: any) => {
			if (s) {//得到二进制数据
				try {
					scope.parse(res, function (gltf) {
						onLoad(gltf);
					}, onError);
				} catch (e) {
					onError(e);
				}
			} else {
				onError && onError(res)
			}
		}
		if (getEnv() == "tb") {
			GlobalLoader.tbLoad(callback, url, "ArrayBuffer")
		} else {
			GlobalLoader.loadRawWeb(callback, url, "arraybuffer")
		}
	}

	parse(data: any, onLoad: (res: any) => void, onError: (err: any) => void) {
		var content: string;
		var extensions = {};
		if (typeof data === 'string') {
			content = data;
		} else {
			var magic = decodeText(new Uint8Array(data, 0, 4));
			if (magic === BINARY_EXTENSION_HEADER_MAGIC) {
				try {
					extensions[EXTENSIONS.KHR_BINARY_GLTF] = new GLTFBinaryExtension(data);
				} catch (error) {
					if (onError) onError(error);
					return;
				}
				content = extensions[EXTENSIONS.KHR_BINARY_GLTF].content;
			} else {
				content = decodeText(new Uint8Array(data));
			}
		}

		var json = JSON.parse(content);

		console.log(json)

		if (json.asset === undefined || json.asset.version[0] < 2) {
			onError && onError("版本格式有问题")
			return;
		}


		var parser //= new GLTFParser(json, extensions);

		parser.parse(function (scene, scenes, cameras, animations, json) {

			var glTF = {
				scene: scene,
				scenes: scenes,
				cameras: cameras,
				animations: animations,
				asset: json.asset,
				parser: parser,
				userData: {}
			};

			// addUnknownExtensionsToUserData(extensions, glTF, json);

			onLoad(glTF);

		}, onError);

	}
}

/**
 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#default-material
 */
function createDefaultMaterial() {
	// return new THREE.MeshStandardMaterial({
	// 	color: 0xFFFFFF,
	// 	emissive: 0x000000,
	// 	metalness: 1,
	// 	roughness: 1,
	// 	transparent: false,
	// 	depthTest: true,
	// 	side: THREE.FrontSide
	// });
	return new LightMaterial({
		color: 0xFFFFFF,
		side: 0
	})

}

/**
 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#morph-targets
 *
 * @param {THREE.BufferGeometry} geometry
 * @param {Array<GLTF.Target>} targets
 * @param {Array<THREE.BufferAttribute>} accessors
 */
function addMorphTargets(geometry, targets, accessors) {

	var hasMorphPosition = false;
	var hasMorphNormal = false;

	for (var i = 0, il = targets.length; i < il; i++) {

		var target = targets[i];

		if (target.POSITION !== undefined) hasMorphPosition = true;
		if (target.NORMAL !== undefined) hasMorphNormal = true;

		if (hasMorphPosition && hasMorphNormal) break;

	}

	if (!hasMorphPosition && !hasMorphNormal) return;

	var morphPositions = [];
	var morphNormals = [];

	for (var i = 0, il = targets.length; i < il; i++) {

		var target = targets[i];
		var attributeName = 'morphTarget' + i;

		if (hasMorphPosition) {

			// Three.js morph position is absolute value. The formula is
			//   basePosition
			//     + weight0 * ( morphPosition0 - basePosition )
			//     + weight1 * ( morphPosition1 - basePosition )
			//     ...
			// while the glTF one is relative
			//   basePosition
			//     + weight0 * glTFmorphPosition0
			//     + weight1 * glTFmorphPosition1
			//     ...
			// then we need to convert from relative to absolute here.

			if (target.POSITION !== undefined) {

				// Cloning not to pollute original accessor
				var positionAttribute = cloneBufferAttribute(accessors[target.POSITION]);
				positionAttribute.name = attributeName;

				var position = geometry.attributes.position;

				for (var j = 0, jl = positionAttribute.count; j < jl; j++) {

					positionAttribute.setXYZ(
						j,
						positionAttribute.getX(j) + position.getX(j),
						positionAttribute.getY(j) + position.getY(j),
						positionAttribute.getZ(j) + position.getZ(j)
					);

				}

			} else {

				positionAttribute = geometry.attributes.position;

			}

			morphPositions.push(positionAttribute);

		}

		if (hasMorphNormal) {

			// see target.POSITION's comment

			var normalAttribute;

			if (target.NORMAL !== undefined) {

				var normalAttribute = cloneBufferAttribute(accessors[target.NORMAL]);
				normalAttribute.name = attributeName;

				var normal = geometry.attributes.normal;

				for (var j = 0, jl = normalAttribute.count; j < jl; j++) {

					normalAttribute.setXYZ(
						j,
						normalAttribute.getX(j) + normal.getX(j),
						normalAttribute.getY(j) + normal.getY(j),
						normalAttribute.getZ(j) + normal.getZ(j)
					);

				}

			} else {

				normalAttribute = geometry.attributes.normal;

			}

			morphNormals.push(normalAttribute);

		}

	}

	if (hasMorphPosition) geometry.morphAttributes.position = morphPositions;
	if (hasMorphNormal) geometry.morphAttributes.normal = morphNormals;

}


function cloneBufferAttribute(attribute) {

	if (attribute.isInterleavedBufferAttribute) {

		var count = attribute.count;
		var itemSize = attribute.itemSize;
		var array = attribute.array.slice(0, count * itemSize);

		for (var i = 0; i < count; ++i) {

			array[i] = attribute.getX(i);
			if (itemSize >= 2) array[i + 1] = attribute.getY(i);
			if (itemSize >= 3) array[i + 2] = attribute.getZ(i);
			if (itemSize >= 4) array[i + 3] = attribute.getW(i);

		}

		return new THREE.BufferAttribute(array, itemSize, attribute.normalized);

	}

	return attribute.clone();

}

THREE.GLTFLoader = (function () {








	/**
	 * @param {THREE.Mesh} mesh
	 * @param {GLTF.Mesh} meshDef
	 */
	function updateMorphTargets(mesh, meshDef) {

		mesh.updateMorphTargets();

		if (meshDef.weights !== undefined) {

			for (var i = 0, il = meshDef.weights.length; i < il; i++) {

				mesh.morphTargetInfluences[i] = meshDef.weights[i];

			}

		}

		// .extras has user-defined data, so check that .extras.targetNames is an array.
		if (meshDef.extras && Array.isArray(meshDef.extras.targetNames)) {

			var targetNames = meshDef.extras.targetNames;

			if (mesh.morphTargetInfluences.length === targetNames.length) {

				mesh.morphTargetDictionary = {};

				for (var i = 0, il = targetNames.length; i < il; i++) {

					mesh.morphTargetDictionary[targetNames[i]] = i;

				}

			} else {

				console.warn('THREE.GLTFLoader: Invalid extras.targetNames length. Ignoring names.');

			}

		}

	}

	function isPrimitiveEqual(a, b) {

		if (a.indices !== b.indices) {

			return false;

		}

		return isObjectEqual(a.attributes, b.attributes);

	}

	function isObjectEqual(a, b) {

		if (Object.keys(a).length !== Object.keys(b).length) return false;

		for (var key in a) {

			if (a[key] !== b[key]) return false;

		}

		return true;

	}

	function isArrayEqual(a, b) {

		if (a.length !== b.length) return false;

		for (var i = 0, il = a.length; i < il; i++) {

			if (a[i] !== b[i]) return false;

		}

		return true;

	}

	function getCachedGeometry(cache, newPrimitive) {

		for (var i = 0, il = cache.length; i < il; i++) {

			var cached = cache[i];

			if (isPrimitiveEqual(cached.primitive, newPrimitive)) return cached.promise;

		}

		return null;

	}

	function getCachedCombinedGeometry(cache, geometries) {

		for (var i = 0, il = cache.length; i < il; i++) {

			var cached = cache[i];

			if (isArrayEqual(geometries, cached.baseGeometries)) return cached.geometry;

		}

		return null;

	}

	function getCachedMultiPassGeometry(cache, geometry, primitives) {

		for (var i = 0, il = cache.length; i < il; i++) {

			var cached = cache[i];

			if (geometry === cached.baseGeometry && isArrayEqual(primitives, cached.primitives)) return cached.geometry;

		}

		return null;

	}


	/**
	 * Checks if we can build a single Mesh with MultiMaterial from multiple primitives.
	 * Returns true if all primitives use the same attributes/morphAttributes/mode
	 * and also have index. Otherwise returns false.
	 *
	 * @param {Array<GLTF.Primitive>} primitives
	 * @return {Boolean}
	 */
	function isMultiPassGeometry(primitives) {

		if (primitives.length < 2) return false;

		var primitive0 = primitives[0];
		var targets0 = primitive0.targets || [];

		if (primitive0.indices === undefined) return false;

		for (var i = 1, il = primitives.length; i < il; i++) {

			var primitive = primitives[i];

			if (primitive0.mode !== primitive.mode) return false;
			if (primitive.indices === undefined) return false;
			if (!isObjectEqual(primitive0.attributes, primitive.attributes)) return false;

			var targets = primitive.targets || [];

			if (targets0.length !== targets.length) return false;

			for (var j = 0, jl = targets0.length; j < jl; j++) {

				if (!isObjectEqual(targets0[j], targets[j])) return false;

			}

		}

		return true;

	}

	/* GLTF PARSER */

	function GLTFParser(json, extensions, options) {

		this.json = json || {};
		this.extensions = extensions || {};
		this.options = options || {};

		// loader object cache
		this.cache = new GLTFRegistry();

		// BufferGeometry caching
		this.primitiveCache = [];
		this.multiplePrimitivesCache = [];
		this.multiPassGeometryCache = [];

		this.textureLoader = new THREE.TextureLoader(this.options.manager);
		this.textureLoader.setCrossOrigin(this.options.crossOrigin);

		this.fileLoader = new THREE.FileLoader(this.options.manager);
		this.fileLoader.setResponseType('arraybuffer');

	}

	GLTFParser.prototype.parse = function (onLoad, onError) {

		var json = this.json;

		// Clear the loader cache
		this.cache.removeAll();

		// Mark the special nodes/meshes in json for efficient parse
		this.markDefs();

		// Fire the callback on complete
		this.getMultiDependencies([

			'scene',
			'animation',
			'camera'

		]).then(function (dependencies) {

			var scenes = dependencies.scenes || [];
			var scene = scenes[json.scene || 0];
			var animations = dependencies.animations || [];
			var cameras = dependencies.cameras || [];

			onLoad(scene, scenes, cameras, animations, json);

		}).catch(onError);

	};

	/**
	 * Marks the special nodes/meshes in json for efficient parse.
	 */
	GLTFParser.prototype.markDefs = function () {

		var nodeDefs = this.json.nodes || [];
		var skinDefs = this.json.skins || [];
		var meshDefs = this.json.meshes || [];

		var meshReferences = {};
		var meshUses = {};

		// Nothing in the node definition indicates whether it is a Bone or an
		// Object3D. Use the skins' joint references to mark bones.
		for (var skinIndex = 0, skinLength = skinDefs.length; skinIndex < skinLength; skinIndex++) {

			var joints = skinDefs[skinIndex].joints;

			for (var i = 0, il = joints.length; i < il; i++) {

				nodeDefs[joints[i]].isBone = true;

			}

		}

		// Meshes can (and should) be reused by multiple nodes in a glTF asset. To
		// avoid having more than one THREE.Mesh with the same name, count
		// references and rename instances below.
		//
		// Example: CesiumMilkTruck sample model reuses "Wheel" meshes.
		for (var nodeIndex = 0, nodeLength = nodeDefs.length; nodeIndex < nodeLength; nodeIndex++) {

			var nodeDef = nodeDefs[nodeIndex];

			if (nodeDef.mesh !== undefined) {

				if (meshReferences[nodeDef.mesh] === undefined) {

					meshReferences[nodeDef.mesh] = meshUses[nodeDef.mesh] = 0;

				}

				meshReferences[nodeDef.mesh]++;

				// Nothing in the mesh definition indicates whether it is
				// a SkinnedMesh or Mesh. Use the node's mesh reference
				// to mark SkinnedMesh if node has skin.
				if (nodeDef.skin !== undefined) {

					meshDefs[nodeDef.mesh].isSkinnedMesh = true;

				}

			}

		}

		this.json.meshReferences = meshReferences;
		this.json.meshUses = meshUses;

	};

	/**
	 * Requests the specified dependency asynchronously, with caching.
	 * @param {string} type
	 * @param {number} index
	 * @return {Promise<Object>}
	 */
	GLTFParser.prototype.getDependency = function (type, index) {

		var cacheKey = type + ':' + index;
		var dependency = this.cache.get(cacheKey);

		if (!dependency) {

			switch (type) {

				case 'scene':
					dependency = this.loadScene(index);
					break;

				case 'node':
					dependency = this.loadNode(index);
					break;

				case 'mesh':
					dependency = this.loadMesh(index);
					break;

				case 'accessor':
					dependency = this.loadAccessor(index);
					break;

				case 'bufferView':
					dependency = this.loadBufferView(index);
					break;

				case 'buffer':
					dependency = this.loadBuffer(index);
					break;

				case 'material':
					dependency = this.loadMaterial(index);
					break;

				case 'texture':
					dependency = this.loadTexture(index);
					break;

				case 'skin':
					dependency = this.loadSkin(index);
					break;

				case 'animation':
					dependency = this.loadAnimation(index);
					break;

				case 'camera':
					dependency = this.loadCamera(index);
					break;

				default:
					throw new Error('Unknown type: ' + type);

			}

			this.cache.add(cacheKey, dependency);

		}

		return dependency;

	};

	/**
	 * Requests all dependencies of the specified type asynchronously, with caching.
	 * @param {string} type
	 * @return {Promise<Array<Object>>}
	 */
	GLTFParser.prototype.getDependencies = function (type) {

		var dependencies = this.cache.get(type);

		if (!dependencies) {

			var parser = this;
			var defs = this.json[type + (type === 'mesh' ? 'es' : 's')] || [];

			dependencies = Promise.all(defs.map(function (def, index) {

				return parser.getDependency(type, index);

			}));

			this.cache.add(type, dependencies);

		}

		return dependencies;

	};

	/**
	 * Requests all multiple dependencies of the specified types asynchronously, with caching.
	 * @param {Array<string>} types
	 * @return {Promise<Object<Array<Object>>>}
	 */
	GLTFParser.prototype.getMultiDependencies = function (types) {

		var results = {};
		var pendings = [];

		for (var i = 0, il = types.length; i < il; i++) {

			var type = types[i];
			var value = this.getDependencies(type);

			value = value.then(function (key, value) {

				results[key] = value;

			}.bind(this, type + (type === 'mesh' ? 'es' : 's')));

			pendings.push(value);

		}

		return Promise.all(pendings).then(function () {

			return results;

		});

	};

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
	 * @param {number} bufferIndex
	 * @return {Promise<ArrayBuffer>}
	 */
	GLTFParser.prototype.loadBuffer = function (bufferIndex) {

		var bufferDef = this.json.buffers[bufferIndex];
		var loader = this.fileLoader;

		if (bufferDef.type && bufferDef.type !== 'arraybuffer') {

			throw new Error('THREE.GLTFLoader: ' + bufferDef.type + ' buffer type is not supported.');

		}

		// If present, GLB container is required to be the first buffer.
		if (bufferDef.uri === undefined && bufferIndex === 0) {

			return Promise.resolve(this.extensions[EXTENSIONS.KHR_BINARY_GLTF].body);

		}

		var options = this.options;

		return new Promise(function (resolve, reject) {

			loader.load(resolveURL(bufferDef.uri, options.path), resolve, undefined, function () {

				reject(new Error('THREE.GLTFLoader: Failed to load buffer "' + bufferDef.uri + '".'));

			});

		});

	};

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#buffers-and-buffer-views
	 * @param {number} bufferViewIndex
	 * @return {Promise<ArrayBuffer>}
	 */
	GLTFParser.prototype.loadBufferView = function (bufferViewIndex) {

		var bufferViewDef = this.json.bufferViews[bufferViewIndex];

		return this.getDependency('buffer', bufferViewDef.buffer).then(function (buffer) {

			var byteLength = bufferViewDef.byteLength || 0;
			var byteOffset = bufferViewDef.byteOffset || 0;
			return buffer.slice(byteOffset, byteOffset + byteLength);

		});

	};

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#accessors
	 * @param {number} accessorIndex
	 * @return {Promise<THREE.BufferAttribute|THREE.InterleavedBufferAttribute>}
	 */
	GLTFParser.prototype.loadAccessor = function (accessorIndex) {

		var parser = this;
		var json = this.json;

		var accessorDef = this.json.accessors[accessorIndex];

		if (accessorDef.bufferView === undefined && accessorDef.sparse === undefined) {

			// Ignore empty accessors, which may be used to declare runtime
			// information about attributes coming from another source (e.g. Draco
			// compression extension).
			return null;

		}

		var pendingBufferViews = [];

		if (accessorDef.bufferView !== undefined) {

			pendingBufferViews.push(this.getDependency('bufferView', accessorDef.bufferView));

		} else {

			pendingBufferViews.push(null);

		}

		if (accessorDef.sparse !== undefined) {

			pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.indices.bufferView));
			pendingBufferViews.push(this.getDependency('bufferView', accessorDef.sparse.values.bufferView));

		}

		return Promise.all(pendingBufferViews).then(function (bufferViews) {

			var bufferView = bufferViews[0];

			var itemSize = WEBGL_TYPE_SIZES[accessorDef.type];
			var TypedArray = WEBGL_COMPONENT_TYPES[accessorDef.componentType];

			// For VEC3: itemSize is 3, elementBytes is 4, itemBytes is 12.
			var elementBytes = TypedArray.BYTES_PER_ELEMENT;
			var itemBytes = elementBytes * itemSize;
			var byteOffset = accessorDef.byteOffset || 0;
			var byteStride = accessorDef.bufferView !== undefined ? json.bufferViews[accessorDef.bufferView].byteStride : undefined;
			var normalized = accessorDef.normalized === true;
			var array, bufferAttribute;

			// The buffer is not interleaved if the stride is the item size in bytes.
			if (byteStride && byteStride !== itemBytes) {

				var ibCacheKey = 'InterleavedBuffer:' + accessorDef.bufferView + ':' + accessorDef.componentType;
				var ib = parser.cache.get(ibCacheKey);

				if (!ib) {

					// Use the full buffer if it's interleaved.
					array = new TypedArray(bufferView);

					// Integer parameters to IB/IBA are in array elements, not bytes.
					ib = new THREE.InterleavedBuffer(array, byteStride / elementBytes);

					parser.cache.add(ibCacheKey, ib);

				}

				bufferAttribute = new THREE.InterleavedBufferAttribute(ib, itemSize, byteOffset / elementBytes, normalized);

			} else {

				if (bufferView === null) {

					array = new TypedArray(accessorDef.count * itemSize);

				} else {

					array = new TypedArray(bufferView, byteOffset, accessorDef.count * itemSize);

				}

				bufferAttribute = new THREE.BufferAttribute(array, itemSize, normalized);

			}

			// https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#sparse-accessors
			if (accessorDef.sparse !== undefined) {

				var itemSizeIndices = WEBGL_TYPE_SIZES.SCALAR;
				var TypedArrayIndices = WEBGL_COMPONENT_TYPES[accessorDef.sparse.indices.componentType];

				var byteOffsetIndices = accessorDef.sparse.indices.byteOffset || 0;
				var byteOffsetValues = accessorDef.sparse.values.byteOffset || 0;

				var sparseIndices = new TypedArrayIndices(bufferViews[1], byteOffsetIndices, accessorDef.sparse.count * itemSizeIndices);
				var sparseValues = new TypedArray(bufferViews[2], byteOffsetValues, accessorDef.sparse.count * itemSize);

				if (bufferView !== null) {

					// Avoid modifying the original ArrayBuffer, if the bufferView wasn't initialized with zeroes.
					bufferAttribute.setArray(bufferAttribute.array.slice());

				}

				for (var i = 0, il = sparseIndices.length; i < il; i++) {

					var index = sparseIndices[i];

					bufferAttribute.setX(index, sparseValues[i * itemSize]);
					if (itemSize >= 2) bufferAttribute.setY(index, sparseValues[i * itemSize + 1]);
					if (itemSize >= 3) bufferAttribute.setZ(index, sparseValues[i * itemSize + 2]);
					if (itemSize >= 4) bufferAttribute.setW(index, sparseValues[i * itemSize + 3]);
					if (itemSize >= 5) throw new Error('THREE.GLTFLoader: Unsupported itemSize in sparse BufferAttribute.');

				}

			}

			return bufferAttribute;

		});

	};

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#textures
	 * @param {number} textureIndex
	 * @return {Promise<THREE.Texture>}
	 */
	GLTFParser.prototype.loadTexture = function (textureIndex) {

		var parser = this;
		var json = this.json;
		var options = this.options;
		var textureLoader = this.textureLoader;

		var URL = window.URL// || window.webkitURL;

		var textureDef = json.textures[textureIndex];

		var textureExtensions = textureDef.extensions || {};

		var source;

		if (textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS]) {

			source = json.images[textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS].source];

		} else {

			source = json.images[textureDef.source];

		}

		var sourceURI = source.uri;
		var isObjectURL = false;

		if (source.bufferView !== undefined) {

			// Load binary image data from bufferView, if provided.

			sourceURI = parser.getDependency('bufferView', source.bufferView).then(function (bufferView) {

				// if (window.arrayBufferToBase64 != undefined) {
				var base64 = ArrayBufferToBase64(bufferView);
				var base64String = `data:${source.mimeType};base64,${base64}`;
				return base64String;
				// }

				// isObjectURL = true;
				// var blob = new Blob([bufferView], { type: source.mimeType });
				// sourceURI = URL.createObjectURL(blob);
				// return sourceURI;

			});

		}

		return Promise.resolve(sourceURI).then(function (sourceURI) {

			// Load Texture resource.

			var loader = THREE.Loader.Handlers.get(sourceURI);

			if (!loader) {

				loader = textureExtensions[EXTENSIONS.MSFT_TEXTURE_DDS]
					? parser.extensions[EXTENSIONS.MSFT_TEXTURE_DDS].ddsLoader
					: textureLoader;

			}

			return new Promise(function (resolve, reject) {

				loader.load(resolveURL(sourceURI, options.path), resolve, undefined, reject);

			});

		}).then(function (texture: any) {

			// Clean up resources and configure Texture.

			if (isObjectURL === true) {

				URL.revokeObjectURL(sourceURI);

			}

			texture.flipY = false;

			if (textureDef.name !== undefined) texture.name = textureDef.name;

			// Ignore unknown mime types, like DDS files.
			// if (source.mimeType in MIME_TYPE_FORMATS) {

			// 	texture.format = MIME_TYPE_FORMATS[source.mimeType];

			// }

			var samplers = json.samplers || {};
			var sampler = samplers[textureDef.sampler] || {};

			// texture.magFilter = WEBGL_FILTERS[sampler.magFilter] || THREE.LinearFilter;
			// texture.minFilter = WEBGL_FILTERS[sampler.minFilter] || THREE.LinearMipMapLinearFilter;
			// texture.wrapS = WEBGL_WRAPPINGS[sampler.wrapS] || THREE.RepeatWrapping;
			// texture.wrapT = WEBGL_WRAPPINGS[sampler.wrapT] || THREE.RepeatWrapping;

			return texture;

		});

	};

	/**
	 * Asynchronously assigns a texture to the given material parameters.
	 * @param {Object} materialParams
	 * @param {string} textureName
	 * @param {number} textureIndex
	 * @return {Promise}
	 */
	GLTFParser.prototype.assignTexture = function (materialParams, textureName, textureIndex) {

		return this.getDependency('texture', textureIndex).then(function (texture) {

			materialParams[textureName] = texture;

		});

	};

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#materials
	 * @param {number} materialIndex
	 * @return {Promise<THREE.Material>}
	 */
	GLTFParser.prototype.loadMaterial = function (materialIndex) {

		var parser = this;
		var json = this.json;
		var extensions = this.extensions;
		var materialDef = json.materials[materialIndex];

		var materialType;
		var materialParams: any = {};
		var materialExtensions = materialDef.extensions || {};

		var pending = [];

		if (materialExtensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS]) {

			var sgExtension = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS];
			materialType = sgExtension.getMaterialType(materialDef);
			pending.push(sgExtension.extendParams(materialParams, materialDef, parser));

		} else if (materialExtensions[EXTENSIONS.KHR_MATERIALS_UNLIT]) {

			var kmuExtension = extensions[EXTENSIONS.KHR_MATERIALS_UNLIT];
			materialType = kmuExtension.getMaterialType(materialDef);
			pending.push(kmuExtension.extendParams(materialParams, materialDef, parser));

		} else {

			// Specification:
			// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#metallic-roughness-material

			materialType = THREE.MeshStandardMaterial;

			var metallicRoughness = materialDef.pbrMetallicRoughness || {};

			materialParams.color = new THREE.Color(1.0, 1.0, 1.0);
			materialParams.opacity = 1.0;

			if (Array.isArray(metallicRoughness.baseColorFactor)) {

				var array = metallicRoughness.baseColorFactor;

				materialParams.color.fromArray(array);
				materialParams.opacity = array[3];

			}

			if (metallicRoughness.baseColorTexture !== undefined) {

				pending.push(parser.assignTexture(materialParams, 'map', metallicRoughness.baseColorTexture.index));

			}

			materialParams.metalness = metallicRoughness.metallicFactor !== undefined ? metallicRoughness.metallicFactor : 1.0;
			materialParams.roughness = metallicRoughness.roughnessFactor !== undefined ? metallicRoughness.roughnessFactor : 1.0;

			if (metallicRoughness.metallicRoughnessTexture !== undefined) {

				var textureIndex = metallicRoughness.metallicRoughnessTexture.index;
				pending.push(parser.assignTexture(materialParams, 'metalnessMap', textureIndex));
				pending.push(parser.assignTexture(materialParams, 'roughnessMap', textureIndex));

			}

		}

		if (materialDef.doubleSided === true) {

			materialParams.side = THREE.DoubleSide;

		}

		// var alphaMode = materialDef.alphaMode || ALPHA_MODES.OPAQUE;

		// if (alphaMode === ALPHA_MODES.BLEND) {

		// 	materialParams.transparent = true;

		// } else {

		// 	materialParams.transparent = false;

		// 	if (alphaMode === ALPHA_MODES.MASK) {

		// 		materialParams.alphaTest = materialDef.alphaCutoff !== undefined ? materialDef.alphaCutoff : 0.5;

		// 	}

		// }

		if (materialDef.normalTexture !== undefined && materialType !== THREE.MeshBasicMaterial) {

			pending.push(parser.assignTexture(materialParams, 'normalMap', materialDef.normalTexture.index));

			materialParams.normalScale = new THREE.Vector2(1, 1);

			if (materialDef.normalTexture.scale !== undefined) {

				materialParams.normalScale.set(materialDef.normalTexture.scale, materialDef.normalTexture.scale);

			}

		}

		if (materialDef.occlusionTexture !== undefined && materialType !== THREE.MeshBasicMaterial) {

			pending.push(parser.assignTexture(materialParams, 'aoMap', materialDef.occlusionTexture.index));

			if (materialDef.occlusionTexture.strength !== undefined) {

				materialParams.aoMapIntensity = materialDef.occlusionTexture.strength;

			}

		}

		if (materialDef.emissiveFactor !== undefined && materialType !== THREE.MeshBasicMaterial) {

			materialParams.emissive = new THREE.Color().fromArray(materialDef.emissiveFactor);

		}

		if (materialDef.emissiveTexture !== undefined && materialType !== THREE.MeshBasicMaterial) {

			pending.push(parser.assignTexture(materialParams, 'emissiveMap', materialDef.emissiveTexture.index));

		}

		return Promise.all(pending).then(function () {

			var material;

			if (materialType === THREE.ShaderMaterial) {

				material = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS].createMaterial(materialParams);

			} else {

				material = new materialType(materialParams);

			}

			if (materialDef.name !== undefined) material.name = materialDef.name;

			// Normal map textures use OpenGL conventions:
			// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#materialnormaltexture
			if (material.normalScale) {

				material.normalScale.y = - material.normalScale.y;

			}

			// baseColorTexture, emissiveTexture, and specularGlossinessTexture use sRGB encoding.
			if (material.map) material.map.encoding = THREE.sRGBEncoding;
			if (material.emissiveMap) material.emissiveMap.encoding = THREE.sRGBEncoding;
			if (material.specularMap) material.specularMap.encoding = THREE.sRGBEncoding;

			// assignExtrasToUserData(material, materialDef);

			// if (materialDef.extensions) addUnknownExtensionsToUserData(extensions, material, materialDef);

			return material;

		});

	};

	/**
	 * @param  {THREE.BufferGeometry} geometry
	 * @param  {GLTF.Primitive} primitiveDef
	 * @param  {Array<THREE.BufferAttribute>} accessors
	 */
	function addPrimitiveAttributes(geometry, primitiveDef, accessors) {

		var attributes = primitiveDef.attributes;

		for (var gltfAttributeName in attributes) {

			var threeAttributeName = ATTRIBUTES[gltfAttributeName];
			var bufferAttribute = accessors[attributes[gltfAttributeName]];

			// Skip attributes already provided by e.g. Draco extension.
			if (!threeAttributeName) continue;
			if (threeAttributeName in geometry.attributes) continue;

			geometry.addAttribute(threeAttributeName, bufferAttribute);

		}

		if (primitiveDef.indices !== undefined && !geometry.index) {

			geometry.setIndex(accessors[primitiveDef.indices]);

		}

		if (primitiveDef.targets !== undefined) {

			addMorphTargets(geometry, primitiveDef.targets, accessors);

		}

		// assignExtrasToUserData(geometry, primitiveDef);

	}

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#geometry
	 *
	 * Creates BufferGeometries from primitives.
	 * If we can build a single BufferGeometry with .groups from multiple primitives, returns one BufferGeometry.
	 * Otherwise, returns BufferGeometries without .groups as many as primitives.
	 *
	 * @param {Array<Object>} primitives
	 * @return {Promise<Array<THREE.BufferGeometry>>}
	 */
	GLTFParser.prototype.loadGeometries = function (primitives) {

		var parser = this;
		var extensions = this.extensions;
		var cache = this.primitiveCache;

		var isMultiPass = isMultiPassGeometry(primitives);
		var originalPrimitives;

		if (isMultiPass) {

			originalPrimitives = primitives; // save original primitives and use later

			// We build a single BufferGeometry with .groups from multiple primitives
			// because all primitives share the same attributes/morph/mode and have indices.

			primitives = [primitives[0]];

			// Sets .groups and combined indices to a geometry later in this method.

		}

		return this.getDependencies('accessor').then(function (accessors) {

			var pending = [];

			for (var i = 0, il = primitives.length; i < il; i++) {

				var primitive = primitives[i];

				// See if we've already created this geometry
				var cached = getCachedGeometry(cache, primitive);

				if (cached) {

					// Use the cached geometry if it exists
					pending.push(cached);

				} else if (primitive.extensions && primitive.extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]) {

					// Use DRACO geometry if available
					var geometryPromise = extensions[EXTENSIONS.KHR_DRACO_MESH_COMPRESSION]
						.decodePrimitive(primitive, parser)
						.then(function (geometry) {

							addPrimitiveAttributes(geometry, primitive, accessors);

							return geometry;

						});

					cache.push({ primitive: primitive, promise: geometryPromise });

					pending.push(geometryPromise);

				} else {

					// Otherwise create a new geometry
					var geometry = new THREE.BufferGeometry();

					addPrimitiveAttributes(geometry, primitive, accessors);

					var geometryPromise: any = Promise.resolve(geometry);

					// Cache this geometry
					cache.push({ primitive: primitive, promise: geometryPromise });

					pending.push(geometryPromise);

				}

			}

			return Promise.all(pending).then(function (geometries) {

				if (isMultiPass) {

					var baseGeometry = geometries[0];

					// See if we've already created this combined geometry
					var cache = parser.multiPassGeometryCache;
					var cached = getCachedMultiPassGeometry(cache, baseGeometry, originalPrimitives);

					if (cached !== null) return [cached.geometry];

					// Cloning geometry because of index override.
					// Attributes can be reused so cloning by myself here.
					var geometry = new THREE.BufferGeometry();

					geometry.name = baseGeometry.name;
					geometry.userData = baseGeometry.userData;

					for (var key in baseGeometry.attributes) geometry.addAttribute(key, baseGeometry.attributes[key]);
					for (var key in baseGeometry.morphAttributes) geometry.morphAttributes[key] = baseGeometry.morphAttributes[key];

					var indices = [];
					var offset = 0;

					for (var i = 0, il = originalPrimitives.length; i < il; i++) {

						var accessor = accessors[originalPrimitives[i].indices];

						for (var j = 0, jl = accessor.count; j < jl; j++) indices.push(accessor.array[j]);

						geometry.addGroup(offset, accessor.count, i);

						offset += accessor.count;

					}

					geometry.setIndex(indices);

					cache.push({ geometry: geometry, baseGeometry: baseGeometry, primitives: originalPrimitives });

					return [geometry];

				} else if (geometries.length > 1 && THREE.BufferGeometryUtils !== undefined) {

					// Tries to merge geometries with BufferGeometryUtils if possible

					for (var i = 1, il = primitives.length; i < il; i++) {

						// can't merge if draw mode is different
						if (primitives[0].mode !== primitives[i].mode) return geometries;

					}

					// See if we've already created this combined geometry
					var cache = parser.multiplePrimitivesCache;
					var cached = getCachedCombinedGeometry(cache, geometries);

					if (cached) {

						if (cached.geometry !== null) return [cached.geometry];

					} else {

						var geometry = THREE.BufferGeometryUtils.mergeBufferGeometries(geometries, true);

						cache.push({ geometry: geometry, baseGeometries: geometries });

						if (geometry !== null) return [geometry];

					}

				}

				return geometries;

			});

		});

	};

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes
	 * @param {number} meshIndex
	 * @return {Promise<THREE.Group|THREE.Mesh|THREE.SkinnedMesh>}
	 */
	GLTFParser.prototype.loadMesh = function (meshIndex) {

		var scope = this;
		var json = this.json;
		var extensions = this.extensions;

		var meshDef = json.meshes[meshIndex];

		return this.getMultiDependencies([

			'accessor',
			'material'

		]).then(function (dependencies) {

			var primitives = meshDef.primitives;
			var originalMaterials = [];

			for (var i = 0, il = primitives.length; i < il; i++) {

				originalMaterials[i] = primitives[i].material === undefined
					? createDefaultMaterial()
					: dependencies.materials[primitives[i].material];

			}

			return scope.loadGeometries(primitives).then(function (geometries) {

				var isMultiMaterial = geometries.length === 1 && geometries[0].groups.length > 0;

				var meshes = [];

				for (var i = 0, il = geometries.length; i < il; i++) {

					var geometry = geometries[i];
					var primitive = primitives[i];

					// 1. create Mesh

					var mesh;

					var material = isMultiMaterial ? originalMaterials : originalMaterials[i];

					if (primitive.mode === WEBGL_CONSTANTS.TRIANGLES ||
						primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP ||
						primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN ||
						primitive.mode === undefined) {

						// .isSkinnedMesh isn't in glTF spec. See .markDefs()
						mesh = meshDef.isSkinnedMesh === true
							? new THREE.SkinnedMesh(geometry, material)
							: new THREE.Mesh(geometry, material);

						if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_STRIP) {

							mesh.drawMode = THREE.TriangleStripDrawMode;

						} else if (primitive.mode === WEBGL_CONSTANTS.TRIANGLE_FAN) {

							mesh.drawMode = THREE.TriangleFanDrawMode;

						}

					} else if (primitive.mode === WEBGL_CONSTANTS.LINES) {

						mesh = new THREE.LineSegments(geometry, material);

					} else if (primitive.mode === WEBGL_CONSTANTS.LINE_STRIP) {

						mesh = new THREE.Line(geometry, material);

					} else if (primitive.mode === WEBGL_CONSTANTS.LINE_LOOP) {

						mesh = new THREE.LineLoop(geometry, material);

					} else if (primitive.mode === WEBGL_CONSTANTS.POINTS) {

						mesh = new THREE.Points(geometry, material);

					} else {

						throw new Error('THREE.GLTFLoader: Primitive mode unsupported: ' + primitive.mode);

					}

					if (Object.keys(mesh.geometry.morphAttributes).length > 0) {

						updateMorphTargets(mesh, meshDef);

					}

					mesh.name = meshDef.name || ('mesh_' + meshIndex);

					if (geometries.length > 1) mesh.name += '_' + i;

					// assignExtrasToUserData(mesh, meshDef);

					meshes.push(mesh);

					// 2. update Material depending on Mesh and BufferGeometry

					var materials = isMultiMaterial ? mesh.material : [mesh.material];

					var useVertexColors = geometry.attributes.color !== undefined;
					var useFlatShading = geometry.attributes.normal === undefined;
					var useSkinning = mesh.isSkinnedMesh === true;
					var useMorphTargets = Object.keys(geometry.morphAttributes).length > 0;
					var useMorphNormals = useMorphTargets && geometry.morphAttributes.normal !== undefined;

					for (var j = 0, jl = materials.length; j < jl; j++) {

						var material = materials[j];

						if (mesh.isPoints) {

							var cacheKey = 'PointsMaterial:' + material.uuid;

							var pointsMaterial = scope.cache.get(cacheKey);

							if (!pointsMaterial) {

								pointsMaterial = new THREE.PointsMaterial();
								THREE.Material.prototype.copy.call(pointsMaterial, material);
								pointsMaterial.color.copy(material.color);
								pointsMaterial.map = material.map;
								pointsMaterial.lights = false; // PointsMaterial doesn't support lights yet

								scope.cache.add(cacheKey, pointsMaterial);

							}

							material = pointsMaterial;

						} else if (mesh.isLine) {

							var cacheKey = 'LineBasicMaterial:' + material.uuid;

							var lineMaterial = scope.cache.get(cacheKey);

							if (!lineMaterial) {

								lineMaterial = new THREE.LineBasicMaterial();
								THREE.Material.prototype.copy.call(lineMaterial, material);
								lineMaterial.color.copy(material.color);
								lineMaterial.lights = false; // LineBasicMaterial doesn't support lights yet

								scope.cache.add(cacheKey, lineMaterial);

							}

							material = lineMaterial;

						}

						// Clone the material if it will be modified
						if (useVertexColors || useFlatShading || useSkinning || useMorphTargets) {

							var cacheKey = 'ClonedMaterial:' + material.uuid + ':';

							if (material.isGLTFSpecularGlossinessMaterial) cacheKey += 'specular-glossiness:';
							if (useSkinning) cacheKey += 'skinning:';
							if (useVertexColors) cacheKey += 'vertex-colors:';
							if (useFlatShading) cacheKey += 'flat-shading:';
							if (useMorphTargets) cacheKey += 'morph-targets:';
							if (useMorphNormals) cacheKey += 'morph-normals:';

							var cachedMaterial = scope.cache.get(cacheKey);

							if (!cachedMaterial) {

								cachedMaterial = material.isGLTFSpecularGlossinessMaterial
									? extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS].cloneMaterial(material)
									: material.clone();

								if (useSkinning) cachedMaterial.skinning = true;
								if (useVertexColors) cachedMaterial.vertexColors = THREE.VertexColors;
								if (useFlatShading) cachedMaterial.flatShading = true;
								if (useMorphTargets) cachedMaterial.morphTargets = true;
								if (useMorphNormals) cachedMaterial.morphNormals = true;

								scope.cache.add(cacheKey, cachedMaterial);

							}

							material = cachedMaterial;

						}

						materials[j] = material;

						// workarounds for mesh and geometry

						if (material.aoMap && geometry.attributes.uv2 === undefined && geometry.attributes.uv !== undefined) {

							console.log('THREE.GLTFLoader: Duplicating UVs to support aoMap.');
							geometry.addAttribute('uv2', new THREE.BufferAttribute(geometry.attributes.uv.array, 2));

						}

						if (material.isGLTFSpecularGlossinessMaterial) {

							// for GLTFSpecularGlossinessMaterial(ShaderMaterial) uniforms runtime update
							mesh.onBeforeRender = extensions[EXTENSIONS.KHR_MATERIALS_PBR_SPECULAR_GLOSSINESS].refreshUniforms;

						}

					}

					mesh.material = isMultiMaterial ? materials : materials[0];

				}

				if (meshes.length === 1) {

					return meshes[0];

				}

				var group = new THREE.Group();

				for (var i = 0; i < meshes.length; i++) {

					group.add(meshes[i]);

				}

				return group;

			});

		});

	};

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#cameras
	 * @param {number} cameraIndex
	 * @return {Promise<THREE.Camera>}
	 */
	GLTFParser.prototype.loadCamera = function (cameraIndex) {

		var camera;
		var cameraDef = this.json.cameras[cameraIndex];
		var params = cameraDef[cameraDef.type];

		if (!params) {

			console.warn('THREE.GLTFLoader: Missing camera parameters.');
			return;

		}

		if (cameraDef.type === 'perspective') {

			camera = new THREE.PerspectiveCamera(THREE.Math.radToDeg(params.yfov), params.aspectRatio || 1, params.znear || 1, params.zfar || 2e6);

		} else if (cameraDef.type === 'orthographic') {

			camera = new THREE.OrthographicCamera(params.xmag / - 2, params.xmag / 2, params.ymag / 2, params.ymag / - 2, params.znear, params.zfar);

		}

		if (cameraDef.name !== undefined) camera.name = cameraDef.name;

		// assignExtrasToUserData(camera, cameraDef);

		return Promise.resolve(camera);

	};

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#skins
	 * @param {number} skinIndex
	 * @return {Promise<Object>}
	 */
	GLTFParser.prototype.loadSkin = function (skinIndex) {

		var skinDef = this.json.skins[skinIndex];

		var skinEntry: any = { joints: skinDef.joints };

		if (skinDef.inverseBindMatrices === undefined) {

			return Promise.resolve(skinEntry);

		}

		return this.getDependency('accessor', skinDef.inverseBindMatrices).then(function (accessor) {

			skinEntry.inverseBindMatrices = accessor;

			return skinEntry;

		});

	};

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#animations
	 * @param {number} animationIndex
	 * @return {Promise<THREE.AnimationClip>}
	 */
	GLTFParser.prototype.loadAnimation = function (animationIndex) {

		var json = this.json;

		var animationDef = json.animations[animationIndex];

		return this.getMultiDependencies([

			'accessor',
			'node'

		]).then(function (dependencies) {

			var tracks = [];

			for (var i = 0, il = animationDef.channels.length; i < il; i++) {

				var channel = animationDef.channels[i];
				var sampler = animationDef.samplers[channel.sampler];

				if (sampler) {

					var target = channel.target;
					var name = target.node !== undefined ? target.node : target.id; // NOTE: target.id is deprecated.
					var input = animationDef.parameters !== undefined ? animationDef.parameters[sampler.input] : sampler.input;
					var output = animationDef.parameters !== undefined ? animationDef.parameters[sampler.output] : sampler.output;

					var inputAccessor = dependencies.accessors[input];
					var outputAccessor = dependencies.accessors[output];

					var node = dependencies.nodes[name];

					if (node) {

						node.updateMatrix();
						node.matrixAutoUpdate = true;

						var TypedKeyframeTrack;

						switch (PATH_PROPERTIES[target.path]) {

							case PATH_PROPERTIES.weights:

								TypedKeyframeTrack = THREE.NumberKeyframeTrack;
								break;

							case PATH_PROPERTIES.rotation:

								TypedKeyframeTrack = THREE.QuaternionKeyframeTrack;
								break;

							case PATH_PROPERTIES.position:
							case PATH_PROPERTIES.scale:
							default:

								TypedKeyframeTrack = THREE.VectorKeyframeTrack;
								break;

						}

						var targetName = node.name ? node.name : node.uuid;

						var interpolation = sampler.interpolation !== undefined ? INTERPOLATION[sampler.interpolation] : THREE.InterpolateLinear;

						var targetNames = [];

						if (PATH_PROPERTIES[target.path] === PATH_PROPERTIES.weights) {

							// node can be THREE.Group here but
							// PATH_PROPERTIES.weights(morphTargetInfluences) should be
							// the property of a mesh object under group.

							node.traverse(function (object) {

								if (object.isMesh === true && object.morphTargetInfluences) {

									targetNames.push(object.name ? object.name : object.uuid);

								}

							});

						} else {

							targetNames.push(targetName);

						}

						// KeyframeTrack.optimize() will modify given 'times' and 'values'
						// buffers before creating a truncated copy to keep. Because buffers may
						// be reused by other tracks, make copies here.
						for (var j = 0, jl = targetNames.length; j < jl; j++) {

							var track = new TypedKeyframeTrack(
								targetNames[j] + '.' + PATH_PROPERTIES[target.path],
								THREE.AnimationUtils.arraySlice(inputAccessor.array, 0),
								THREE.AnimationUtils.arraySlice(outputAccessor.array, 0),
								interpolation
							);

							// Here is the trick to enable custom interpolation.
							// Overrides .createInterpolant in a factory method which creates custom interpolation.
							if (sampler.interpolation === 'CUBICSPLINE') {

								track.createInterpolant = function InterpolantFactoryMethodGLTFCubicSpline(result) {

									// A CUBICSPLINE keyframe in glTF has three output values for each input value,
									// representing inTangent, splineVertex, and outTangent. As a result, track.getValueSize()
									// must be divided by three to get the interpolant's sampleSize argument.

									return new GLTFCubicSplineInterpolant(this.times, this.values, this.getValueSize() / 3, result);

								};

								// Workaround, provide an alternate way to know if the interpolant type is cubis spline to track.
								// track.getInterpolation() doesn't return valid value for custom interpolant.
								track.createInterpolant.isInterpolantFactoryMethodGLTFCubicSpline = true;

							}

							tracks.push(track);

						}

					}

				}

			}

			var name = animationDef.name !== undefined ? animationDef.name : 'animation_' + animationIndex;

			return new THREE.AnimationClip(name, undefined, tracks);

		});

	};

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#nodes-and-hierarchy
	 * @param {number} nodeIndex
	 * @return {Promise<THREE.Object3D>}
	 */
	GLTFParser.prototype.loadNode = function (nodeIndex) {

		var json = this.json;
		var extensions = this.extensions;

		var meshReferences = json.meshReferences;
		var meshUses = json.meshUses;

		var nodeDef = json.nodes[nodeIndex];

		return this.getMultiDependencies([

			'mesh',
			'skin',
			'camera',
			'light'

		]).then(function (dependencies) {

			var node;

			// .isBone isn't in glTF spec. See .markDefs
			if (nodeDef.isBone === true) {

				node = new THREE.Bone();

			} else if (nodeDef.mesh !== undefined) {

				var mesh = dependencies.meshes[nodeDef.mesh];

				if (meshReferences[nodeDef.mesh] > 1) {

					var instanceNum = meshUses[nodeDef.mesh]++;

					node = mesh.clone();
					node.name += '_instance_' + instanceNum;

					// onBeforeRender copy for Specular-Glossiness
					node.onBeforeRender = mesh.onBeforeRender;

					for (var i = 0, il = node.children.length; i < il; i++) {

						node.children[i].name += '_instance_' + instanceNum;
						node.children[i].onBeforeRender = mesh.children[i].onBeforeRender;

					}

				} else {

					node = mesh;

				}

			} else if (nodeDef.camera !== undefined) {

				node = dependencies.cameras[nodeDef.camera];

			} else if (nodeDef.extensions
				&& nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL]
				&& nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].light !== undefined) {

				var lights = extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].lights;
				node = lights[nodeDef.extensions[EXTENSIONS.KHR_LIGHTS_PUNCTUAL].light];

			} else {

				node = new THREE.Object3D();

			}

			if (nodeDef.name !== undefined) {

				node.name = THREE.PropertyBinding.sanitizeNodeName(nodeDef.name);

			}

			// assignExtrasToUserData(node, nodeDef);

			// if (nodeDef.extensions) addUnknownExtensionsToUserData(extensions, node, nodeDef);

			if (nodeDef.matrix !== undefined) {

				var matrix = new THREE.Matrix4();
				matrix.fromArray(nodeDef.matrix);
				node.applyMatrix(matrix);

			} else {

				if (nodeDef.translation !== undefined) {

					node.position.fromArray(nodeDef.translation);

				}

				if (nodeDef.rotation !== undefined) {

					node.quaternion.fromArray(nodeDef.rotation);

				}

				if (nodeDef.scale !== undefined) {

					node.scale.fromArray(nodeDef.scale);

				}

			}

			return node;

		});

	};

	/**
	 * Specification: https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#scenes
	 * @param {number} sceneIndex
	 * @return {Promise<THREE.Scene>}
	 */
	GLTFParser.prototype.loadScene = function () {

		// scene node hierachy builder

		function buildNodeHierachy(nodeId, parentObject, json, allNodes, skins) {

			var node = allNodes[nodeId];
			var nodeDef = json.nodes[nodeId];

			// build skeleton here as well

			if (nodeDef.skin !== undefined) {

				var meshes = node.isGroup === true ? node.children : [node];

				for (var i = 0, il = meshes.length; i < il; i++) {

					var mesh = meshes[i];
					var skinEntry = skins[nodeDef.skin];

					var bones = [];
					var boneInverses = [];

					for (var j = 0, jl = skinEntry.joints.length; j < jl; j++) {

						var jointId = skinEntry.joints[j];
						var jointNode = allNodes[jointId];

						if (jointNode) {

							bones.push(jointNode);

							var mat = new THREE.Matrix4();

							if (skinEntry.inverseBindMatrices !== undefined) {

								mat.fromArray(skinEntry.inverseBindMatrices.array, j * 16);

							}

							boneInverses.push(mat);

						} else {

							console.warn('THREE.GLTFLoader: Joint "%s" could not be found.', jointId);

						}

					}

					mesh.bind(new THREE.Skeleton(bones, boneInverses), mesh.matrixWorld);

				}

			}

			// build node hierachy

			parentObject.add(node);

			if (nodeDef.children) {

				var children = nodeDef.children;

				for (var i = 0, il = children.length; i < il; i++) {

					var child = children[i];
					buildNodeHierachy(child, node, json, allNodes, skins);

				}

			}

		}

		return function loadScene(sceneIndex) {

			var json = this.json;
			var extensions = this.extensions;
			var sceneDef = this.json.scenes[sceneIndex];

			return this.getMultiDependencies([

				'node',
				'skin'

			]).then(function (dependencies) {

				var scene = new THREE.Scene();
				if (sceneDef.name !== undefined) scene.name = sceneDef.name;

				// assignExtrasToUserData(scene, sceneDef);

				// if (sceneDef.extensions) addUnknownExtensionsToUserData(extensions, scene, sceneDef);

				var nodeIds = sceneDef.nodes || [];

				for (var i = 0, il = nodeIds.length; i < il; i++) {

					buildNodeHierachy(nodeIds[i], scene, json, dependencies.nodes, dependencies.skins);

				}

				return scene;

			});

		};

	}();

	return GLTFLoader;

})();


function decodeText(array) {

	if (typeof TextDecoder !== 'undefined') {
		return new TextDecoder().decode(array);
	}

	// Avoid the String.fromCharCode.apply(null, array) shortcut, which
	// throws a "maximum call stack size exceeded" error for large arrays.

	var s = '';

	for (var i = 0, il = array.length; i < il; i++) {

		// Implicitly assumes little-endian.
		s += String.fromCharCode(array[i]);

	}

	// Merges multi-byte utf-8 characters.
	return decodeURIComponent(escape(s));

}

function GLTFBinaryExtension(data) {
	this.name = EXTENSIONS.KHR_BINARY_GLTF;
	this.content = null;
	this.body = null;
	var headerView = new DataView(data, 0, BINARY_EXTENSION_HEADER_LENGTH);
	this.header = {
		magic: decodeText(new Uint8Array(data.slice(0, 4))),
		version: headerView.getUint32(4, true),
		length: headerView.getUint32(8, true)
	};

	if (this.header.magic !== BINARY_EXTENSION_HEADER_MAGIC) {

		throw new Error('THREE.GLTFLoader: Unsupported glTF-Binary header.');

	} else if (this.header.version < 2.0) {

		throw new Error('THREE.GLTFLoader: Legacy binary file detected. Use LegacyGLTFLoader instead.');

	}

	var chunkView = new DataView(data, BINARY_EXTENSION_HEADER_LENGTH);
	var chunkIndex = 0;

	while (chunkIndex < chunkView.byteLength) {

		var chunkLength = chunkView.getUint32(chunkIndex, true);
		chunkIndex += 4;

		var chunkType = chunkView.getUint32(chunkIndex, true);
		chunkIndex += 4;

		if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.JSON) {

			var contentArray = new Uint8Array(data, BINARY_EXTENSION_HEADER_LENGTH + chunkIndex, chunkLength);
			this.content = decodeText(contentArray);

		} else if (chunkType === BINARY_EXTENSION_CHUNK_TYPES.BIN) {

			var byteOffset = BINARY_EXTENSION_HEADER_LENGTH + chunkIndex;
			this.body = data.slice(byteOffset, byteOffset + chunkLength);

		}

		// Clients must ignore chunks with unknown types.

		chunkIndex += chunkLength;

	}

	if (this.content === null) {
		throw new Error('THREE.GLTFLoader: JSON content not found.');
	}
}


/* UTILITY FUNCTIONS */

function resolveURL(url, path) {

	// Invalid URL
	if (typeof url !== 'string' || url === '') return '';

	// Absolute URL http://,https://,//
	if (/^(https?:)?\/\//i.test(url)) return url;

	// Data URI
	if (/^data:.*,.*$/i.test(url)) return url;

	// Blob URL
	if (/^blob:.*$/i.test(url)) return url;

	// Relative URL
	return path + url;

}

function ArrayBufferToBase64(buff) {
	var alph =
		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",
		enc = "",
		n,
		p,
		bits,
		d = new Uint8Array(buff);
	var len = buff.byteLength * 8;
	for (var offset = 0; offset < len; offset += 6) {
		n = (offset / 8) | 0;
		p = offset % 8;
		bits = ((d[n] || 0) << p) >> 2;
		if (p > 2) {
			bits |= (d[n + 1] || 0) >> (10 - p);
		}
		enc += alph.charAt(bits & 63);
	}
	enc += p == 4 ? "=" : p == 6 ? "==" : "";
	return enc;
}

class GLTFRegistry {
	private static objects = {}
	constructor() { }
	get(key) {
		return GLTFRegistry.objects[key];
	}
	add(key, object) {
		GLTFRegistry.objects[key] = object;
	}
	remove(key) {
		delete GLTFRegistry.objects[key];
	}
	removeAll() {
		GLTFRegistry.objects = {};
	}
}