"use strict";

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Collection of methods connected to optimizations of loaded models (mesh merging, interleaving data, ..).
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @namespace
 */
var ModelLoadUtilities = function(){};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// INFO CUBES
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

ModelLoadUtilities.infoCube = {};

/**
 * Create info cube for pickable group of models.
 *
 * @param   {THREE.Vector3}   position          position of info cube in the scene
 * @param   {number}          [size]            size of one cube side in meters
 * @param   {boolean}         [modifyVertices]  TRUE = vertices positions will be modified directly in geometry, used for creating static info cubes for picking purposes; FALSE (default) = position will be stored as attribute and info cube will get own shader, this is used for spinning info cubes which are actually visible
 * @returns {THREE.Mesh}
 */

ModelLoadUtilities.infoCube.createInfoCube = function (position, size, modifyVertices)
{
  if (!Util.isInitialized(modifyVertices))
  {
    var modifyVertices = false;
  }

  if (!Util.isInitialized(size))
  {
    var size = Preferences.Picking.INFO_CUBE_SIZE_METERS;
  }

  var geometry = new THREE.BoxGeometry (size, size, size);
  var material = CustomThreeShaderMaterials.info_cube;
  var cube = new THREE.Mesh (geometry, material);

  if (modifyVertices)
  {
    cube.applyMatrix (new THREE.Matrix4().setPosition(position));
  }

  cube.userData.translation = [position.x, position.y, position.z];

  return cube;
};

ModelLoadUtilities.loadTextureImage = function (url)
{
  var texture = new THREE.Texture();

  var loader = new THREE.ImageLoader();

  loader.load (url, function (image)
  {
    texture.image = image;
    texture.needsUpdate = true;
  });

  return texture;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MESH MERGING
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

ModelLoadUtilities.meshMerging = {};

// debug
ModelLoadUtilities.meshMerging.lastMergedMesh = null;
ModelLoadUtilities.meshMerging.lastMergedMeshOriginalGroups = null;

/**
 * [DEV]
 *
 * @param {number}            increaseFactor
 * @param {THREE.Object3D}    mergedMesh
 * @param {*}                 originalGroups
 */
ModelLoadUtilities.meshMerging.increaseNumberOfDrawCalls = function (increaseFactor, mergedMesh, originalGroups)
{
  var newGroups = [];

  if (!Util.isInitialized (mergedMesh))
  {
    mergedMesh = ModelLoadUtilities.meshMerging.lastMergedMesh;
  }
  var mergedBufferGeometry = mergedMesh.geometry;

  if (!Util.isInitialized (originalGroups))
  {
    originalGroups = ModelLoadUtilities.meshMerging.lastMergedMeshOriginalGroups;
  }

  var splitGroup = function (newGroups, currentGroup, coef)
  {
    var end = currentGroup.start + currentGroup.count;

    var triangles = currentGroup.count / 3;

    if (triangles < coef)
    {
      coef = triangles;
    }

    var trianglesPerSplit = Math.floor (triangles / coef);
    var leftOver = triangles % coef;

    var elemsPerSplit = trianglesPerSplit*3;

    // split is nice
    if (leftOver === 0)
    {
      for (var s = 0; s < coef; s++)
      {
        newGroups.push ({
          start: currentGroup.start + s * elemsPerSplit,
          count: elemsPerSplit,
          materialIndex: currentGroup.materialIndex
        });
      }
    }
    // odd number of triangles
    else
    {
      for (var s = 0; s < coef-1; s++)
      {
        newGroups.push ({
          start: currentGroup.start + s * elemsPerSplit,
          count: elemsPerSplit,
          materialIndex: currentGroup.materialIndex
        });
      }

      // add last fucked split
      s = coef-1;
      newGroups.push ({
        start: currentGroup.start + s * elemsPerSplit,
        count: end - ( currentGroup.start + s * elemsPerSplit ),
        materialIndex: currentGroup.materialIndex
      });
    }
  };

  for (var i = 0; i < originalGroups.length; i++)
  {
    var currentGroup = originalGroups[i];

    splitGroup (newGroups, currentGroup, increaseFactor);
  }

  // update geometry groups and we are done
  mergedBufferGeometry.groups = newGroups;
  mergedBufferGeometry.groupsNeedUpdate = true;

  ModelLoadUtilities.meshMerging.printStats(mergedMesh);
};

/**
 * Support function for mergeMeshes(). It is used for collecting all material used by geometry. Ensures that we will have no duplicit materials.
 *
 * @param {THREE.Material}    material                            material which shall be added to the hashmap with all materials (keyed by material names)
 * @param {number}            materialOldIndexInFaces             what was material's index in faces of original geometry
 * @param {Object}            materialNewMappingIndexInFaces      maps old material indices to material indices in global array (allCombinedMaterials)
 * @param {THREE.Material[]}  allCombinedMaterials                array with all material which are found in meshes, each material in array is unique
 * @param {Object}            allCombinedMaterialsIndicesHashmap  for each material name, we save at which index in array allCombinedMaterials it is
 */
ModelLoadUtilities.meshMerging._addMaterial = function (material, materialOldIndexInFaces, materialNewMappingIndexInFaces, allCombinedMaterials, allCombinedMaterialsIndicesHashmap)
{
  var materialIndex;

  // material not in array so far, so add it
  if (typeof allCombinedMaterialsIndicesHashmap[material.name] === "undefined")
  {
    allCombinedMaterials.push(material);

    materialIndex = allCombinedMaterials.length-1;

    allCombinedMaterialsIndicesHashmap[material.name] = materialIndex;
  }
  // is in array already, grab its index in the array
  else
  {
    materialIndex = allCombinedMaterialsIndicesHashmap[material.name];
  }

  materialNewMappingIndexInFaces[materialOldIndexInFaces] = materialIndex;
};

/**
 * Performs optimization of mesh merging (mesh combining). Parameters for optimization can be found in Preferences.Optimization.
 * These optimizations dramatically reduce number of WebGL state changes and drawing calls. It is best suited for
 * static geometry. The disadvantage is that by merging all together, we lose some functionality (or rather it becomes ineffective)
 * since we cannot treat mesh one by one. For example, we have to say goodbye to frustum culling or LOD.
 *
 * Currently tested for models loaded by Collada Loader (DAE).
 *
 * @param   {THREE.Object3D}  groupWithMeshes         scene graph node with all meshes that shall be merged together
 * @param   {Object}          [attributesToInclude]   options object specifying which attributes shall be converted from mesh.userData.attributes to BufferGeometry and how
 * @returns {THREE.Object3D}                          scene graph node which contains merged mesh
 */
ModelLoadUtilities.meshMerging.mergeMeshes = function (groupWithMeshes, attributesToInclude)
{
  // it is extremely important that matrices (mainly world matrix) are up-to date
  groupWithMeshes.updateMatrix();
  groupWithMeshes.updateMatrixWorld();

  //////////////////////////////////////////////////////////
  // PREPARING INFO OF ATTRIBUTES FOR CONVERTING
  //////////////////////////////////////////////////////////

  // (not actual THREE attributes, just our info definition for converting)
  var attributes = [];

  // gather all attributes
  var attributeNames = Util.isInitialized(attributesToInclude) ? Object.keys (attributesToInclude) : [];

  // in order to allocate correct amount for attributes, we first have to compute how many faces will the resulting merged mesh have
  var cntMergedMeshFaces = 0;
  if (attributeNames.length !== 0)
  {
    groupWithMeshes.traverseVisible (function (currentGroupObject)
    {
      if (currentGroupObject instanceof THREE.Mesh)
      {
        cntMergedMeshFaces += currentGroupObject.geometry.faces.length;
      }
    });
  }

  for (var i = 0; i < attributeNames.length; i++ )
  {
    var currentAttributeName = attributeNames[i];
    var currentAttribute = attributesToInclude[currentAttributeName];

    currentAttribute.finalData =
    {
      // cntMergedMeshFaces * 3 = number of vertices, and that must be multiplied by size of data for each vertex!!!
      attributeTHREE: new THREE.BufferAttribute (new Float32Array (cntMergedMeshFaces * 3 * currentAttribute.itemSize), currentAttribute.itemSize),
      currentIdx: 0 // = current size of attributeTHREE.array, i.e. how many elements have been stored so far
    };

    attributes.push (currentAttribute);
  }

  //////////////////////////////////////////////////////////
  // TRAVERSING SCENE (FOR MERGING AND ATTRIBUTES PURPOSES)
  //////////////////////////////////////////////////////////

  // (for further explanation, see diploma thesis - Chapter "Optimization")

  var mergedGeometry = new THREE.Geometry();

  // picking id for each vertex (or for each face if _DEV_USE_NORMAL_GEOMETRY_FOR_MESH_MERGING is set)
  //var mergedGeometryPickingIds = [];

  // array with all material which are found in meshes, each maetrial in array is unique
  var allCombinedMaterials = [];

  // for each material name, we save at which index in array allCombinedMaterials it is
  var allCombinedMaterialsIndicesHashmap = {};

  // use traverse() function so we automatically go into the .children if any (only visible ones will be merged!)
  groupWithMeshes.traverseVisible (function (currentGroupObject)
  {
    if (currentGroupObject instanceof THREE.Mesh)
    {
      var mesh = currentGroupObject;

      //////////////////////////////////////////////////////////////////////////////
      // MESH MERGING
      //////////////////////////////////////////////////////////////////////////////

      // this maps old material indices to material indices in global array (allCombinedMaterials)
      var materialNewMappingIndexInFaces = {};

      // multi material: process each underlying material in it
      if (mesh.material instanceof THREE.MultiMaterial)
      {
        for (var m = 0; m < mesh.material.materials.length; m++)
        {
          ModelLoadUtilities.meshMerging._addMaterial (mesh.material.materials[m], m, materialNewMappingIndexInFaces, allCombinedMaterials, allCombinedMaterialsIndicesHashmap);
        }
      }
      // just one material (it has always id 0)
      else
      {
        ModelLoadUtilities.meshMerging._addMaterial(mesh.material, 0, materialNewMappingIndexInFaces, allCombinedMaterials, allCombinedMaterialsIndicesHashmap);
      }

      // special case: if mesh has ShaderMaterial (or RawShaderMaterial), it is rendered whole by custom shader (i.e. each face is rendered by this material)
       // but the problem is that some objects, e.g. THREE.BoxGeometry to be precise, have materialIndex of faces set to various values, i.e. each face has different value
       // (which is reasonable when the mesh is classic model, but in our case, the material referenced by faces DO NOT EXIST - we have just the ShaderMaterial)
       // solution is simple - set materialIndex of all faces to 0 (that is index of ShaderMaterial)
      if ((mesh.material instanceof THREE.ShaderMaterial) || (mesh.material instanceof THREE.RawShaderMaterial))
      {
        // remap faces material ids - now they will point to merged mesh material array (allCombinedMaterials)
        for (var f = 0; f < mesh.geometry.faces.length; f++)
        {
          var face = mesh.geometry.faces[f];

          face.materialIndex = materialNewMappingIndexInFaces[ 0 ];
        }
      }
      // standard case
      else
      {
        // remap faces material ids - now they will point to merged mesh material array (allCombinedMaterials)
        for (var f = 0; f < mesh.geometry.faces.length; f++)
        {
          var face = mesh.geometry.faces[f];

          // look where old material face id is mapped to
          face.materialIndex = materialNewMappingIndexInFaces[ face.materialIndex ];
        }
      }

      var normalGeometry = mesh.geometry;

      // (note: previous solution - does not work for groups sometimes, we have to multiply by world matrix of current object, not the whole scene group)
      //groupWithMeshes.updateMatrix();
      //mergedGeometry.merge (normalGeometry, groupWithMeshes.matrix);
      // new solution:
      mergedGeometry.merge (normalGeometry, currentGroupObject.matrixWorld); // note: no third parameter specifying material offset: we have already remapped material ids in faces

      //////////////////////////////////////////////////////////////////////////////
      // ATTRIBUTES TRANSFORM
      //////////////////////////////////////////////////////////////////////////////

      // attributes, or rather data from which real attributes for BufferGeometry will be created, are stored in mesh.userData.attributes

      // note: when we have per face attribute, we expect that if we have 10 faces, then we have 10 attribute values stored in mesh
      // note: per vertex attribute - we expect that if mesh has 9 vertices (and e.g. 14 faces - that does not matter now), it will have 9 attribute values defined (it WILL NOT have 9*3 values defined)

      // for each attribute that shall be incorporated
      for (var a = 0; a < attributes.length; a++)
      {
        var currentAttribute = attributes[a];

        // this is where we store all data for current attribute (don't forget to access it using variable currentBufferIdx below)
        var transformedDataPerVertex = currentAttribute.finalData.attributeTHREE.array;
        var currentBufferIdx = currentAttribute.finalData.currentIdx;

        // shortcut to transformation function
        var transform = currentAttribute.transformationCallback;

        // try to grab an array with source values for converting current attribute
        var currentAttributeToConvert = Util.isInitialized(mesh.userData.attributes) ? mesh.userData.attributes[ attributeNames[a] ] : null;

        // if mesh does not have attribute defined, but transformation callback was provided, it means that the transformation callback is more of a "creation callback"
        // we will create a dummy array so we do not have to make a million ifs when accessing it e.g. like currentAttributeToConvert[faceIdx],
        // but of course, when accessing, it will return undefined - but that does not bother us, the creation callback works only with reference to mesh and face index (or vertex index) anyway
        if (!currentAttributeToConvert && transform)
        {
          currentAttributeToConvert = [];
        }

        /////////////////////////////////////////////
        // CONVERTING ATTRIBUTE
        /////////////////////////////////////////////
        // (mesh has this attribute defined in mesh.userData)
        if (currentAttributeToConvert)
        {
          // shall we transform this attribute somehow?
          var transformationDefined = Util.isInitialized(currentAttribute.transformationCallback);

          // shortcut to transformation function
          //var transform = currentAttribute.transformationCallback;

          ////////////////////////////////////
          // TRANSFORMATION
          ////////////////////////////////////

          // (we have to transform values from source array somehow)
          if (transformationDefined)
          {
            if (currentAttribute.perFaceAttribute)
            {
              for (var faceIdx = 0; faceIdx < mesh.geometry.faces.length; faceIdx++)
              {
                // in case of per face attributes, we have one item (can be 1, 2 or more values) for all face vertices
                // pass mesh reference, index of current face and current value of face which shall be converted
                var transformedValueForFace = transform(mesh, faceIdx, currentAttributeToConvert[faceIdx]);

                // copy transformed vertex data for this face into the attribute's buffer (there are 3 vertices per face)
                // (these fors can be joined, but for clarity, I have left it like this)
                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = transformedValueForFace;
                }
                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = transformedValueForFace;
                }
                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = transformedValueForFace;
                }
              }
            }
            // per vertex attribute
            else
            {
              // transform data for all vertices using provided transformation callback
              var transformedVerticesData = [];
              for (var vertexIdx = 0; vertexIdx < mesh.geometry.vertices.length; vertexIdx++)
              {
                // pass reference to the mesh, current vertex index AND current attribute to convert - that is enough info for whatever
                transformedVerticesData.push ( transform(mesh, vertexIdx, currentAttributeToConvert[vertexIdx]) );
              }

              // now the catch is that vertices are shared by multiple faces - we have to duplicate this vertex data for storing into buffers
              for (var faceIdx = 0; faceIdx < mesh.geometry.faces.length; faceIdx++)
              {
                var currentFace = mesh.geometry.faces[faceIdx];

                // copy transformed vertex data for this face into the attribute's buffer
                /*transformedDataPerVertex[currentBufferIdx++] = transformedVerticesData[currentFace.a];
                transformedDataPerVertex[currentBufferIdx++] = transformedVerticesData[currentFace.b];
                transformedDataPerVertex[currentBufferIdx++] = transformedVerticesData[currentFace.c];*/

                // copy transformed vertex data for this face into the attribute's buffer

                // first vertex
                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = transformedVerticesData[currentFace.a][i];
                }

                // second vertex
                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = transformedVerticesData[currentFace.b][i];
                }

                // third vertex
                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = transformedVerticesData[currentFace.c][i];
                }
              }
            }
          }
          ////////////////////////////////////
          // JUST COPYING
          ////////////////////////////////////
          // (no transformation of attributes is needed, just copy them from the source array)
          else
          {
            // (note: storing into transformedDataPerVertex is very, very similar to if branch above)

            if (currentAttribute.perFaceAttribute)
            {

              for (var faceIdx = 0; faceIdx < mesh.geometry.faces.length; faceIdx++)
              {
                var valueForFace = currentAttributeToConvert[faceIdx];

                // each vertex of a face will get same value
                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = valueForFace;
                }
                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = valueForFace;
                }
                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = valueForFace;
                }
              }
            }
            else
            {
              for (var faceIdx = 0; faceIdx < mesh.geometry.faces.length; faceIdx++)
              {
                var currentFace = mesh.geometry.faces[faceIdx];

                // (just like in if-branch with transformationDefined, pick value which corresponds to vertex of this face)
                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = currentAttributeToConvert[currentFace.a][i];
                }

                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = currentAttributeToConvert[currentFace.b][i];
                }

                for (var i = 0; i < currentAttribute.itemSize; i++)
                {
                  transformedDataPerVertex[currentBufferIdx++] = currentAttributeToConvert[currentFace.c][i];
                }
              }
            }
          }
        }
        /////////////////////////////////////////////
        // DEFAULT VALUES FOR ATTRIBUTE
        /////////////////////////////////////////////
        // mesh doesn't have this attribute and even no transformation (in that case, creation) callback has been provided, so we fill it with default values
        else
        {

          // if no default value is specified, we use 0
          var defaultValue = Util.isInitialized(currentAttribute.defaultValue) ? currentAttribute.defaultValue : 0;

          // if defaultValues is not a number, but a callback, we execute this callback with a mesh as an argument -- it will compute default value based on some mesh property
          // (this is used when all vertices of a mesh shall have same attribute value)
          if (defaultValue.constructor === Function)
          {
            defaultValue = defaultValue (mesh);
          }

          for (var faceIdx = 0; faceIdx < mesh.geometry.faces.length; faceIdx++)
          {
            for (var i = 0; i < currentAttribute.itemSize; i++)
            {
              transformedDataPerVertex[currentBufferIdx++] = defaultValue;
            }
            for (var i = 0; i < currentAttribute.itemSize; i++)
            {
              transformedDataPerVertex[currentBufferIdx++] = defaultValue;
            }
            for (var i = 0; i < currentAttribute.itemSize; i++)
            {
              transformedDataPerVertex[currentBufferIdx++] = defaultValue;
            }
          }
        }

        /////////////////////////////////////////////
        // CURRENT BUFFER INDEX UPDATE
        /////////////////////////////////////////////

        // IMPORTANT!!! JavaScript does not have pointers, so when we have used currentBufferIdx, we have been incrementing independent value --> we have to "update it back"

        currentAttribute.finalData.currentIdx = currentBufferIdx;
      }

      //////////////////////////////////////////

    }
  });

  //////////////////////////////////////////////////////////
  // CREATING BUFFER GEOMETRY FROM GEOMETRY
  //////////////////////////////////////////////////////////

  if (Preferences.Optimizations._DEV_USE_NORMAL_GEOMETRY_FOR_MESH_MERGING)
  {
    console.warn("You have not created BufferGeometry during scene loading. Picking won't work.");
  }
  else
  {
    var mergedBufferGeometry = new THREE.BufferGeometry();
    mergedBufferGeometry.fromGeometry (mergedGeometry);

    // buffer geometry created using .fromGeometry() has attribute color, which is not used by us, so kill it
    mergedBufferGeometry.removeAttribute("color");

    // we no longer need the original geometry (note: it could be useful during debugging, though!)
    mergedGeometry.dispose();

    mergedGeometry = mergedBufferGeometry;
    mergedBufferGeometry = null;

    // for all attributes, add them (just like with picking below), i.e. use that .finalData

    ///////////////////////////////////////////
    // ATTRIBUTE ADDITION TO BUFFER GEOMETRY
    ///////////////////////////////////////////

    for (var a = 0; a < attributes.length; a++)
    {
      mergedGeometry.addAttribute (attributeNames[a], attributes[a].finalData.attributeTHREE);

      // (is this needed when we have just added a new attribute?)
      mergedGeometry.attributes[attributeNames[a]].needsUpdate = true;
    }
  }

  ///////////////////////////////////////////
  // COMPUTING BOUNDING VOLUMES
  ///////////////////////////////////////////

  // good idea to compute bounding volumes now, because it may not be possible later (e.g. computing bounding volumes does not work in Three.js if the attributes are interleaved)
  mergedGeometry.computeBoundingBox();
  mergedGeometry.computeBoundingSphere();

  ///////////////////////////////////////////
  // DATA REARRANGEMENT OPTIMIZATIONS
  ///////////////////////////////////////////

  if (Preferences.Application.isDevMode())
  {
    if (Preferences.Optimizations.MATERIAL_DATA_REARRANGEMENT) // join material drawing groups
    {
      if (Preferences.Optimizations._DEV_USE_NORMAL_GEOMETRY_FOR_MESH_MERGING)
      {
        console.warn ("Material data rearrangement (joining of drawing groups) was not performed - geometry is not BufferGeometry.");

        if (Preferences.Optimizations.INTERLEAVED_BUFFER)
        {
          console.warn ("Interleaved buffer was not created - geometry is not BufferGeometry.");
        }
      }
      else
      {
        ModelLoadUtilities.meshMerging.dataRearrangementOptimization (mergedGeometry);
      }
    }
    else
    {
      if (Preferences.Optimizations.INTERLEAVED_BUFFER)
      {
        console.warn ("Interleaved buffer was not created - material data rearrangement optimization must be turned on for this to work.");
      }
    }
  }

  /////////////////////////////////////////////////////////////
  // CREATING FINAL MESH FROM MERGED AND OPTIMIZED GEOMETRY
  /////////////////////////////////////////////////////////////

  var mergedMaterial = new THREE.MeshFaceMaterial (allCombinedMaterials);
  var mergedMesh = new THREE.Mesh (mergedGeometry, mergedMaterial);

  // no attribute on normal geometry, not even for debug
  /*if (Preferences.Optimizations._DEV_USE_NORMAL_GEOMETRY_FOR_MESH_MERGING)
  {
    mergedMesh.userData.pickingIds = mergedGeometryPickingIds;
  }*/

  // disable matrix auto update? nevermind, probably wont be an issue when we have just one huge mesh :)
  // object.matrixAutoUpdate = false;
  // object.updateMatrix();

  mergedMesh.castShadow = true;
  mergedMesh.receiveShadow = true;

  // debug
  ModelLoadUtilities.meshMerging.lastMergedMesh = mergedMesh;
  ModelLoadUtilities.meshMerging.lastMergedMeshOriginalGroups = mergedGeometry.groups;
  ModelLoadUtilities.meshMerging.printStats (mergedMesh);

  // originally, we were returning only the big one merged mesh
  //return mergedMesh;

  // now, there is a possibility that more merged mesh will be created, so we return their group (Object3D container)
  var mergedMeshGroup = new THREE.Object3D();
  mergedMeshGroup.add (mergedMesh);

  return mergedMeshGroup;
};

/**
 * Sorts drawing groups (drawing calls) of Three.js by material.
 *
 * @param {*} groups
 */
ModelLoadUtilities.meshMerging.sortGroupsByMaterial = function (groups)
{
  groups.sort ( function(a, b)
  {
    return (a.materialIndex - b.materialIndex);
  });
};

/**
 * Triangle rearrangement. We rearrange the data so the triangles with same material are in contiguous blocks of memory.
 *
 * @param {THREE.BufferGeometry}  mergedBufferGeometry    merged big mesh
 */
ModelLoadUtilities.meshMerging.dataRearrangementOptimization = function (mergedBufferGeometry)
{
  //////////////////////////////////////////////////////
  // SORTING GROUPS BY MATERIAL
  //////////////////////////////////////////////////////

  ModelLoadUtilities.meshMerging.sortGroupsByMaterial (mergedBufferGeometry.groups);

  //////////////////////////////////////////////////////
  // BUFFER(S) FOR ATTRIBUTES CREATION
  //////////////////////////////////////////////////////

  // Note:
  // Following code mentions words "items" and "elements".
  // - Item is an attribute (position, uv, etc.) an consists of floats.
  //   How many floats item has is given by itemSize, for example position has 3 floats, uv has 2 floats.
  //   In other words, Item is some data associated with one vertex.
  // - Element = different name for float.
  // - Vertex in context of this method is often treated as a collection of items (attributes).
  //   Simple collection could have, for example, following attributes: position, normal, uv
  //

  // empty arrays where we will copy data from attribute arrays (separate buffers, i.e. used when options.interleavedBuffer is FALSE)
  var attributeNewDataBuffer = {};

  // empty array where we will copy data from attribute arrays (one big buffer, i.e. used when options.interleavedBuffer is TRUE)

  /**
   * @type {THREE.InterleavedBuffer}
   */
  var allDataInterleavedBuffer;

  /**
   * @type {THREE.BufferAttribute[]}
   */
  var attributes = [];

  // gather all attributes of the geometry

  var attributeNames = Object.keys (mergedBufferGeometry.attributes);
  for (var i = 0; i < attributeNames.length; i++ )
  {
    var currentAttributeName = attributeNames[i];
    var currentAtribute = mergedBufferGeometry.attributes[currentAttributeName];

    if (!Preferences.Optimizations.INTERLEAVED_BUFFER)
    {
      // prepare empty arrays where we will copy data from attribute arrays
      attributeNewDataBuffer[currentAttributeName] = new Float32Array (currentAtribute.array.length);

      // add our own .currentSize property which will represent current number of elements (floats) stored in new buffer so far
      currentAtribute.currentSize = 0;
    }

    attributes.push (currentAtribute);
  }

  if (Preferences.Optimizations.INTERLEAVED_BUFFER)
  {
    // compute how many floats each vertex needs (i.e. how many items has vertex in its collection)
    // e.g. if we have usual attributes like position = 3 floats, normal = 3 floats, uv = 2 floats, pickingId = 1 float => then that is total of 9 floats per 1 vertex
    var itemsPerVertex = 0;
    for (var i = 0; i < attributes.length; i++)
    {
      itemsPerVertex += attributes[i].itemSize;
    }

    var vertexCount = mergedBufferGeometry.attributes.position.count; // note: do not mistake for .length

    // prepare one empty array for an interleaved buffer where we will copy data from attribute arrays
    allDataInterleavedBuffer = new THREE.InterleavedBuffer (new Float32Array (itemsPerVertex * vertexCount), itemsPerVertex);

    // again, add our own .currentSize property which will represent current number of elements (floats) stored in new buffer so far
    allDataInterleavedBuffer.currentSize = 0;
  }

  //////////////////////////////////////////////////////
  // ATTRIBUTE DATA COPY FUNCTIONS
  //////////////////////////////////////////////////////

  /**
   * Copies part of the data in attribute's data array to a new array. This method is used for separate data buffers,
   * i.e. when geometry has one buffer for each attribute.
   *
   * The method is very similar to copyAttributeDataRangeInterleaved().
   *
   * @param {THREE.BufferAttribute}   currentAttribute    attribute which is just being copied
   * @param {number}                  sourceStartItemIdx  from which item in an attribute array shall we start with copying
   * @param {number}                  cntItemsToCopy      how many items will be copied from an attribute array
   * @param {Float32Array}            targetBuffer        into which buffer (array) will the items be copied
   */
  var copyAttributeDataRange = function (currentAttribute, sourceStartItemIdx, cntItemsToCopy, targetBuffer)
  {
    // at which position in targetBuffer we will start to insert copied items
    //var idxTargetInsertion = cntCopiedVerticesSoFar * attribute.itemSize;

    // at which array position in targetBuffer we will start to insert copied items
    var idxTargetInsertion = currentAttribute.currentSize; // in case of separate buffers, we have .currentSize stored in "old" buffers

    // number of elements (floats) which have been copied so far in this method
    var cntTotalCopiedElements = 0;

    // for each item (vertex)
    for (var i = 0; i < cntItemsToCopy; i++)
    {
        // how many elements (floats) of this attribute have been copied so far (in context of current item! not in context of all items)
        var cntAttributeCopiedElements = 0;

        // from which position in attribute array shall we start with copying
        var idxSourceStart = (sourceStartItemIdx + i) * currentAttribute.itemSize; // i = number of items ("vertices") copied so far; e.g. attribute position, 1 item = X1 Y1 Z1, 2 items = X1 Y1 Z1 and X2 Y2 Z2 and so on...

        for (var b = 0; b < currentAttribute.itemSize; b++) // account for number of floats per item (uv has 2, other like position have 3)
        {
          targetBuffer[idxTargetInsertion + cntTotalCopiedElements] = currentAttribute.array[idxSourceStart + cntAttributeCopiedElements];

          cntTotalCopiedElements++;
          cntAttributeCopiedElements++;
        }
    }

    currentAttribute.currentSize += cntTotalCopiedElements;
  };

  /**
   * Copies part of the data from all attributes' array to a new interleaved array. This method is used for interleaved data buffer,
   * i.e. when geometry has one buffer for all attributes (shared buffer) and the attributes are interleaved (e.g. P P P N N N UV UV).
   *
   * The method is very similar to copyAttributeDataRangeInterleaved().
   *
   * @param {number}                    sourceStartItemIdx  from which item in an attribute array shall we start with copying
   * @param {number}                    cntItemsToCopy      how many items will be copied from an attribute array
   * @param {THREE.InterleavedBuffer}   targetBuffer        interleaved buffer which contains an array where the items will be copied
   */
  var copyAttributeDataRangeInterleaved = function (sourceStartItemIdx, cntItemsToCopy, targetBuffer)
  {
    // at which position in targetBuffer we will start to insert copied items
    var idxTargetInsertion = targetBuffer.currentSize;

    // number of elements (floats) which have been copied so far in this method
    var cntTotalCopiedElements = 0;

    // for each item (vertex)
    for (var i = 0; i < cntItemsToCopy; i++)
    {
      // for current item (vertex), copy data of each attribute to target array
      for (var a = 0; a < attributes.length; a++)
      {
        var currentAttribute = attributes[a];

        // how many elements (floats) of this attribute have been copied so far (in context of current item! not in context of all items)
        var cntAttributeCopiedElements = 0;

        // from which position in attribute array shall we start with copying
        var idxSourceStart = (sourceStartItemIdx + i) * currentAttribute.itemSize; // i = number of items (vertices) copied so far

        for (var b = 0; b < currentAttribute.itemSize; b++) // account for number of floats per item (uv has 2, other like position have 3)
        {
          targetBuffer.array[idxTargetInsertion + cntTotalCopiedElements] = currentAttribute.array[idxSourceStart + cntAttributeCopiedElements];

          cntTotalCopiedElements++;
          cntAttributeCopiedElements++;
        }
      }
    }

    targetBuffer.currentSize += cntTotalCopiedElements;
  };

  //////////////////////////////////////////////////////
  // CREATING JOINED GROUPS OF SAME MATERIAL
  //////////////////////////////////////////////////////

  /**
   * Result of the algorithm - if there are N materials, there will be N joined groups (joined group contain geometry of all groups which have identical material).
   *
   * @type {Array}
   */
  var joinedGroupsWithSameMaterial = [];

  /**
   * Creates a new group which "encloses" all groups with same material index.
   *
   * @param   {number}                newJoinedGroupMaterialIndex   material index of all groups which are in this joined group
   * @param   {number}                cntTotalCopiedItems           current number of how many items we have copied so far from those original attributes arrays (copy of global variable)
   * @returns {{start: number, count: number, materialIndex: number}}
   */
  var createJoinedGroup = function (newJoinedGroupMaterialIndex, cntTotalCopiedItems)
  {
    var groupsExist = (joinedGroupsWithSameMaterial.length !== 0);

    var newJoinedGroupStart,  newJoinedGroupCount;
    if (groupsExist)
    {
      var previousGroup = joinedGroupsWithSameMaterial[joinedGroupsWithSameMaterial.length-1];

      newJoinedGroupStart = previousGroup.start + previousGroup.count; // it starts right after the last group
      newJoinedGroupCount = cntTotalCopiedItems - newJoinedGroupStart;
    }
    else
    {
      newJoinedGroupStart = 0;
      newJoinedGroupCount = cntTotalCopiedItems;
    }

    joinedGroupsWithSameMaterial.push({
      start: newJoinedGroupStart,
      count: newJoinedGroupCount,
      materialIndex:  newJoinedGroupMaterialIndex
    });
  };

  //////////////////////////////////////////////////////
  // ACTUAL ALGORITHM OF DATA REARRANGEMENT
  //////////////////////////////////////////////////////

  var cntTotalCopiedItems = 0;

  for (var i = 0; i < mergedBufferGeometry.groups.length; i++)
  {
    var currentGroup = mergedBufferGeometry.groups[i];

    if (Preferences.Optimizations.INTERLEAVED_BUFFER)
    {
      copyAttributeDataRangeInterleaved (currentGroup.start, currentGroup.count, allDataInterleavedBuffer);
    }
    else
    {
      for (var a = 0; a < attributes.length; a++)
      {
        copyAttributeDataRange (mergedBufferGeometry.attributes [ attributeNames[a] ],  currentGroup.start, currentGroup.count, attributeNewDataBuffer[ attributeNames[a] ]);
      }
    }

    cntTotalCopiedItems += currentGroup.count;

    if ((i === mergedBufferGeometry.groups.length - 1) || // last iteration - we have to create joined group, OR
        (currentGroup.materialIndex !== mergedBufferGeometry.groups[i+1].materialIndex) // end of groups with current materialIndex (remember, the groups are sorted by material index)
        )
    {
      // create a huge group for the material
      createJoinedGroup (currentGroup.materialIndex, cntTotalCopiedItems);
    }
  }

  if (Preferences.Optimizations.INTERLEAVED_BUFFER)
  {
    var currentOffset = 0;
    for (var i = 0; i < attributes.length; i++)
    {
      var currentAttributeName = attributeNames[i];
      var currentAttribute = attributes[i];

      var currentAttributeItemSize = currentAttribute.itemSize;

      mergedBufferGeometry.removeAttribute(currentAttributeName);

      // note: order of attributes in interleaving depends on the result of Object.keys(), see the start of this whole method;
      //       generally it will look e.g.: P P P | N N N | UV UV | PI
      //                       array index: 0 1 2   3 4 5   6  7    8
      //                       item size:     3       3       2     1
      //       as for definitions of interleaved buffer attributes for this example:
      //       ... (bigBuffer, 3, 0)
      //       ... (bigBuffer, 3, 3)
      //       ... (bigBuffer, 2, 6)
      //       ... (bigBuffer, 1, 8)
      //mergedBufferGeometry.attributes[currentAttributeName] = new THREE.InterleavedBufferAttribute (bigBuffer, currentAttributeItemSize, currentOffset);
      mergedBufferGeometry.addAttribute(currentAttributeName, new THREE.InterleavedBufferAttribute (allDataInterleavedBuffer, currentAttributeItemSize, currentOffset));
      // note2: we have actually created a new attribute (this is in contrast to the behavior in else branch of this if)

      currentOffset += currentAttributeItemSize;

      mergedBufferGeometry.attributes[currentAttributeName].needsUpdate = true;
    }
  }
  else
  {
    for (var i = 0; i < attributes.length; i++)
    {
      var currentAttributeName = attributeNames[i];

      // assign new attribute arrays (note: we are assigning only new arrays, apart from that, we are not modifying BufferAttribute)
      mergedBufferGeometry.attributes[currentAttributeName].array = attributeNewDataBuffer[currentAttributeName];

      // tell Three.js it should update buffers on graphic card
      mergedBufferGeometry.attributes[currentAttributeName].needsUpdate = true;
    }
  }

  // (manually) update geometry groups and we are done
  mergedBufferGeometry.groups = joinedGroupsWithSameMaterial;

  // not used, but interesting bit of code:
  //rendererThree.objects.update ({geometry: mergedBufferGeometry}); - to force updating of geometry; dont forget to set needsUpdate on buffer Attributes to true
};

/**
 * [DEV]
 * @param mergedMesh
 */
ModelLoadUtilities.meshMerging.printStats = function (mergedMesh)
{
  var groups = (mergedMesh.geometry instanceof THREE.BufferGeometry) ? mergedMesh.geometry.groups : (Util.isInitialized(mergedMesh.geometry._bufferGeometry) ? mergedMesh.geometry._bufferGeometry.groups : []);

  //console.log("mergedGeometry - draw calls: " + groups.length);

  if (mergedMesh.material instanceof THREE.MultiMaterial)
  {
    //console.log("mergedGeometry - materials: " + mergedMesh.material.materials.length);
  }
  else
  {
    //console.log("mergedGeometry - materials: just 1! weird...");
  }
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
