/**
 * @author mrdoob / http://mrdoob.com/
 * @author Mugen87 / https://github.com/Mugen87
 */

module.exports = THREE.ColladaLoader = function ( manager ) {

  this.manager = ( manager !== undefined ) ? manager : THREE.DefaultLoadingManager;

};

THREE.ColladaLoader.prototype = {

  constructor: THREE.ColladaLoader,

  crossOrigin: 'anonymous',

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

    var scope = this;

    var path = ( scope.path === undefined ) ? THREE.LoaderUtils.extractUrlBase( url ) : scope.path;

    var loader = new THREE.FileLoader( scope.manager );
    loader.setPath( scope.path );
    loader.load( url, function ( text ) {

      onLoad( scope.parse( text, path ) );

    }, onProgress, onError );

  },

  setPath: function ( value ) {

    this.path = value;
    return this;

  },

  setResourcePath: function ( value ) {

    this.resourcePath = value;
    return this;

  },

  options: {

    set convertUpAxis( value ) {

      console.warn( 'THREE.ColladaLoader: options.convertUpAxis() has been removed. Up axis is converted automatically.' );

    }

  },

  setCrossOrigin: function ( value ) {

    this.crossOrigin = value;
    return this;

  },

  parse: function ( text, path ) {

    function getElementsByTagName( xml, name ) {

      // Non recursive xml.getElementsByTagName() ...

      var array = [];
      var childNodes = xml.childNodes;

      for ( var i = 0, l = childNodes.length; i < l; i ++ ) {

        var child = childNodes[ i ];

        if ( child.nodeName === name ) {

          array.push( child );

        }

      }

      return array;

    }

    function parseStrings( text ) {

      if ( text.length === 0 ) return [];

      var parts = text.trim().split( /\s+/ );
      var array = new Array( parts.length );

      for ( var i = 0, l = parts.length; i < l; i ++ ) {

        array[ i ] = parts[ i ];

      }

      return array;

    }

    function parseFloats( text ) {

      if ( text.length === 0 ) return [];

      var parts = text.trim().split( /\s+/ );
      var array = new Array( parts.length );

      for ( var i = 0, l = parts.length; i < l; i ++ ) {

        array[ i ] = parseFloat( parts[ i ] );

      }

      return array;

    }

    function parseInts( text ) {

      if ( text.length === 0 ) return [];

      var parts = text.trim().split( /\s+/ );
      var array = new Array( parts.length );

      for ( var i = 0, l = parts.length; i < l; i ++ ) {

        array[ i ] = parseInt( parts[ i ] );

      }

      return array;

    }

    function parseId( text ) {

      return text.substring( 1 );

    }

    function generateId() {

      return 'three_default_' + ( count ++ );

    }

    function isEmpty( object ) {

      return Object.keys( object ).length === 0;

    }

    // asset

    function parseAsset( xml ) {

      return {
        unit: parseAssetUnit( getElementsByTagName( xml, 'unit' )[ 0 ] ),
        upAxis: parseAssetUpAxis( getElementsByTagName( xml, 'up_axis' )[ 0 ] )
      };

    }

    function parseAssetUnit( xml ) {

      if ( ( xml !== undefined ) && ( xml.hasAttribute( 'meter' ) === true ) ) {

        return parseFloat( xml.getAttribute( 'meter' ) );

      } else {

        return 1; // default 1 meter

      }

    }

    function parseAssetUpAxis( xml ) {

      return xml !== undefined ? xml.textContent : 'Y_UP';

    }

    // library

    function parseLibrary( xml, libraryName, nodeName, parser ) {

      var library = getElementsByTagName( xml, libraryName )[ 0 ];

      if ( library !== undefined ) {

        var elements = getElementsByTagName( library, nodeName );

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

          parser( elements[ i ] );

        }

      }

    }

    function buildLibrary( data, builder ) {

      for ( var name in data ) {

        var object = data[ name ];
        object.build = builder( data[ name ] );

      }

    }

    // get

    function getBuild( data, builder ) {

      if ( data.build !== undefined ) return data.build;

      data.build = builder( data );

      return data.build;

    }

    // animation

    function parseAnimation( xml ) {

      var data = {
        sources: {},
        samplers: {},
        channels: {}
      };

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        var id;

        switch ( child.nodeName ) {

          case 'source':
            id = child.getAttribute( 'id' );
            data.sources[ id ] = parseSource( child );
            break;

          case 'sampler':
            id = child.getAttribute( 'id' );
            data.samplers[ id ] = parseAnimationSampler( child );
            break;

          case 'channel':
            id = child.getAttribute( 'target' );
            data.channels[ id ] = parseAnimationChannel( child );
            break;

          default:
            console.log( child );

        }

      }

      library.animations[ xml.getAttribute( 'id' ) ] = data;

    }

    function parseAnimationSampler( xml ) {

      var data = {
        inputs: {},
      };

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'input':
            var id = parseId( child.getAttribute( 'source' ) );
            var semantic = child.getAttribute( 'semantic' );
            data.inputs[ semantic ] = id;
            break;

        }

      }

      return data;

    }

    function parseAnimationChannel( xml ) {

      var data = {};

      var target = xml.getAttribute( 'target' );

      // parsing SID Addressing Syntax

      var parts = target.split( '/' );

      var id = parts.shift();
      var sid = parts.shift();

      // check selection syntax

      var arraySyntax = ( sid.indexOf( '(' ) !== - 1 );
      var memberSyntax = ( sid.indexOf( '.' ) !== - 1 );

      if ( memberSyntax ) {

        //  member selection access

        parts = sid.split( '.' );
        sid = parts.shift();
        data.member = parts.shift();

      } else if ( arraySyntax ) {

        // array-access syntax. can be used to express fields in one-dimensional vectors or two-dimensional matrices.

        var indices = sid.split( '(' );
        sid = indices.shift();

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

          indices[ i ] = parseInt( indices[ i ].replace( /\)/, '' ) );

        }

        data.indices = indices;

      }

      data.id = id;
      data.sid = sid;

      data.arraySyntax = arraySyntax;
      data.memberSyntax = memberSyntax;

      data.sampler = parseId( xml.getAttribute( 'source' ) );

      return data;

    }

    function buildAnimation( data ) {

      var tracks = [];

      var channels = data.channels;
      var samplers = data.samplers;
      var sources = data.sources;

      for ( var target in channels ) {

        if ( channels.hasOwnProperty( target ) ) {

          var channel = channels[ target ];
          var sampler = samplers[ channel.sampler ];

          var inputId = sampler.inputs.INPUT;
          var outputId = sampler.inputs.OUTPUT;

          var inputSource = sources[ inputId ];
          var outputSource = sources[ outputId ];

          var animation = buildAnimationChannel( channel, inputSource, outputSource );

          createKeyframeTracks( animation, tracks );

        }

      }

      return tracks;

    }

    function getAnimation( id ) {

      return getBuild( library.animations[ id ], buildAnimation );

    }

    function buildAnimationChannel( channel, inputSource, outputSource ) {

      var node = library.nodes[ channel.id ];
      var object3D = getNode( node.id );

      var transform = node.transforms[ channel.sid ];
      var defaultMatrix = node.matrix.clone().transpose();

      var time, stride;
      var i, il, j, jl;

      var data = {};

      // the collada spec allows the animation of data in various ways.
      // depending on the transform type (matrix, translate, rotate, scale), we execute different logic

      switch ( transform ) {

        case 'matrix':

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

            time = inputSource.array[ i ];
            stride = i * outputSource.stride;

            if ( data[ time ] === undefined ) data[ time ] = {};

            if ( channel.arraySyntax === true ) {

              var value = outputSource.array[ stride ];
              var index = channel.indices[ 0 ] + 4 * channel.indices[ 1 ];

              data[ time ][ index ] = value;

            } else {

              for ( j = 0, jl = outputSource.stride; j < jl; j ++ ) {

                data[ time ][ j ] = outputSource.array[ stride + j ];

              }

            }

          }

          break;

        case 'translate':
          console.warn( 'THREE.ColladaLoader: Animation transform type "%s" not yet implemented.', transform );
          break;

        case 'rotate':
          console.warn( 'THREE.ColladaLoader: Animation transform type "%s" not yet implemented.', transform );
          break;

        case 'scale':
          console.warn( 'THREE.ColladaLoader: Animation transform type "%s" not yet implemented.', transform );
          break;

      }

      var keyframes = prepareAnimationData( data, defaultMatrix );

      var animation = {
        name: object3D.uuid,
        keyframes: keyframes
      };

      return animation;

    }

    function prepareAnimationData( data, defaultMatrix ) {

      var keyframes = [];

      // transfer data into a sortable array

      for ( var time in data ) {

        keyframes.push( { time: parseFloat( time ), value: data[ time ] } );

      }

      // ensure keyframes are sorted by time

      keyframes.sort( ascending );

      // now we clean up all animation data, so we can use them for keyframe tracks

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

        transformAnimationData( keyframes, i, defaultMatrix.elements[ i ] );

      }

      return keyframes;

      // array sort function

      function ascending( a, b ) {

        return a.time - b.time;

      }

    }

    var position = new THREE.Vector3();
    var scale = new THREE.Vector3();
    var quaternion = new THREE.Quaternion();

    function createKeyframeTracks( animation, tracks ) {

      var keyframes = animation.keyframes;
      var name = animation.name;

      var times = [];
      var positionData = [];
      var quaternionData = [];
      var scaleData = [];

      for ( var i = 0, l = keyframes.length; i < l; i ++ ) {

        var keyframe = keyframes[ i ];

        var time = keyframe.time;
        var value = keyframe.value;

        matrix.fromArray( value ).transpose();
        matrix.decompose( position, quaternion, scale );

        times.push( time );
        positionData.push( position.x, position.y, position.z );
        quaternionData.push( quaternion.x, quaternion.y, quaternion.z, quaternion.w );
        scaleData.push( scale.x, scale.y, scale.z );

      }

      if ( positionData.length > 0 ) tracks.push( new THREE.VectorKeyframeTrack( name + '.position', times, positionData ) );
      if ( quaternionData.length > 0 ) tracks.push( new THREE.QuaternionKeyframeTrack( name + '.quaternion', times, quaternionData ) );
      if ( scaleData.length > 0 ) tracks.push( new THREE.VectorKeyframeTrack( name + '.scale', times, scaleData ) );

      return tracks;

    }

    function transformAnimationData( keyframes, property, defaultValue ) {

      var keyframe;

      var empty = true;
      var i, l;

      // check, if values of a property are missing in our keyframes

      for ( i = 0, l = keyframes.length; i < l; i ++ ) {

        keyframe = keyframes[ i ];

        if ( keyframe.value[ property ] === undefined ) {

          keyframe.value[ property ] = null; // mark as missing

        } else {

          empty = false;

        }

      }

      if ( empty === true ) {

        // no values at all, so we set a default value

        for ( i = 0, l = keyframes.length; i < l; i ++ ) {

          keyframe = keyframes[ i ];

          keyframe.value[ property ] = defaultValue;

        }

      } else {

        // filling gaps

        createMissingKeyframes( keyframes, property );

      }

    }

    function createMissingKeyframes( keyframes, property ) {

      var prev, next;

      for ( var i = 0, l = keyframes.length; i < l; i ++ ) {

        var keyframe = keyframes[ i ];

        if ( keyframe.value[ property ] === null ) {

          prev = getPrev( keyframes, i, property );
          next = getNext( keyframes, i, property );

          if ( prev === null ) {

            keyframe.value[ property ] = next.value[ property ];
            continue;

          }

          if ( next === null ) {

            keyframe.value[ property ] = prev.value[ property ];
            continue;

          }

          interpolate( keyframe, prev, next, property );

        }

      }

    }

    function getPrev( keyframes, i, property ) {

      while ( i >= 0 ) {

        var keyframe = keyframes[ i ];

        if ( keyframe.value[ property ] !== null ) return keyframe;

        i --;

      }

      return null;

    }

    function getNext( keyframes, i, property ) {

      while ( i < keyframes.length ) {

        var keyframe = keyframes[ i ];

        if ( keyframe.value[ property ] !== null ) return keyframe;

        i ++;

      }

      return null;

    }

    function interpolate( key, prev, next, property ) {

      if ( ( next.time - prev.time ) === 0 ) {

        key.value[ property ] = prev.value[ property ];
        return;

      }

      key.value[ property ] = ( ( key.time - prev.time ) * ( next.value[ property ] - prev.value[ property ] ) / ( next.time - prev.time ) ) + prev.value[ property ];

    }

    // animation clips

    function parseAnimationClip( xml ) {

      var data = {
        name: xml.getAttribute( 'id' ) || 'default',
        start: parseFloat( xml.getAttribute( 'start' ) || 0 ),
        end: parseFloat( xml.getAttribute( 'end' ) || 0 ),
        animations: []
      };

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'instance_animation':
            data.animations.push( parseId( child.getAttribute( 'url' ) ) );
            break;

        }

      }

      library.clips[ xml.getAttribute( 'id' ) ] = data;

    }

    function buildAnimationClip( data ) {

      var tracks = [];

      var name = data.name;
      var duration = ( data.end - data.start ) || - 1;
      var animations = data.animations;

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

        var animationTracks = getAnimation( animations[ i ] );

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

          tracks.push( animationTracks[ j ] );

        }

      }

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

    }

    function getAnimationClip( id ) {

      return getBuild( library.clips[ id ], buildAnimationClip );

    }

    // controller

    function parseController( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'skin':
            // there is exactly one skin per controller
            data.id = parseId( child.getAttribute( 'source' ) );
            data.skin = parseSkin( child );
            break;

          case 'morph':
            data.id = parseId( child.getAttribute( 'source' ) );
            console.warn( 'THREE.ColladaLoader: Morph target animation not supported yet.' );
            break;

        }

      }

      library.controllers[ xml.getAttribute( 'id' ) ] = data;

    }

    function parseSkin( xml ) {

      var data = {
        sources: {}
      };

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'bind_shape_matrix':
            data.bindShapeMatrix = parseFloats( child.textContent );
            break;

          case 'source':
            var id = child.getAttribute( 'id' );
            data.sources[ id ] = parseSource( child );
            break;

          case 'joints':
            data.joints = parseJoints( child );
            break;

          case 'vertex_weights':
            data.vertexWeights = parseVertexWeights( child );
            break;

        }

      }

      return data;

    }

    function parseJoints( xml ) {

      var data = {
        inputs: {}
      };

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'input':
            var semantic = child.getAttribute( 'semantic' );
            var id = parseId( child.getAttribute( 'source' ) );
            data.inputs[ semantic ] = id;
            break;

        }

      }

      return data;

    }

    function parseVertexWeights( xml ) {

      var data = {
        inputs: {}
      };

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'input':
            var semantic = child.getAttribute( 'semantic' );
            var id = parseId( child.getAttribute( 'source' ) );
            var offset = parseInt( child.getAttribute( 'offset' ) );
            data.inputs[ semantic ] = { id: id, offset: offset };
            break;

          case 'vcount':
            data.vcount = parseInts( child.textContent );
            break;

          case 'v':
            data.v = parseInts( child.textContent );
            break;

        }

      }

      return data;

    }

    function buildController( data ) {

      var build = {
        id: data.id
      };

      var geometry = library.geometries[ build.id ];

      if ( data.skin !== undefined ) {

        build.skin = buildSkin( data.skin );

        // we enhance the 'sources' property of the corresponding geometry with our skin data

        geometry.sources.skinIndices = build.skin.indices;
        geometry.sources.skinWeights = build.skin.weights;

      }

      return build;

    }

    function buildSkin( data ) {

      var BONE_LIMIT = 4;

      var build = {
        joints: [], // this must be an array to preserve the joint order
        indices: {
          array: [],
          stride: BONE_LIMIT
        },
        weights: {
          array: [],
          stride: BONE_LIMIT
        }
      };

      var sources = data.sources;
      var vertexWeights = data.vertexWeights;

      var vcount = vertexWeights.vcount;
      var v = vertexWeights.v;
      var jointOffset = vertexWeights.inputs.JOINT.offset;
      var weightOffset = vertexWeights.inputs.WEIGHT.offset;

      var jointSource = data.sources[ data.joints.inputs.JOINT ];
      var inverseSource = data.sources[ data.joints.inputs.INV_BIND_MATRIX ];

      var weights = sources[ vertexWeights.inputs.WEIGHT.id ].array;
      var stride = 0;

      var i, j, l;

      // procces skin data for each vertex

      for ( i = 0, l = vcount.length; i < l; i ++ ) {

        var jointCount = vcount[ i ]; // this is the amount of joints that affect a single vertex
        var vertexSkinData = [];

        for ( j = 0; j < jointCount; j ++ ) {

          var skinIndex = v[ stride + jointOffset ];
          var weightId = v[ stride + weightOffset ];
          var skinWeight = weights[ weightId ];

          vertexSkinData.push( { index: skinIndex, weight: skinWeight } );

          stride += 2;

        }

        // we sort the joints in descending order based on the weights.
        // this ensures, we only procced the most important joints of the vertex

        vertexSkinData.sort( descending );

        // now we provide for each vertex a set of four index and weight values.
        // the order of the skin data matches the order of vertices

        for ( j = 0; j < BONE_LIMIT; j ++ ) {

          var d = vertexSkinData[ j ];

          if ( d !== undefined ) {

            build.indices.array.push( d.index );
            build.weights.array.push( d.weight );

          } else {

            build.indices.array.push( 0 );
            build.weights.array.push( 0 );

          }

        }

      }

      // setup bind matrix

      if ( data.bindShapeMatrix ) {

        build.bindMatrix = new THREE.Matrix4().fromArray( data.bindShapeMatrix ).transpose();

      } else {

        build.bindMatrix = new THREE.Matrix4().identity();

      }

      // process bones and inverse bind matrix data

      for ( i = 0, l = jointSource.array.length; i < l; i ++ ) {

        var name = jointSource.array[ i ];
        var boneInverse = new THREE.Matrix4().fromArray( inverseSource.array, i * inverseSource.stride ).transpose();

        build.joints.push( { name: name, boneInverse: boneInverse } );

      }

      return build;

      // array sort function

      function descending( a, b ) {

        return b.weight - a.weight;

      }

    }

    function getController( id ) {

      return getBuild( library.controllers[ id ], buildController );

    }

    // image

    function parseImage( xml ) {

      var data = {
        init_from: getElementsByTagName( xml, 'init_from' )[ 0 ].textContent
      };

      library.images[ xml.getAttribute( 'id' ) ] = data;

    }

    function buildImage( data ) {

      if ( data.build !== undefined ) return data.build;

      return data.init_from;

    }

    function getImage( id ) {

      var data = library.images[ id ];

      if ( data !== undefined ) {

        return getBuild( data, buildImage );

      }

      console.warn( 'THREE.ColladaLoader: Couldn\'t find image with ID:', id );

      return null;

    }

    // effect

    function parseEffect( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'profile_COMMON':
            data.profile = parseEffectProfileCOMMON( child );
            break;

        }

      }

      library.effects[ xml.getAttribute( 'id' ) ] = data;

    }

    function parseEffectProfileCOMMON( xml ) {

      var data = {
        surfaces: {},
        samplers: {}
      };

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'newparam':
            parseEffectNewparam( child, data );
            break;

          case 'technique':
            data.technique = parseEffectTechnique( child );
            break;

          case 'extra':
            data.extra = parseEffectExtra( child );
            break;

        }

      }

      return data;

    }

    function parseEffectNewparam( xml, data ) {

      var sid = xml.getAttribute( 'sid' );

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'surface':
            data.surfaces[ sid ] = parseEffectSurface( child );
            break;

          case 'sampler2D':
            data.samplers[ sid ] = parseEffectSampler( child );
            break;

        }

      }

    }

    function parseEffectSurface( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'init_from':
            data.init_from = child.textContent;
            break;

        }

      }

      return data;

    }

    function parseEffectSampler( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'source':
            data.source = child.textContent;
            break;

        }

      }

      return data;

    }

    function parseEffectTechnique( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'constant':
          case 'lambert':
          case 'blinn':
          case 'phong':
            data.type = child.nodeName;
            data.parameters = parseEffectParameters( child );
            break;

        }

      }

      return data;

    }

    function parseEffectParameters( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'emission':
          case 'diffuse':
          case 'specular':
          case 'bump':
          case 'ambient':
          case 'shininess':
          case 'transparency':
            data[ child.nodeName ] = parseEffectParameter( child );
            break;
          case 'transparent':
            data[ child.nodeName ] = {
              opaque: child.getAttribute( 'opaque' ),
              data: parseEffectParameter( child )
            };
            break;

        }

      }

      return data;

    }

    function parseEffectParameter( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'color':
            data[ child.nodeName ] = parseFloats( child.textContent );
            break;

          case 'float':
            data[ child.nodeName ] = parseFloat( child.textContent );
            break;

          case 'texture':
            data[ child.nodeName ] = { id: child.getAttribute( 'texture' ), extra: parseEffectParameterTexture( child ) };
            break;

        }

      }

      return data;

    }

    function parseEffectParameterTexture( xml ) {

      var data = {
        technique: {}
      };

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'extra':
            parseEffectParameterTextureExtra( child, data );
            break;

        }

      }

      return data;

    }

    function parseEffectParameterTextureExtra( xml, data ) {

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'technique':
            parseEffectParameterTextureExtraTechnique( child, data );
            break;

        }

      }

    }

    function parseEffectParameterTextureExtraTechnique( xml, data ) {

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'repeatU':
          case 'repeatV':
          case 'offsetU':
          case 'offsetV':
            data.technique[ child.nodeName ] = parseFloat( child.textContent );
            break;

          case 'wrapU':
          case 'wrapV':

            // some files have values for wrapU/wrapV which become NaN via parseInt

            if ( child.textContent.toUpperCase() === 'TRUE' ) {

              data.technique[ child.nodeName ] = 1;

            } else if ( child.textContent.toUpperCase() === 'FALSE' ) {

              data.technique[ child.nodeName ] = 0;

            } else {

              data.technique[ child.nodeName ] = parseInt( child.textContent );

            }

            break;

        }

      }

    }

    function parseEffectExtra( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'technique':
            data.technique = parseEffectExtraTechnique( child );
            break;

        }

      }

      return data;

    }

    function parseEffectExtraTechnique( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'double_sided':
            data[ child.nodeName ] = parseInt( child.textContent );
            break;

        }

      }

      return data;

    }

    function buildEffect( data ) {

      return data;

    }

    function getEffect( id ) {

      return getBuild( library.effects[ id ], buildEffect );

    }

    // material

    function parseMaterial( xml ) {

      var data = {
        name: xml.getAttribute( 'name' )
      };

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'instance_effect':
            data.url = parseId( child.getAttribute( 'url' ) );
            break;

        }

      }

      library.materials[ xml.getAttribute( 'id' ) ] = data;

    }

    function getTextureLoader( image ) {

      var loader;

      var extension = image.slice( ( image.lastIndexOf( '.' ) - 1 >>> 0 ) + 2 ); // http://www.jstips.co/en/javascript/get-file-extension/
      extension = extension.toLowerCase();

      switch ( extension ) {

        case 'tga':
          loader = tgaLoader;
          break;

        default:
          loader = textureLoader;

      }

      return loader;

    }

    function buildMaterial( data ) {

      var effect = getEffect( data.url );
      var technique = effect.profile.technique;
      var extra = effect.profile.extra;

      var material;

      switch ( technique.type ) {

        case 'phong':
        case 'blinn':
          material = new THREE.MeshPhongMaterial();
          break;

        case 'lambert':
          material = new THREE.MeshLambertMaterial();
          break;

        default:
          material = new THREE.MeshBasicMaterial();
          break;

      }

      material.name = data.name;

      function getTexture( textureObject ) {

        var sampler = effect.profile.samplers[ textureObject.id ];
        var image = null;

        // get image

        if ( sampler !== undefined ) {

          var surface = effect.profile.surfaces[ sampler.source ];
          image = getImage( surface.init_from );

        } else {

          console.warn( 'THREE.ColladaLoader: Undefined sampler. Access image directly (see #12530).' );
          image = getImage( textureObject.id );

        }

        // create texture if image is avaiable

        if ( image !== null ) {

          var loader = getTextureLoader( image );

          if ( loader !== undefined ) {

            var texture = loader.load( image );

            var extra = textureObject.extra;

            if ( extra !== undefined && extra.technique !== undefined && isEmpty( extra.technique ) === false ) {

              var technique = extra.technique;

              texture.wrapS = technique.wrapU ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;
              texture.wrapT = technique.wrapV ? THREE.RepeatWrapping : THREE.ClampToEdgeWrapping;

              texture.offset.set( technique.offsetU || 0, technique.offsetV || 0 );
              texture.repeat.set( technique.repeatU || 1, technique.repeatV || 1 );

            } else {

              texture.wrapS = THREE.RepeatWrapping;
              texture.wrapT = THREE.RepeatWrapping;

            }

            return texture;

          } else {

            console.warn( 'THREE.ColladaLoader: Loader for texture %s not found.', image );

            return null;

          }

        } else {

          console.warn( 'THREE.ColladaLoader: Couldn\'t create texture with ID:', textureObject.id );

          return null;

        }

      }

      var parameters = technique.parameters;

      for ( var key in parameters ) {

        var parameter = parameters[ key ];

        switch ( key ) {

          case 'diffuse':
            if ( parameter.color ) material.color.fromArray( parameter.color );
            if ( parameter.texture ) material.map = getTexture( parameter.texture );
            break;
          case 'specular':
            if ( parameter.color && material.specular ) material.specular.fromArray( parameter.color );
            if ( parameter.texture ) material.specularMap = getTexture( parameter.texture );
            break;
          case 'bump':
            if ( parameter.texture ) material.normalMap = getTexture( parameter.texture );
            break;
          case 'ambient':
            if ( parameter.texture ) material.lightMap = getTexture( parameter.texture );
            break;
          case 'shininess':
            if ( parameter.float && material.shininess ) material.shininess = parameter.float;
            break;
          case 'emission':
            if ( parameter.color && material.emissive ) material.emissive.fromArray( parameter.color );
            if ( parameter.texture ) material.emissiveMap = getTexture( parameter.texture );
            break;

        }

      }

      //

      var transparent = parameters[ 'transparent' ];
      var transparency = parameters[ 'transparency' ];

      // <transparency> does not exist but <transparent>

      if ( transparency === undefined && transparent ) {

        transparency = {
          float: 1
        };

      }

      // <transparent> does not exist but <transparency>

      if ( transparent === undefined && transparency ) {

        transparent = {
          opaque: 'A_ONE',
          data: {
            color: [ 1, 1, 1, 1 ]
          } };

      }

      if ( transparent && transparency ) {

        // handle case if a texture exists but no color

        if ( transparent.data.texture ) {

          // we do not set an alpha map (see #13792)

          material.transparent = true;

        } else {

          var color = transparent.data.color;

          switch ( transparent.opaque ) {

            case 'A_ONE':
              material.opacity = color[ 3 ] * transparency.float;
              break;
            case 'RGB_ZERO':
              material.opacity = 1 - ( color[ 0 ] * transparency.float );
              break;
            case 'A_ZERO':
              material.opacity = 1 - ( color[ 3 ] * transparency.float );
              break;
            case 'RGB_ONE':
              material.opacity = color[ 0 ] * transparency.float;
              break;
            default:
              console.warn( 'THREE.ColladaLoader: Invalid opaque type "%s" of transparent tag.', transparent.opaque );

          }

          if ( material.opacity < 1 ) material.transparent = true;

        }

      }

      //

      if ( extra !== undefined && extra.technique !== undefined && extra.technique.double_sided === 1 ) {

        material.side = THREE.DoubleSide;

      }

      return material;

    }

    function getMaterial( id ) {

      return getBuild( library.materials[ id ], buildMaterial );

    }

    // camera

    function parseCamera( xml ) {

      var data = {
        name: xml.getAttribute( 'name' )
      };

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'optics':
            data.optics = parseCameraOptics( child );
            break;

        }

      }

      library.cameras[ xml.getAttribute( 'id' ) ] = data;

    }

    function parseCameraOptics( xml ) {

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

        var child = xml.childNodes[ i ];

        switch ( child.nodeName ) {

          case 'technique_common':
            return parseCameraTechnique( child );

        }

      }

      return {};

    }

    function parseCameraTechnique( xml ) {

      var data = {};

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

        var child = xml.childNodes[ i ];

        switch ( child.nodeName ) {

          case 'perspective':
          case 'orthographic':

            data.technique = child.nodeName;
            data.parameters = parseCameraParameters( child );

            break;

        }

      }

      return data;

    }

    function parseCameraParameters( xml ) {

      var data = {};

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

        var child = xml.childNodes[ i ];

        switch ( child.nodeName ) {

          case 'xfov':
          case 'yfov':
          case 'xmag':
          case 'ymag':
          case 'znear':
          case 'zfar':
          case 'aspect_ratio':
            data[ child.nodeName ] = parseFloat( child.textContent );
            break;

        }

      }

      return data;

    }

    function buildCamera( data ) {

      var camera;

      switch ( data.optics.technique ) {

        case 'perspective':
          camera = new THREE.PerspectiveCamera(
            data.optics.parameters.yfov,
            data.optics.parameters.aspect_ratio,
            data.optics.parameters.znear,
            data.optics.parameters.zfar
          );
          break;

        case 'orthographic':
          var ymag = data.optics.parameters.ymag;
          var xmag = data.optics.parameters.xmag;
          var aspectRatio = data.optics.parameters.aspect_ratio;

          xmag = ( xmag === undefined ) ? ( ymag * aspectRatio ) : xmag;
          ymag = ( ymag === undefined ) ? ( xmag / aspectRatio ) : ymag;

          xmag *= 0.5;
          ymag *= 0.5;

          camera = new THREE.OrthographicCamera(
            - xmag, xmag, ymag, - ymag, // left, right, top, bottom
            data.optics.parameters.znear,
            data.optics.parameters.zfar
          );
          break;

        default:
          camera = new THREE.PerspectiveCamera();
          break;

      }

      camera.name = data.name;

      return camera;

    }

    function getCamera( id ) {

      var data = library.cameras[ id ];

      if ( data !== undefined ) {

        return getBuild( data, buildCamera );

      }

      console.warn( 'THREE.ColladaLoader: Couldn\'t find camera with ID:', id );

      return null;

    }

    // light

    function parseLight( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'technique_common':
            data = parseLightTechnique( child );
            break;

        }

      }

      library.lights[ xml.getAttribute( 'id' ) ] = data;

    }

    function parseLightTechnique( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'directional':
          case 'point':
          case 'spot':
          case 'ambient':

            data.technique = child.nodeName;
            data.parameters = parseLightParameters( child );

        }

      }

      return data;

    }

    function parseLightParameters( xml ) {

      var data = {};

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'color':
            var array = parseFloats( child.textContent );
            data.color = new THREE.Color().fromArray( array );
            break;

          case 'falloff_angle':
            data.falloffAngle = parseFloat( child.textContent );
            break;

          case 'quadratic_attenuation':
            var f = parseFloat( child.textContent );
            data.distance = f ? Math.sqrt( 1 / f ) : 0;
            break;

        }

      }

      return data;

    }

    function buildLight( data ) {

      var light;

      switch ( data.technique ) {

        case 'directional':
          light = new THREE.DirectionalLight();
          break;

        case 'point':
          light = new THREE.PointLight();
          break;

        case 'spot':
          light = new THREE.SpotLight();
          break;

        case 'ambient':
          light = new THREE.AmbientLight();
          break;

      }

      if ( data.parameters.color ) light.color.copy( data.parameters.color );
      if ( data.parameters.distance ) light.distance = data.parameters.distance;

      return light;

    }

    function getLight( id ) {

      var data = library.lights[ id ];

      if ( data !== undefined ) {

        return getBuild( data, buildLight );

      }

      console.warn( 'THREE.ColladaLoader: Couldn\'t find light with ID:', id );

      return null;

    }

    // geometry

    function parseGeometry( xml ) {

      var data = {
        name: xml.getAttribute( 'name' ),
        sources: {},
        vertices: {},
        primitives: []
      };

      var mesh = getElementsByTagName( xml, 'mesh' )[ 0 ];

      // the following tags inside geometry are not supported yet (see https://github.com/mrdoob/three.js/pull/12606): convex_mesh, spline, brep
      if ( mesh === undefined ) return;

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

        var child = mesh.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        var id = child.getAttribute( 'id' );

        switch ( child.nodeName ) {

          case 'source':
            data.sources[ id ] = parseSource( child );
            break;

          case 'vertices':
            // data.sources[ id ] = data.sources[ parseId( getElementsByTagName( child, 'input' )[ 0 ].getAttribute( 'source' ) ) ];
            data.vertices = parseGeometryVertices( child );
            break;

          case 'polygons':
            console.warn( 'THREE.ColladaLoader: Unsupported primitive type: ', child.nodeName );
            break;

          case 'lines':
          case 'linestrips':
          case 'polylist':
          case 'triangles':
            data.primitives.push( parseGeometryPrimitive( child ) );
            break;

          default:
            console.log( child );

        }

      }

      library.geometries[ xml.getAttribute( 'id' ) ] = data;

    }

    function parseSource( xml ) {

      var data = {
        array: [],
        stride: 3
      };

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'float_array':
            data.array = parseFloats( child.textContent );
            break;

          case 'Name_array':
            data.array = parseStrings( child.textContent );
            break;

          case 'technique_common':
            var accessor = getElementsByTagName( child, 'accessor' )[ 0 ];

            if ( accessor !== undefined ) {

              data.stride = parseInt( accessor.getAttribute( 'stride' ) );

            }
            break;

        }

      }

      return data;

    }

    function parseGeometryVertices( xml ) {

      var data = {};

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        data[ child.getAttribute( 'semantic' ) ] = parseId( child.getAttribute( 'source' ) );

      }

      return data;

    }

    function parseGeometryPrimitive( xml ) {

      var primitive = {
        type: xml.nodeName,
        material: xml.getAttribute( 'material' ),
        count: parseInt( xml.getAttribute( 'count' ) ),
        inputs: {},
        stride: 0,
        hasUV: false
      };

      for ( var i = 0, l = xml.childNodes.length; i < l; i ++ ) {

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'input':
            var id = parseId( child.getAttribute( 'source' ) );
            var semantic = child.getAttribute( 'semantic' );
            var offset = parseInt( child.getAttribute( 'offset' ) );
            var set = parseInt( child.getAttribute( 'set' ) );
            var inputname = ( set > 0 ? semantic + set : semantic );
            primitive.inputs[ inputname ] = { id: id, offset: offset };
            primitive.stride = Math.max( primitive.stride, offset + 1 );
            if ( semantic === 'TEXCOORD' ) primitive.hasUV = true;
            break;

          case 'vcount':
            primitive.vcount = parseInts( child.textContent );
            break;

          case 'p':
            primitive.p = parseInts( child.textContent );
            break;

        }

      }

      return primitive;

    }

    function groupPrimitives( primitives ) {

      var build = {};

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

        var primitive = primitives[ i ];

        if ( build[ primitive.type ] === undefined ) build[ primitive.type ] = [];

        build[ primitive.type ].push( primitive );

      }

      return build;

    }

    function checkUVCoordinates( primitives ) {

      var count = 0;

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

        var primitive = primitives[ i ];

        if ( primitive.hasUV === true ) {

          count ++;

        }

      }

      if ( count > 0 && count < primitives.length ) {

        primitives.uvsNeedsFix = true;

      }

    }

    function buildGeometry( data ) {

      var build = {};

      var sources = data.sources;
      var vertices = data.vertices;
      var primitives = data.primitives;

      if ( primitives.length === 0 ) return {};

      // our goal is to create one buffer geometry for a single type of primitives
      // first, we group all primitives by their type

      var groupedPrimitives = groupPrimitives( primitives );

      for ( var type in groupedPrimitives ) {

        var primitiveType = groupedPrimitives[ type ];

        // second, ensure consistent uv coordinates for each type of primitives (polylist,triangles or lines)

        checkUVCoordinates( primitiveType );

        // third, create a buffer geometry for each type of primitives

        build[ type ] = buildGeometryType( primitiveType, sources, vertices );

      }

      return build;

    }

    function buildGeometryType( primitives, sources, vertices ) {

      var build = {};

      var position = { array: [], stride: 0 };
      var normal = { array: [], stride: 0 };
      var uv = { array: [], stride: 0 };
      var uv2 = { array: [], stride: 0 };
      var color = { array: [], stride: 0 };

      var skinIndex = { array: [], stride: 4 };
      var skinWeight = { array: [], stride: 4 };

      var geometry = new THREE.BufferGeometry();

      var materialKeys = [];

      var start = 0;

      for ( var p = 0; p < primitives.length; p ++ ) {

        var primitive = primitives[ p ];
        var inputs = primitive.inputs;

        // groups

        var count = 0;

        switch ( primitive.type ) {

          case 'lines':
          case 'linestrips':
            count = primitive.count * 2;
            break;

          case 'triangles':
            count = primitive.count * 3;
            break;

          case 'polylist':

            for ( var g = 0; g < primitive.count; g ++ ) {

              var vc = primitive.vcount[ g ];

              switch ( vc ) {

                case 3:
                  count += 3; // single triangle
                  break;

                case 4:
                  count += 6; // quad, subdivided into two triangles
                  break;

                default:
                  count += ( vc - 2 ) * 3; // polylist with more than four vertices
                  break;

              }

            }

            break;

          default:
            console.warn( 'THREE.ColladaLoader: Unknow primitive type:', primitive.type );

        }

        geometry.addGroup( start, count, p );
        start += count;

        // material

        if ( primitive.material ) {

          materialKeys.push( primitive.material );

        }

        // geometry data

        for ( var name in inputs ) {

          var input = inputs[ name ];

          switch ( name ) {

            case 'VERTEX':
              for ( var key in vertices ) {

                var id = vertices[ key ];

                switch ( key ) {

                  case 'POSITION':
                    var prevLength = position.array.length;
                    buildGeometryData( primitive, sources[ id ], input.offset, position.array );
                    position.stride = sources[ id ].stride;

                    if ( sources.skinWeights && sources.skinIndices ) {

                      buildGeometryData( primitive, sources.skinIndices, input.offset, skinIndex.array );
                      buildGeometryData( primitive, sources.skinWeights, input.offset, skinWeight.array );

                    }

                    // see #3803

                    if ( primitive.hasUV === false && primitives.uvsNeedsFix === true ) {

                      var count = ( position.array.length - prevLength ) / position.stride;

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

                        // fill missing uv coordinates

                        uv.array.push( 0, 0 );

                      }

                    }
                    break;

                  case 'NORMAL':
                    buildGeometryData( primitive, sources[ id ], input.offset, normal.array );
                    normal.stride = sources[ id ].stride;
                    break;

                  case 'COLOR':
                    buildGeometryData( primitive, sources[ id ], input.offset, color.array );
                    color.stride = sources[ id ].stride;
                    break;

                  case 'TEXCOORD':
                    buildGeometryData( primitive, sources[ id ], input.offset, uv.array );
                    uv.stride = sources[ id ].stride;
                    break;

                  case 'TEXCOORD1':
                    buildGeometryData( primitive, sources[ id ], input.offset, uv2.array );
                    uv.stride = sources[ id ].stride;
                    break;

                  default:
                    console.warn( 'THREE.ColladaLoader: Semantic "%s" not handled in geometry build process.', key );

                }

              }
              break;

            case 'NORMAL':
              buildGeometryData( primitive, sources[ input.id ], input.offset, normal.array );
              normal.stride = sources[ input.id ].stride;
              break;

            case 'COLOR':
              buildGeometryData( primitive, sources[ input.id ], input.offset, color.array );
              color.stride = sources[ input.id ].stride;
              break;

            case 'TEXCOORD':
              buildGeometryData( primitive, sources[ input.id ], input.offset, uv.array );
              uv.stride = sources[ input.id ].stride;
              break;

            case 'TEXCOORD1':
              buildGeometryData( primitive, sources[ input.id ], input.offset, uv2.array );
              uv2.stride = sources[ input.id ].stride;
              break;

          }

        }

      }

      // build geometry

      if ( position.array.length > 0 ) geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( position.array, position.stride ) );
      if ( normal.array.length > 0 ) geometry.addAttribute( 'normal', new THREE.Float32BufferAttribute( normal.array, normal.stride ) );
      if ( color.array.length > 0 ) geometry.addAttribute( 'color', new THREE.Float32BufferAttribute( color.array, color.stride ) );
      if ( uv.array.length > 0 ) geometry.addAttribute( 'uv', new THREE.Float32BufferAttribute( uv.array, uv.stride ) );
      if ( uv2.array.length > 0 ) geometry.addAttribute( 'uv2', new THREE.Float32BufferAttribute( uv2.array, uv2.stride ) );

      if ( skinIndex.array.length > 0 ) geometry.addAttribute( 'skinIndex', new THREE.Float32BufferAttribute( skinIndex.array, skinIndex.stride ) );
      if ( skinWeight.array.length > 0 ) geometry.addAttribute( 'skinWeight', new THREE.Float32BufferAttribute( skinWeight.array, skinWeight.stride ) );

      build.data = geometry;
      build.type = primitives[ 0 ].type;
      build.materialKeys = materialKeys;

      return build;

    }

    function buildGeometryData( primitive, source, offset, array ) {

      var indices = primitive.p;
      var stride = primitive.stride;
      var vcount = primitive.vcount;

      function pushVector( i ) {

        var index = indices[ i + offset ] * sourceStride;
        var length = index + sourceStride;

        for ( ; index < length; index ++ ) {

          array.push( sourceArray[ index ] );

        }

      }

      var sourceArray = source.array;
      var sourceStride = source.stride;

      if ( primitive.vcount !== undefined ) {

        var index = 0;

        for ( var i = 0, l = vcount.length; i < l; i ++ ) {

          var count = vcount[ i ];

          if ( count === 4 ) {

            var a = index + stride * 0;
            var b = index + stride * 1;
            var c = index + stride * 2;
            var d = index + stride * 3;

            pushVector( a ); pushVector( b ); pushVector( d );
            pushVector( b ); pushVector( c ); pushVector( d );

          } else if ( count === 3 ) {

            var a = index + stride * 0;
            var b = index + stride * 1;
            var c = index + stride * 2;

            pushVector( a ); pushVector( b ); pushVector( c );

          } else if ( count > 4 ) {

            for ( var k = 1, kl = ( count - 2 ); k <= kl; k ++ ) {

              var a = index + stride * 0;
              var b = index + stride * k;
              var c = index + stride * ( k + 1 );

              pushVector( a ); pushVector( b ); pushVector( c );

            }

          }

          index += stride * count;

        }

      } else {

        for ( var i = 0, l = indices.length; i < l; i += stride ) {

          pushVector( i );

        }

      }

    }

    function getGeometry( id ) {

      return getBuild( library.geometries[ id ], buildGeometry );

    }

    // kinematics

    function parseKinematicsModel( xml ) {

      var data = {
        name: xml.getAttribute( 'name' ) || '',
        joints: {},
        links: []
      };

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'technique_common':
            parseKinematicsTechniqueCommon( child, data );
            break;

        }

      }

      library.kinematicsModels[ xml.getAttribute( 'id' ) ] = data;

    }

    function buildKinematicsModel( data ) {

      if ( data.build !== undefined ) return data.build;

      return data;

    }

    function getKinematicsModel( id ) {

      return getBuild( library.kinematicsModels[ id ], buildKinematicsModel );

    }

    function parseKinematicsTechniqueCommon( xml, data ) {

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'joint':
            data.joints[ child.getAttribute( 'sid' ) ] = parseKinematicsJoint( child );
            break;

          case 'link':
            data.links.push( parseKinematicsLink( child ) );
            break;

        }

      }

    }

    function parseKinematicsJoint( xml ) {

      var data;

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'prismatic':
          case 'revolute':
            data = parseKinematicsJointParameter( child );
            break;

        }

      }

      return data;

    }

    function parseKinematicsJointParameter( xml, data ) {

      var data = {
        sid: xml.getAttribute( 'sid' ),
        name: xml.getAttribute( 'name' ) || '',
        axis: new THREE.Vector3(),
        limits: {
          min: 0,
          max: 0
        },
        type: xml.nodeName,
        static: false,
        zeroPosition: 0,
        middlePosition: 0
      };

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'axis':
            var array = parseFloats( child.textContent );
            data.axis.fromArray( array );
            break;
          case 'limits':
            var max = child.getElementsByTagName( 'max' )[ 0 ];
            var min = child.getElementsByTagName( 'min' )[ 0 ];

            data.limits.max = parseFloat( max.textContent );
            data.limits.min = parseFloat( min.textContent );
            break;

        }

      }

      // if min is equal to or greater than max, consider the joint static

      if ( data.limits.min >= data.limits.max ) {

        data.static = true;

      }

      // calculate middle position

      data.middlePosition = ( data.limits.min + data.limits.max ) / 2.0;

      return data;

    }

    function parseKinematicsLink( xml ) {

      var data = {
        sid: xml.getAttribute( 'sid' ),
        name: xml.getAttribute( 'name' ) || '',
        attachments: [],
        transforms: []
      };

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'attachment_full':
            data.attachments.push( parseKinematicsAttachment( child ) );
            break;

          case 'matrix':
          case 'translate':
          case 'rotate':
            data.transforms.push( parseKinematicsTransform( child ) );
            break;

        }

      }

      return data;

    }

    function parseKinematicsAttachment( xml ) {

      var data = {
        joint: xml.getAttribute( 'joint' ).split( '/' ).pop(),
        transforms: [],
        links: []
      };

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'link':
            data.links.push( parseKinematicsLink( child ) );
            break;

          case 'matrix':
          case 'translate':
          case 'rotate':
            data.transforms.push( parseKinematicsTransform( child ) );
            break;

        }

      }

      return data;

    }

    function parseKinematicsTransform( xml ) {

      var data = {
        type: xml.nodeName
      };

      var array = parseFloats( xml.textContent );

      switch ( data.type ) {

        case 'matrix':
          data.obj = new THREE.Matrix4();
          data.obj.fromArray( array ).transpose();
          break;

        case 'translate':
          data.obj = new THREE.Vector3();
          data.obj.fromArray( array );
          break;

        case 'rotate':
          data.obj = new THREE.Vector3();
          data.obj.fromArray( array );
          data.angle = THREE.Math.degToRad( array[ 3 ] );
          break;

      }

      return data;

    }

    // physics

    function parsePhysicsModel( xml ) {

      var data = {
        name: xml.getAttribute( 'name' ) || '',
        rigidBodies: {}
      };

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'rigid_body':
            data.rigidBodies[ child.getAttribute( 'name' ) ] = {};
            parsePhysicsRigidBody( child, data.rigidBodies[ child.getAttribute( 'name' ) ] );
            break;

        }

      }

      library.physicsModels[ xml.getAttribute( 'id' ) ] = data;

    }

    function parsePhysicsRigidBody( xml, data ) {

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'technique_common':
            parsePhysicsTechniqueCommon( child, data );
            break;

        }

      }

    }

    function parsePhysicsTechniqueCommon( xml, data ) {

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'inertia':
            data.inertia = parseFloats( child.textContent );
            break;

          case 'mass':
            data.mass = parseFloats( child.textContent )[ 0 ];
            break;

        }

      }

    }

    // scene

    function parseKinematicsScene( xml ) {

      var data = {
        bindJointAxis: []
      };

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'bind_joint_axis':
            data.bindJointAxis.push( parseKinematicsBindJointAxis( child ) );
            break;

        }

      }

      library.kinematicsScenes[ parseId( xml.getAttribute( 'url' ) ) ] = data;

    }

    function parseKinematicsBindJointAxis( xml ) {

      var data = {
        target: xml.getAttribute( 'target' ).split( '/' ).pop()
      };

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'axis':
            var param = child.getElementsByTagName( 'param' )[ 0 ];
            data.axis = param.textContent;
            var tmpJointIndex = data.axis.split( 'inst_' ).pop().split( 'axis' )[ 0 ];
            data.jointIndex = tmpJointIndex.substr( 0, tmpJointIndex.length - 1 );
            break;

        }

      }

      return data;

    }

    function buildKinematicsScene( data ) {

      if ( data.build !== undefined ) return data.build;

      return data;

    }

    function getKinematicsScene( id ) {

      return getBuild( library.kinematicsScenes[ id ], buildKinematicsScene );

    }

    function setupKinematics() {

      var kinematicsModelId = Object.keys( library.kinematicsModels )[ 0 ];
      var kinematicsSceneId = Object.keys( library.kinematicsScenes )[ 0 ];
      var visualSceneId = Object.keys( library.visualScenes )[ 0 ];

      if ( kinematicsModelId === undefined || kinematicsSceneId === undefined ) return;

      var kinematicsModel = getKinematicsModel( kinematicsModelId );
      var kinematicsScene = getKinematicsScene( kinematicsSceneId );
      var visualScene = getVisualScene( visualSceneId );

      var bindJointAxis = kinematicsScene.bindJointAxis;
      var jointMap = {};

      for ( var i = 0, l = bindJointAxis.length; i < l; i ++ ) {

        var axis = bindJointAxis[ i ];

        // the result of the following query is an element of type 'translate', 'rotate','scale' or 'matrix'

        var targetElement = collada.querySelector( '[sid="' + axis.target + '"]' );

        if ( targetElement ) {

          // get the parent of the transfrom element

          var parentVisualElement = targetElement.parentElement;

          // connect the joint of the kinematics model with the element in the visual scene

          connect( axis.jointIndex, parentVisualElement );

        }

      }

      function connect( jointIndex, visualElement ) {

        var visualElementName = visualElement.getAttribute( 'name' );
        var joint = kinematicsModel.joints[ jointIndex ];

        visualScene.traverse( function ( object ) {

          if ( object.name === visualElementName ) {

            jointMap[ jointIndex ] = {
              object: object,
              transforms: buildTransformList( visualElement ),
              joint: joint,
              position: joint.zeroPosition
            };

          }

        } );

      }

      var m0 = new THREE.Matrix4();

      kinematics = {

        joints: kinematicsModel && kinematicsModel.joints,

        getJointValue: function ( jointIndex ) {

          var jointData = jointMap[ jointIndex ];

          if ( jointData ) {

            return jointData.position;

          } else {

            console.warn( 'THREE.ColladaLoader: Joint ' + jointIndex + ' doesn\'t exist.' );

          }

        },

        setJointValue: function ( jointIndex, value ) {

          var jointData = jointMap[ jointIndex ];

          if ( jointData ) {

            var joint = jointData.joint;

            if ( value > joint.limits.max || value < joint.limits.min ) {

              console.warn( 'THREE.ColladaLoader: Joint ' + jointIndex + ' value ' + value + ' outside of limits (min: ' + joint.limits.min + ', max: ' + joint.limits.max + ').' );

            } else if ( joint.static ) {

              console.warn( 'THREE.ColladaLoader: Joint ' + jointIndex + ' is static.' );

            } else {

              var object = jointData.object;
              var axis = joint.axis;
              var transforms = jointData.transforms;

              matrix.identity();

              // each update, we have to apply all transforms in the correct order

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

                var transform = transforms[ i ];

                // if there is a connection of the transform node with a joint, apply the joint value

                if ( transform.sid && transform.sid.indexOf( jointIndex ) !== - 1 ) {

                  switch ( joint.type ) {

                    case 'revolute':
                      matrix.multiply( m0.makeRotationAxis( axis, THREE.Math.degToRad( value ) ) );
                      break;

                    case 'prismatic':
                      matrix.multiply( m0.makeTranslation( axis.x * value, axis.y * value, axis.z * value ) );
                      break;

                    default:
                      console.warn( 'THREE.ColladaLoader: Unknown joint type: ' + joint.type );
                      break;

                  }

                } else {

                  switch ( transform.type ) {

                    case 'matrix':
                      matrix.multiply( transform.obj );
                      break;

                    case 'translate':
                      matrix.multiply( m0.makeTranslation( transform.obj.x, transform.obj.y, transform.obj.z ) );
                      break;

                    case 'scale':
                      matrix.scale( transform.obj );
                      break;

                    case 'rotate':
                      matrix.multiply( m0.makeRotationAxis( transform.obj, transform.angle ) );
                      break;

                  }

                }

              }

              object.matrix.copy( matrix );
              object.matrix.decompose( object.position, object.quaternion, object.scale );

              jointMap[ jointIndex ].position = value;

            }

          } else {

            console.log( 'THREE.ColladaLoader: ' + jointIndex + ' does not exist.' );

          }

        }

      };

    }

    function buildTransformList( node ) {

      var transforms = [];

      var xml = collada.querySelector( '[id="' + node.id + '"]' );

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'matrix':
            var array = parseFloats( child.textContent );
            var matrix = new THREE.Matrix4().fromArray( array ).transpose();
            transforms.push( {
              sid: child.getAttribute( 'sid' ),
              type: child.nodeName,
              obj: matrix
            } );
            break;

          case 'translate':
          case 'scale':
            var array = parseFloats( child.textContent );
            var vector = new THREE.Vector3().fromArray( array );
            transforms.push( {
              sid: child.getAttribute( 'sid' ),
              type: child.nodeName,
              obj: vector
            } );
            break;

          case 'rotate':
            var array = parseFloats( child.textContent );
            var vector = new THREE.Vector3().fromArray( array );
            var angle = THREE.Math.degToRad( array[ 3 ] );
            transforms.push( {
              sid: child.getAttribute( 'sid' ),
              type: child.nodeName,
              obj: vector,
              angle: angle
            } );
            break;

        }

      }

      return transforms;

    }

    // nodes

    function prepareNodes( xml ) {

      var elements = xml.getElementsByTagName( 'node' );

      // ensure all node elements have id attributes

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

        var element = elements[ i ];

        if ( element.hasAttribute( 'id' ) === false ) {

          element.setAttribute( 'id', generateId() );

        }

      }

    }

    var matrix = new THREE.Matrix4();
    var vector = new THREE.Vector3();

    function parseNode( xml ) {

      var data = {
        name: xml.getAttribute( 'name' ) || '',
        type: xml.getAttribute( 'type' ),
        id: xml.getAttribute( 'id' ),
        sid: xml.getAttribute( 'sid' ),
        matrix: new THREE.Matrix4(),
        nodes: [],
        instanceCameras: [],
        instanceControllers: [],
        instanceLights: [],
        instanceGeometries: [],
        instanceNodes: [],
        transforms: {}
      };

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

        var child = xml.childNodes[ i ];

        if ( child.nodeType !== 1 ) continue;

        switch ( child.nodeName ) {

          case 'node':
            data.nodes.push( child.getAttribute( 'id' ) );
            parseNode( child );
            break;

          case 'instance_camera':
            data.instanceCameras.push( parseId( child.getAttribute( 'url' ) ) );
            break;

          case 'instance_controller':
            data.instanceControllers.push( parseNodeInstance( child ) );
            break;

          case 'instance_light':
            data.instanceLights.push( parseId( child.getAttribute( 'url' ) ) );
            break;

          case 'instance_geometry':
            data.instanceGeometries.push( parseNodeInstance( child ) );
            break;

          case 'instance_node':
            data.instanceNodes.push( parseId( child.getAttribute( 'url' ) ) );
            break;

          case 'matrix':
            var array = parseFloats( child.textContent );
            data.matrix.multiply( matrix.fromArray( array ).transpose() );
            data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName;
            break;

          case 'translate':
            var array = parseFloats( child.textContent );
            vector.fromArray( array );
            data.matrix.multiply( matrix.makeTranslation( vector.x, vector.y, vector.z ) );
            data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName;
            break;

          case 'rotate':
            var array = parseFloats( child.textContent );
            var angle = THREE.Math.degToRad( array[ 3 ] );
            data.matrix.multiply( matrix.makeRotationAxis( vector.fromArray( array ), angle ) );
            data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName;
            break;

          case 'scale':
            var array = parseFloats( child.textContent );
            data.matrix.scale( vector.fromArray( array ) );
            data.transforms[ child.getAttribute( 'sid' ) ] = child.nodeName;
            break;

          case 'extra':
            break;

          default:
            console.log( child );

        }

      }

      if ( hasNode( data.id ) ) {

        console.warn( 'THREE.ColladaLoader: There is already a node with ID %s. Exclude current node from further processing.', data.id );

      } else {

        library.nodes[ data.id ] = data;

      }

      return data;

    }

    function parseNodeInstance( xml ) {

      var data = {
        id: parseId( xml.getAttribute( 'url' ) ),
        materials: {},
        skeletons: []
      };

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

        var child = xml.childNodes[ i ];

        switch ( child.nodeName ) {

          case 'bind_material':
            var instances = child.getElementsByTagName( 'instance_material' );

            for ( var j = 0; j < instances.length; j ++ ) {

              var instance = instances[ j ];
              var symbol = instance.getAttribute( 'symbol' );
              var target = instance.getAttribute( 'target' );

              data.materials[ symbol ] = parseId( target );

            }

            break;

          case 'skeleton':
            data.skeletons.push( parseId( child.textContent ) );
            break;

          default:
            break;

        }

      }

      return data;

    }

    function buildSkeleton( skeletons, joints ) {

      var boneData = [];
      var sortedBoneData = [];

      var i, j, data;

      // a skeleton can have multiple root bones. collada expresses this
      // situtation with multiple "skeleton" tags per controller instance

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

        var skeleton = skeletons[ i ];

        var root;

        if ( hasNode( skeleton ) ) {

          root = getNode( skeleton );
          buildBoneHierarchy( root, joints, boneData );

        } else if ( hasVisualScene( skeleton ) ) {

          // handle case where the skeleton refers to the visual scene (#13335)

          var visualScene = library.visualScenes[ skeleton ];
          var children = visualScene.children;

          for ( var j = 0; j < children.length; j ++ ) {

            var child = children[ j ];

            if ( child.type === 'JOINT' ) {

              var root = getNode( child.id );
              buildBoneHierarchy( root, joints, boneData );

            }

          }

        } else {

          console.error( 'THREE.ColladaLoader: Unable to find root bone of skeleton with ID:', skeleton );

        }

      }

      // sort bone data (the order is defined in the corresponding controller)

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

        for ( j = 0; j < boneData.length; j ++ ) {

          data = boneData[ j ];

          if ( data.bone.name === joints[ i ].name ) {

            sortedBoneData[ i ] = data;
            data.processed = true;
            break;

          }

        }

      }

      // add unprocessed bone data at the end of the list

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

        data = boneData[ i ];

        if ( data.processed === false ) {

          sortedBoneData.push( data );
          data.processed = true;

        }

      }

      // setup arrays for skeleton creation

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

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

        data = sortedBoneData[ i ];

        bones.push( data.bone );
        boneInverses.push( data.boneInverse );

      }

      return new THREE.Skeleton( bones, boneInverses );

    }

    function buildBoneHierarchy( root, joints, boneData ) {

      // setup bone data from visual scene

      root.traverse( function ( object ) {

        if ( object.isBone === true ) {

          var boneInverse;

          // retrieve the boneInverse from the controller data

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

            var joint = joints[ i ];

            if ( joint.name === object.name ) {

              boneInverse = joint.boneInverse;
              break;

            }

          }

          if ( boneInverse === undefined ) {

            // Unfortunately, there can be joints in the visual scene that are not part of the
            // corresponding controller. In this case, we have to create a dummy boneInverse matrix
            // for the respective bone. This bone won't affect any vertices, because there are no skin indices
            // and weights defined for it. But we still have to add the bone to the sorted bone list in order to
            // ensure a correct animation of the model.

            boneInverse = new THREE.Matrix4();

          }

          boneData.push( { bone: object, boneInverse: boneInverse, processed: false } );

        }

      } );

    }

    function buildNode( data ) {

      var objects = [];

      var matrix = data.matrix;
      var nodes = data.nodes;
      var type = data.type;
      var instanceCameras = data.instanceCameras;
      var instanceControllers = data.instanceControllers;
      var instanceLights = data.instanceLights;
      var instanceGeometries = data.instanceGeometries;
      var instanceNodes = data.instanceNodes;

      // nodes

      for ( var i = 0, l = nodes.length; i < l; i ++ ) {

        objects.push( getNode( nodes[ i ] ) );

      }

      // instance cameras

      for ( var i = 0, l = instanceCameras.length; i < l; i ++ ) {

        var instanceCamera = getCamera( instanceCameras[ i ] );

        if ( instanceCamera !== null ) {

          objects.push( instanceCamera.clone() );

        }

      }

      // instance controllers

      for ( var i = 0, l = instanceControllers.length; i < l; i ++ ) {

        var instance = instanceControllers[ i ];
        var controller = getController( instance.id );
        var geometries = getGeometry( controller.id );
        var newObjects = buildObjects( geometries, instance.materials );

        var skeletons = instance.skeletons;
        var joints = controller.skin.joints;

        var skeleton = buildSkeleton( skeletons, joints );

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

          var object = newObjects[ j ];

          if ( object.isSkinnedMesh ) {

            object.bind( skeleton, controller.skin.bindMatrix );
            object.normalizeSkinWeights();

          }

          objects.push( object );

        }

      }

      // instance lights

      for ( var i = 0, l = instanceLights.length; i < l; i ++ ) {

        var instanceLight = getLight( instanceLights[ i ] );

        if ( instanceLight !== null ) {

          objects.push( instanceLight.clone() );

        }

      }

      // instance geometries

      for ( var i = 0, l = instanceGeometries.length; i < l; i ++ ) {

        var instance = instanceGeometries[ i ];

        // a single geometry instance in collada can lead to multiple object3Ds.
        // this is the case when primitives are combined like triangles and lines

        var geometries = getGeometry( instance.id );
        var newObjects = buildObjects( geometries, instance.materials );

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

          objects.push( newObjects[ j ] );

        }

      }

      // instance nodes

      for ( var i = 0, l = instanceNodes.length; i < l; i ++ ) {

        objects.push( getNode( instanceNodes[ i ] ).clone() );

      }

      var object;

      if ( nodes.length === 0 && objects.length === 1 ) {

        object = objects[ 0 ];

      } else {

        object = ( type === 'JOINT' ) ? new THREE.Bone() : new THREE.Group();

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

          object.add( objects[ i ] );

        }

      }

      if ( object.name === '' ) {

        object.name = ( type === 'JOINT' ) ? data.sid : data.name;

      }

      object.matrix.copy( matrix );
      object.matrix.decompose( object.position, object.quaternion, object.scale );

      return object;

    }

    var fallbackMaterial = new THREE.MeshBasicMaterial( { color: 0xff00ff } );

    function resolveMaterialBinding( keys, instanceMaterials ) {

      var materials = [];

      for ( var i = 0, l = keys.length; i < l; i ++ ) {

        var id = instanceMaterials[ keys[ i ] ];

        if ( id === undefined ) {

          console.warn( 'THREE.ColladaLoader: Material with key %s not found. Apply fallback material.', keys[ i ] );
          materials.push( fallbackMaterial );

        } else {

          materials.push( getMaterial( id ) );

        }

      }

      return materials;

    }

    function buildObjects( geometries, instanceMaterials ) {

      var objects = [];

      for ( var type in geometries ) {

        var geometry = geometries[ type ];

        var materials = resolveMaterialBinding( geometry.materialKeys, instanceMaterials );

        // handle case if no materials are defined

        if ( materials.length === 0 ) {

          if ( type === 'lines' || type === 'linestrips' ) {

            materials.push( new THREE.LineBasicMaterial() );

          } else {

            materials.push( new THREE.MeshPhongMaterial() );

          }

        }

        // regard skinning

        var skinning = ( geometry.data.attributes.skinIndex !== undefined );

        if ( skinning ) {

          for ( var i = 0, l = materials.length; i < l; i ++ ) {

            materials[ i ].skinning = true;

          }

        }

        // choose between a single or multi materials (material array)

        var material = ( materials.length === 1 ) ? materials[ 0 ] : materials;

        // now create a specific 3D object

        var object;

        switch ( type ) {

          case 'lines':
            object = new THREE.LineSegments( geometry.data, material );
            break;

          case 'linestrips':
            object = new THREE.Line( geometry.data, material );
            break;

          case 'triangles':
          case 'polylist':
            if ( skinning ) {

              object = new THREE.SkinnedMesh( geometry.data, material );

            } else {

              object = new THREE.Mesh( geometry.data, material );

            }
            break;

        }

        objects.push( object );

      }

      return objects;

    }

    function hasNode( id ) {

      return library.nodes[ id ] !== undefined;

    }

    function getNode( id ) {

      return getBuild( library.nodes[ id ], buildNode );

    }

    // visual scenes

    function parseVisualScene( xml ) {

      var data = {
        name: xml.getAttribute( 'name' ),
        children: []
      };

      prepareNodes( xml );

      var elements = getElementsByTagName( xml, 'node' );

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

        data.children.push( parseNode( elements[ i ] ) );

      }

      library.visualScenes[ xml.getAttribute( 'id' ) ] = data;

    }

    function buildVisualScene( data ) {

      var group = new THREE.Group();
      group.name = data.name;

      var children = data.children;

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

        var child = children[ i ];

        group.add( getNode( child.id ) );

      }

      return group;

    }

    function hasVisualScene( id ) {

      return library.visualScenes[ id ] !== undefined;

    }

    function getVisualScene( id ) {

      return getBuild( library.visualScenes[ id ], buildVisualScene );

    }

    // scenes

    function parseScene( xml ) {

      var instance = getElementsByTagName( xml, 'instance_visual_scene' )[ 0 ];
      return getVisualScene( parseId( instance.getAttribute( 'url' ) ) );

    }

    function setupAnimations() {

      var clips = library.clips;

      if ( isEmpty( clips ) === true ) {

        if ( isEmpty( library.animations ) === false ) {

          // if there are animations but no clips, we create a default clip for playback

          var tracks = [];

          for ( var id in library.animations ) {

            var animationTracks = getAnimation( id );

            for ( var i = 0, l = animationTracks.length; i < l; i ++ ) {

              tracks.push( animationTracks[ i ] );

            }

          }

          animations.push( new THREE.AnimationClip( 'default', - 1, tracks ) );

        }

      } else {

        for ( var id in clips ) {

          animations.push( getAnimationClip( id ) );

        }

      }

    }

    if ( text.length === 0 ) {

      return { scene: new THREE.Scene() };

    }

    var xml = new DOMParser().parseFromString( text, 'application/xml' );

    var collada = getElementsByTagName( xml, 'COLLADA' )[ 0 ];

    // metadata

    var version = collada.getAttribute( 'version' );
    console.log( 'THREE.ColladaLoader: File version', version );

    var asset = parseAsset( getElementsByTagName( collada, 'asset' )[ 0 ] );
    var textureLoader = new THREE.TextureLoader( this.manager );
    textureLoader.setPath( this.resourcePath || path ).setCrossOrigin( this.crossOrigin );

    var tgaLoader;

    if ( THREE.TGALoader ) {

      tgaLoader = new THREE.TGALoader( this.manager );
      tgaLoader.setPath( this.resourcePath || path );

    }

    //

    var animations = [];
    var kinematics = {};
    var count = 0;

    //

    var library = {
      animations: {},
      clips: {},
      controllers: {},
      images: {},
      effects: {},
      materials: {},
      cameras: {},
      lights: {},
      geometries: {},
      nodes: {},
      visualScenes: {},
      kinematicsModels: {},
      physicsModels: {},
      kinematicsScenes: {}
    };

    parseLibrary( collada, 'library_animations', 'animation', parseAnimation );
    parseLibrary( collada, 'library_animation_clips', 'animation_clip', parseAnimationClip );
    parseLibrary( collada, 'library_controllers', 'controller', parseController );
    parseLibrary( collada, 'library_images', 'image', parseImage );
    parseLibrary( collada, 'library_effects', 'effect', parseEffect );
    parseLibrary( collada, 'library_materials', 'material', parseMaterial );
    parseLibrary( collada, 'library_cameras', 'camera', parseCamera );
    parseLibrary( collada, 'library_lights', 'light', parseLight );
    parseLibrary( collada, 'library_geometries', 'geometry', parseGeometry );
    parseLibrary( collada, 'library_nodes', 'node', parseNode );
    parseLibrary( collada, 'library_visual_scenes', 'visual_scene', parseVisualScene );
    parseLibrary( collada, 'library_kinematics_models', 'kinematics_model', parseKinematicsModel );
    parseLibrary( collada, 'library_physics_models', 'physics_model', parsePhysicsModel );
    parseLibrary( collada, 'scene', 'instance_kinematics_scene', parseKinematicsScene );

    buildLibrary( library.animations, buildAnimation );
    buildLibrary( library.clips, buildAnimationClip );
    buildLibrary( library.controllers, buildController );
    buildLibrary( library.images, buildImage );
    buildLibrary( library.effects, buildEffect );
    buildLibrary( library.materials, buildMaterial );
    buildLibrary( library.cameras, buildCamera );
    buildLibrary( library.lights, buildLight );
    buildLibrary( library.geometries, buildGeometry );
    buildLibrary( library.visualScenes, buildVisualScene );

    setupAnimations();
    setupKinematics();

    var scene = parseScene( getElementsByTagName( collada, 'scene' )[ 0 ] );

    if ( asset.upAxis === 'Z_UP' ) {

      scene.quaternion.setFromEuler( new THREE.Euler( - Math.PI / 2, 0, 0 ) );

    }

    scene.scale.multiplyScalar( asset.unit );

    return {
      animations: animations,
      kinematics: kinematics,
      library: library,
      scene: scene
    };

  }

};


/**
 * Based on aframe/examples/showcase/tracked-controls.
 *
 * Implement bounding sphere collision detection for entities with a mesh.
 * Sets the specified state on the intersected entities.
 *
 * @property {string} objects - Selector of the entities to test for collision.
 * @property {string} state - State to set on collided entities.
 *
 */
module.exports = AFRAME.registerComponent('sphere-collider', {
  schema: {
    objects: {default: ''},
    state: {default: 'collided'},
    radius: {default: 0.05},
    watch: {default: true}
  },

  init: function () {
    /** @type {MutationObserver} */
    this.observer = null;
    /** @type {Array<Element>} Elements to watch for collisions. */
    this.els = [];
    /** @type {Array<Element>} Elements currently in collision state. */
    this.collisions = [];

    this.handleHit = this.handleHit.bind(this);
    this.handleHitEnd = this.handleHitEnd.bind(this);
  },

  remove: function () {
    this.pause();
  },

  play: function () {
    const sceneEl = this.el.sceneEl;

    if (this.data.watch) {
      this.observer = new MutationObserver(this.update.bind(this, null));
      this.observer.observe(sceneEl, {childList: true, subtree: true});
    }
  },

  pause: function () {
    if (this.observer) {
      this.observer.disconnect();
      this.observer = null;
    }
  },

  /**
   * Update list of entities to test for collision.
   */
  update: function () {
    const data = this.data;
    let objectEls;

    // Push entities into list of els to intersect.
    if (data.objects) {
      objectEls = this.el.sceneEl.querySelectorAll(data.objects);
    } else {
      // If objects not defined, intersect with everything.
      objectEls = this.el.sceneEl.children;
    }
    // Convert from NodeList to Array
    this.els = Array.prototype.slice.call(objectEls);
  },

  tick: (function () {
    const position = new THREE.Vector3(),
        meshPosition = new THREE.Vector3(),
        colliderScale = new THREE.Vector3(),
        size = new THREE.Vector3(),
        box = new THREE.Box3(),
        distanceMap = new Map();
    return function () {
      const el = this.el,
          data = this.data,
          mesh = el.getObject3D('mesh'),
          collisions = [];
      let colliderRadius;

      if (!mesh) { return; }

      distanceMap.clear();
      el.object3D.getWorldPosition(position);
      el.object3D.getWorldScale(colliderScale);
      colliderRadius = data.radius * scaleFactor(colliderScale);
      // Update collision list.
      this.els.forEach(intersect);

      // Emit events and add collision states, in order of distance.
      collisions
        .sort((a, b) => distanceMap.get(a) > distanceMap.get(b) ? 1 : -1)
        .forEach(this.handleHit);

      // Remove collision state from current element.
      if (collisions.length === 0) { el.emit('hit', {el: null}); }

      // Remove collision state from other elements.
      this.collisions
        .filter((el) => !distanceMap.has(el))
        .forEach(this.handleHitEnd);

      // Store new collisions
      this.collisions = collisions;

      // Bounding sphere collision detection
      function intersect (el) {
        let radius, mesh, distance, extent;

        if (!el.isEntity) { return; }

        mesh = el.getObject3D('mesh');

        if (!mesh) { return; }

        box.setFromObject(mesh).getSize(size);
        extent = Math.max(size.x, size.y, size.z) / 2;
        radius = Math.sqrt(2 * extent * extent);
        box.getCenter(meshPosition);

        if (!radius) { return; }

        distance = position.distanceTo(meshPosition);
        if (distance < radius + colliderRadius) {
          collisions.push(el);
          distanceMap.set(el, distance);
        }
      }
      // use max of scale factors to maintain bounding sphere collision
      function scaleFactor (scaleVec) {
        return Math.max.apply(null, scaleVec.toArray());
      }
    };
  })(),

  handleHit: function (targetEl) {
    targetEl.emit('hit');
    targetEl.addState(this.data.state);
    this.el.emit('hit', {el: targetEl});
  },
  handleHitEnd: function (targetEl) {
    targetEl.emit('hitend');
    targetEl.removeState(this.data.state);
    this.el.emit('hitend', {el: targetEl});
  }
});