"use strict";

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

/**
 * Governs the virtual Prague Castle scene - contains reference to the Three.js scene,  contains useful
 * lists of objects (eg pickable models,  floor models /heightmap/). Main task of this class
 * is to properly load the scene and setup all models,  lights,  assign picking IDs etc.
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @constructor
 */
var Scene = function()
{
  /**
   * Main scene with models.
   *
   * @type {THREE.Scene}
   */
  this.scene = new THREE.Scene();

  /**
   * Scene with sky shader that makes visual effects of sky depending on the sun position (i.e. time of the day).
   *
   * @type {THREE.Scene}
   */
  this.sceneSky = new THREE.Scene();

  /**
   * All normal models from the scene (i.e. without info cubes,  lights,  etc.).
   *
   * @type {THREE.Object3D}
   */
  this.entireSceneModels = null;

  /**
   * Faces that are in some passageway and they form a ceiling of that passageway.
   *
   * @type {THREE.Object3D}
   */
  this.ceilingModels     = null;

  /**
   * Same as ceilingModels,  but floor faces.
   *
   * @type {THREE.Object3D}
   */
  this.floorModels       = null;

  /**
   * List of models that can be picked. This list is relevant until the mesh merging happens.
   *
   * @type {THREE.Object3D}
   */
  this.pickableModels = null;

  /**
   * Spinning info cubes near pickable models.
   *
   * @type {THREE.Object3D}
   */
  this.infoCubeModels = new THREE.Object3D();

  /**
   * Spinning info cubes near pickable models.
   *
   * @type {Array}
   */
  this.waterMaterials = [];

  /**
   * For switching models which are visible. This is useful during generation of heightmap etc.
   *
   * @type {THREE.Object3D}
   */
  this._currentlyVisibleModels = null;

  /**
   * Ambient light of the scene.
   *
   * @type {THREE.AmbientLight}
   */
  this.ambientLight = new THREE.AmbientLight (Preferences.Lights.SCENE_TERRAIN_AMBIENT_LIGHT_AT_DAY.color.getHex());

  /**
   * All lights in the night scene.
   *
   * @type {THREE.Object3D}
   */
  this.lights = new THREE.Object3D();
  this.lights.visible = true; // must be always true,  otherwise the shaders for light are not compiled and it won't be possible to turn the lights on

  ///////////////////////////////////////////////////////////////////////
  // SUN AND SHADOW MAPPING SETUP
  ///////////////////////////////////////////////////////////////////////

  /**
   * The sun. Watch out,  this is just the light. The mesh is elsewhere (VisualEffects.Sky),  although they will have
   * same positions.
   *
   * @type {THREE.DirectionalLight}
   */
  this.sun = Preferences.Lights.SCENE_DEFAULT_DIRECTIONAL_LIGHT;

  // super important to define these things right: the scene must be enclosed in the shadow frustum

  this.sun.castShadow = true;
  this.sun.position.copy (Preferences.Camera.ORTHO_POSITION);

  this.sun.shadowDarkness = Preferences.VisualEffects.SHADOW_MAP_SHADOW_DARKNESS;
  this.sun.shadowMapWidth = Preferences.VisualEffects.SHADOW_MAP_TEXTURE_SIZE_SUN.x;
  this.sun.shadowMapHeight = Preferences.VisualEffects.SHADOW_MAP_TEXTURE_SIZE_SUN.y;
  this.sun.shadowBias = Preferences.VisualEffects.SHADOW_MAP_SHADOW_BIAS;

  this.sun.shadowCameraFar = Preferences.Lights.SUN_SHADOW_CAMERA_FAR;
  this.sun.shadowCameraNear = Preferences.Lights.SUN_SHADOW_CAMERA_NEAR;

  this.sun.shadowCameraLeft   = Preferences.Camera.SHADOW_MAP_ORTHO_LEFT;
  this.sun.shadowCameraRight  = Preferences.Camera.SHADOW_MAP_ORTHO_RIGHT;
  this.sun.shadowCameraTop    = Preferences.Camera.SHADOW_MAP_ORTHO_TOP;
  this.sun.shadowCameraBottom = Preferences.Camera.SHADOW_MAP_ORTHO_BOTTOM;

  ///////////////////////////////////////////////////////////////////////
  // ADDING (SO FAR EMPTY) OBJECTS TO SCENE
  ///////////////////////////////////////////////////////////////////////

  this.scene.add (this.lights);
  this.scene.add (this.ambientLight);

  // little hack: if we had used this.sceneSky.add,  then the ambientLight would be disconnected from the scene graph of the primary scene; this way,  it is included in both
  this.sceneSky.children.push (this.ambientLight);

  this.scene.add (this.sun);

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

  /**
   * List of materials that are used by sprites.
   *
   * @type {THREE.Material[]}
   */
  this.spriteMaterials = [];

  /**
   * List of materials that used normal/bump mapping.
   *
   * @type {THREE.Material[]}
   */
  this.normalAndBumpMaterials = [];
};

/**
 * Changs visibility of all info cubes.
 *
 * @param {boolean}   visible   desired visibility
 */
Scene.prototype.setInfoCubesVisibility = function (visible)
{
  this.infoCubeModels.visible = visible;
};

/**
 * Hides all info cubes in the scene.
 */
Scene.prototype.hideInfoCubes = function ()
{
  this.setInfoCubesVisibility (false);
};

/**
 * Makes all info cubes in the scene visible.
 */
Scene.prototype.showInfoCubes  = function ()
{
  this.setInfoCubesVisibility (true);
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MESH MERGING,  TRIANGLE REARRANGEMENT
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Optimizes the models in the using technique of mesh merging.
 */
Scene.prototype.optimizeScene = function ()
{
  // optimize all scene models and add to the scene graph as currently visible group of models
  //this.entireSceneModels = ModelLoadUtilities.meshMerging.mergeMeshes (this.entireSceneModels,  true,  true);

  var meshesForMerging = new THREE.Object3D().add (this.entireSceneModels);

  if (Preferences.Optimizations.MESH_MERGING)
  {
    this.entireSceneModels = ModelLoadUtilities.meshMerging.mergeMeshes (meshesForMerging,
        {
          "pickingId":
          {
            perFaceAttribute: true,
            itemSize: 1,

            defaultValue: function (mesh)
            {
              // we expect that in scene loading,  we have found all Object3D (= groups) with prefix "picking_" and to all its descendants,  we have saved userData.pickingId = some integer
              // (we have to convert the integer into float in [0, 1] because our picking algorithm works with floats)
              return (mesh.userData.pickingId ? Preferences.Picking.getPickingIdInRange0to1(mesh.userData.pickingId) : 0);
            }
          }
  });
  }
  else
  {
    if (Preferences.Optimizations.MATERIAL_DATA_REARRANGEMENT)
    {
      console.warn ("Material data rearrangement not performed - mesh merging must be turned on for this to work.");
    }

    if (Preferences.Optimizations.INTERLEAVED_BUFFER)
    {
      console.warn ("Interleaved buffer not created - mesh merging must be turned on for this to work.");
    }

    this.entireSceneModels.receiveShadow = true;
    this.entireSceneModels.castShadow = true;

    this.entireSceneModels.traverse(function(obj)
    {
      obj.receiveShadow = true;
      obj.castShadow = true;
    });
  }

  this.setVisibleSceneModels (this.entireSceneModels);
};

/**
 * Changes which models are visible in the scene. Used in preparation phases,  e.g. generating heightmaps.
 *
 * @param {THREE.Object3D}  modelsGroup   group of objects
 */
Scene.prototype.setVisibleSceneModels = function (modelsGroup)
{
  if (this._currentlyVisibleModels)
  {
    this.scene.remove (this._currentlyVisibleModels);
  }

  this.scene.add (modelsGroup);
  this._currentlyVisibleModels = modelsGroup;
};

/////////////////////////////////////////////////////////
// WATER IN THE FOUNTAIN
/////////////////////////////////////////////////////////

Scene.prototype.animateWater = function (delta)
{
	for(var i = 0; i < this.waterMaterials.length; i ++) {
		this.waterMaterials[i].map.offset.y += 0.5 * delta * 0.001;
	}
}

Scene.prototype.getWaterMaterials = function ()
{
    var waterModels = this.findGroupsWithPrefix (this.entireSceneModels,  Preferences.Prefixes.WATER);

    this.waterMaterials = [];

    if (typeof waterModels.children === 'undefined' && !waterModels.children.length) {
        return;
    }

    waterModels = waterModels.children;

    for (var i = 0; i < waterModels.length; i ++) {
        var material = waterModels[i].children[0].material;
        material.map.wrapS = THREE.RepeatWrapping;
        material.map.wrapT = THREE.RepeatWrapping;
        this.waterMaterials.push(material);
    }
}

/**
 * The most important method of this class,  which loads a scene from a single file.
 *
 * @param {string}          sceneFilename   file with the scene (DAE format)
 * @param {AssetJob|Asset}  parentJob       after the scene is loaded,  this job will be marked as finished
 */
Scene.prototype.loadScene = function (sceneFilename,  parentJob)
{
  var _this = this;

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

  this._initLights();

  /////////////////////////////////////////////////////////
  // SCENE FILE LOAD
  /////////////////////////////////////////////////////////

  // init Collada loader and load our scene
  // note: the Collada loader had to be modified a little,  for example,  Sketchup exports both front and back faces,  so we have to throw the back faces away in Collada.js
  var loader = new THREE.ColladaLoader();
  loader.options.convertUpAxis = true;

  loader.load (sceneFilename,  function (collada)
  {
    if (Preferences.Application.isDevMode())
    {
      console.log("scene models enter into function: " + Date.now());
    }

    // assign loaded scene into our variable
    this.entireSceneModels = collada.scene;
    this.entireSceneModels.updateMatrix();
    this.entireSceneModels.updateMatrixWorld(); // extremely important!

    this._assignPickingIds (this.entireSceneModels);

    this.getWaterMaterials();

    // we are generating heightmap -> just add imported models,  no optimizations performed
    if (Preferences.Heightmap.GENERATE_HEIGHTMAP)
    {
      this.hideInfoCubes(); // hide info cubes so they do not mess with out heightmap
      this.ceilingModels = this.findGroupsWithPrefix (this.entireSceneModels,  Preferences.Prefixes.HEIGHTMAP_CEILING_LAYER);
      this.floorModels = this.findGroupsWithPrefix (this.entireSceneModels,  Preferences.Prefixes.HEIGHTMAP_FLOOR_LAYER);
    }
    else
    {
        this.removeGroupsWithPrefix(this.entireSceneModels, Preferences.Prefixes.HEIGHTMAP_CEILING_LAYER);
        this.removeGroupsWithPrefix(this.entireSceneModels, Preferences.Prefixes.HEIGHTMAP_FLOOR_LAYER);
    }

    if (Preferences.Application.isDevMode())
    {
      console.log("scene models end loaded: " + Date.now());
    }

    // temporarily commented off
    parentJob.startJob();

  }.bind(this) );
};

Scene.prototype.addLight = function()
{
  if (!Util.isInitialized(this.grabbedLightPositions))
  {
    this.grabbedLightPositions = [];
  }

  this.grabbedLightPositions.push (player.getPosition().clone());
};

/**
 * Initializes all lights in the scene.
 *
 * @private
 */
Scene.prototype._initLights = function()
{
  var _this = this;

  function createPointLight(position, color, distance, decay)
  {
    var pointLight = new THREE.PointLight (color.getHex(), 1, distance ? distance : 120, decay ? decay : 3.25);
    pointLight.position.copy(position);

    _this.lights.add (pointLight);
  }

  for (var i = 0; i < PointsLightsConfig.length; i ++) {
    var light = PointsLightsConfig[i];
    var location = new THREE.Vector3 (light.location[0], light.location[1], light.location[2]);
    var color = new THREE.Color(0.884, 0.7974,0.1);
    createPointLight (location, color); // TODO - strength and decay
  }

};

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

/**
 * Finds all pickable groups (= meshes) in the objectGroup node and assigns them a picking id.
 * IMPORTANT: This method must be called before meshMerge() in order for picking to work.
 *
 * @private
 * @param {THREE.Object3D}  objectGroup   node where we will search for pickable meshes (they have prefix "pickable_" assigned in SketchUp)
 */
Scene.prototype._assignPickingIds = function (objectGroup)
{
  var _this = this;

  this.pickableModels = picking.assignPickingIds (objectGroup);
};

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

/**
 * Finds all object groups whose names start with given prefix
 *
 * @param   {THREE.Object3D}  objectGroup     node where we shall searching for groups with defined prefix
 * @param   {string}          prefix          we will search if object.name starts with this prefix
 * @returns {THREE.Object3D}                  node whose children are groups whose names start with prefix
 */
Scene.prototype.findGroupsWithPrefix = function (objectGroup, prefix)
{
  var envelopingNode = new THREE.Object3D();

  objectGroup.traverse (function (object)
  {
    if ((object instanceof THREE.Object3D) && (object.name.indexOf(prefix) === 0))
    {
      // note: using object.setPositionFromMatrix(matrixWorldOriginal) etc. was attempt to restore original position of object, but when we correctly use updateMatrixWorld() on scene, it is not needed
      //object.userData.matrixWorldOriginal = object.matrixWorld.clone();
      //object.userData.matrixOriginal = object.matrix.clone();
      //object.userData.parentOriginal = object.parent;

      // note: we cannot use standard Three.js .add(), because it changes parent of object and detaches it from current parent, which is bad from 2 reasons:
      //      1) It messes up the for cycle for traversing, because now one child (object) was taken away
      //      2) The child (object) is no longer in the hierarchy of scene
      // solution: we just push the object to children of envelopingNode
      envelopingNode.children.push (object);
    }
  });

  return envelopingNode;
};

/**
 * Finds all object groups whose names start with given prefix and removes them from the given object group.
 *
 * @param   {THREE.Object3D}  objectGroup     node where we shall searching for groups with defined prefix
 * @param   {string}          prefix          we will search if object.name starts with this prefix
 */
Scene.prototype.removeGroupsWithPrefix = function (objectGroup, prefix)
{
  var groupsToDelete = this.findGroupsWithPrefix (objectGroup, prefix);

  for (var i = 0; i < groupsToDelete.children.length; i++)
  {
    var objectToDelete = groupsToDelete.children[i];

    objectToDelete.parent.remove (objectToDelete);
  }
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// LIGHT MANIPULATION
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Turns on/off lights in the castle area.
 *
 * @param {boolean} visible   TRUE if light shall be turned on
 */
Scene.prototype.turnNightLights = function (visible)
{
  this.lights.visible = visible;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SPRITES
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Saves material which is used by some sprite in the scene to the list.
 *
 * @param {THREE.Material}  spriteMaterial    material that is used by some sprite
 */
Scene.prototype.saveSpriteMaterial = function (spriteMaterial)
{
  this.spriteMaterials.push (spriteMaterial);

  spriteMaterial.map.wrapS = THREE.ClampToEdgeWrapping;
  spriteMaterial.map.wrapT = THREE.ClampToEdgeWrapping;
  spriteMaterial.side = THREE.DoubleSide;

  // this fixes the problem where transparent parts of the sprite would actually won't allow to see-through, and also fixes rendering of one sprite behind other one
  spriteMaterial.alphaTest = 0.2;

  spriteMaterial["my_doNotIncludeInShadowmap"] = true;

  // !!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!!!!
  // in three.js, THREE.WebGLShadowMap, we must have added following line
  // "if (groupMaterial.my_doNotIncludeInShadowmap) continue;"
  // before the line
  // "_renderer.renderBufferDirect( shadowCamera, _lights, null, geometry, depthMaterial, object, group );"

  // to prevent sprites from rendering into the shadowmap (depth shader of three.js does not account for transparent parts)
};

/**
 * Changrs visibility of all materials which are used by some sprite.
 *
 * @param {boolean}   visible   desired visibility
 */
Scene.prototype.setSpriteVisibility = function (visible)
{
  for (var i = 0; i < this.spriteMaterials.length; i++)
  {
    this.spriteMaterials[i].visible = visible;
  }
};

/**
 * Hides all sprites in the scene.
 */
Scene.prototype.hideSprites = function ()
{
  this.setSpriteVisibility (false);
};

/**
 * Makes all sprites in the scene visible.
 */
Scene.prototype.showSprites = function ()
{
  this.setSpriteVisibility (true);
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MATERIALS WITH NORMAL/BUMP MAP
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Remembers that given material has normal or bump map applied.
 *
 * @param {THREE.Material}  normalAndBumpMaterial   material with normal or bump map
 * @param {boolean}         hasNormalMap            TRUE if material has normal map, FALSE if it has bump map
 */
Scene.prototype.saveNormalAndBumpMaterial = function (normalAndBumpMaterial, hasNormalMap)
{
  this.normalAndBumpMaterials.push (normalAndBumpMaterial);

  normalAndBumpMaterial["my_isNormalRatherThanBumpMap"] = hasNormalMap;

  if (hasNormalMap)
  {
    normalAndBumpMaterial["my_bumpOrNormalMap"] = normalAndBumpMaterial.normalMap;
  }
  else
  {
    normalAndBumpMaterial["my_bumpOrNormalMap"] = normalAndBumpMaterial.bumpMap;
  }
};

/**
 * Turns normal/bump mapping on or off.
 *
 * @param {boolean}   visible   TRUE if normal and bump mapping shall be turned on, FALSE if turned off
 */
Scene.prototype.setNormalAndBumpMapsVisibility = function (visible)
{
  for (var i = 0; i < this.normalAndBumpMaterials.length; i++)
  {
    var normalAndBumpMaterial = this.normalAndBumpMaterials[i];

    // TRUE if it uses normal maps
    if (normalAndBumpMaterial["my_isNormalRatherThanBumpMap"])
    {
      normalAndBumpMaterial.normalMap = visible ? normalAndBumpMaterial["my_bumpOrNormalMap"] : null;
    }
    // FALSE if bump maps
    else
    {
      normalAndBumpMaterial.bumpMap = visible ? normalAndBumpMaterial["my_bumpOrNormalMap"] : null;
    }
  }

  this.updateMaterials (this.normalAndBumpMaterials);
};

/**
 * Turns off normal/bump mapping feature of materials that have it.
 */
Scene.prototype.showNormalAndBumpMaps = function()
{
  this.setNormalAndBumpMapsVisibility (true);
};

/**
 * Turns on again the normal/bump mapping feature of materials that have it.
 */
Scene.prototype.hideNormalAndBumpMaps = function()
{
  this.setNormalAndBumpMapsVisibility (false);
};


////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// [DEV]
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * [DEV] Prints filenames of images of materials for given materials
 *
 * @param {THREE.Material[]}  [materialsToPrint]   materials that will be printed; default is all scene materials if no parameter is passed
 */
Scene.prototype.printMaterials = function (materialsToPrint)
{
  if (!Util.isInitialized (materialsToPrint))
  {
    var materialsToPrint = this.entireSceneModels.children[0].material.materials;
  }

  console.log ("Materials array: ");
  console.log (materialsToPrint);

  for (var i = 0; i < materialsToPrint.length; i++)
  {
    var hasMap = materialsToPrint[i].map;

    if (hasMap)
    {
      var imageFilename = materialsToPrint[i].map.image.src;
    }
    else
    {
      var imageFilename = "-- no map property assigned to the material --";
    }

    console.log ("Material " + i + " - image filename: " + imageFilename);
  }
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// MATERIAL UPDATE
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Recompiles shaders of all given and such. Must be called e.g. after changing shadow settings.
 *
 * @param {THREE.Material[]}  [materialsToUpdate]   materials that will be updated; default is all scene materials if no parameter is passed
 */
Scene.prototype.updateMaterials = function (materialsToUpdate)
{
  if (!Util.isInitialized (materialsToUpdate))
  {
    var materialsToUpdate = this.entireSceneModels.children[0].material.materials;
  }

  for (var i = 0; i < materialsToUpdate.length; i++)
  {
    materialsToUpdate[i].needsUpdate = true;
  }
};

Scene.prototype.addObject = function (name) {
    var geometry = new THREE.CubeGeometry(1, 1, 1);
    geometry.rotateX(0.785);
    geometry.rotateZ(0.785);
    var cube = new THREE.Mesh(geometry, new THREE.MeshNormalMaterial());
    cube.name = name;
    this.scene.add(cube);
    return cube;
};

Scene.prototype.updateOrAdd = function (name, position) {
	var obj = this.scene.getObjectByName(name);
	if (!obj) {
		obj = this.addObject(name);
	}
	if (position) {
		obj.position.x = position.x;
		obj.position.y = position.y + 1;
		obj.position.z = position.z;
	}

	obj.rotation.y += 0.002;
};

Scene.prototype.removeObject = function (name) {
	var obj = this.scene.getObjectByName(name);

	if (obj) {
		this.scene.remove(obj);
	}
};
