"use strict";

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

/**
 * Heightmap collision detection. Heightmap is generated from top-down view, orthographic projection.
 * Depth of pixels in framebuffer is stored as a color of each pixel (= distance to the camera).
 * This depth is then used to compute world height (in "meters") for current position.Should not be called directly,
 * only via factory methods.
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @param         {Map}                      map                  used for computing coordinates into the heightmap from 3D position of the player
 * @param         {string}                   name                 name of the heighmap layer, e.g. "layer_ground" or something like that
 * @param         {WorldHeightmapProperties} heightmapProperties  properties for the heightmap (camera etc.)
 * @constructor
 */
var Heightmap = function (map, name, heightmapProperties)
{

  this.name = name;

  /**
   * Underlying map object (sed for sample coordinates computation).
   *
   * @type {Map}
   */
  this.map = map;

  /**
   * Array with sample values of the heightmap.
   *
   * @type {Uint8Array}
   */
  this.valueMap = null;

  /////////////////////////////////////////////////////
  // HEIGHTMAP PROPERTIES - COLLISION DETECTION
  /////////////////////////////////////////////////////

  /**
   *
   * @type {WorldHeightmapProperties}
   */
  this.heightmapProperties = heightmapProperties;

  /////////////////////////////////////////////////////
  // TEMPORARIES
  /////////////////////////////////////////////////////

  this._tmp_pixelColor = null;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FACTORY METHODS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * [DEV] Draws world in top-down ortographic projection, color of pixels will represent depth (distance to camera).
 * Result framebuffer will be saved and used as a heightmap.
 *
 * @param   {Map}                       map                   object used for projecting coordinates etc.
 * @param   {string}                    name                  name of the layer (just for information purposes)
 * @param   {WorldHeightmapProperties}  heightmapProperties
 * @returns {Heightmap}
 */
Heightmap.createFromWorld = function (map, name, heightmapProperties)
{
  var heightmap = new Heightmap (map, name, heightmapProperties);

  heightmap.valueMap = new Uint8Array (heightmap.map.width * heightmap.map.height * 4);

  return heightmap;
};

/**
 * Loads an image from file, draws it onto auxiliary 2D canvas and retrieves framebuffer of the canvas (pixel data)
 * in the WebGL coordinate format.
 *
 * @param   {string}                    imageFilename         image with heightmap values
 * @param   {Map}                       map                   underlying map which will be sued by the heightmap
 * @param   {string}                    name                  name of the layer (just for information purposes)
 * @param   {WorldHeightmapProperties}  heightmapProperties   used for correct behavior initialization of the heightmap
 * @param   {Asset}                     mapCreation           asset with job which is responsible for loading heightmap layers
 * @returns {Heightmap}                                       heightmap object with values initialized from the image
 */
Heightmap.createFromImage = function (imageFilename, map, name, heightmapProperties, mapCreation)
{
  var heightmap = new Heightmap (map, name, heightmapProperties);

  var assetHeightmapImageLoad = assetLoader.loadImageRawData (imageFilename, heightmap, "valueMap");

  mapCreation.waitFor (assetHeightmapImageLoad);
  assetHeightmapImageLoad.startJob();

  return heightmap;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// HEIGHT COMPUTATIONS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Computes world height for supplied entity position (2D screen coordinates which are in WebGL coordinate
 * system, i.e. [0,0] in bottom-left corner).
 * (Note: the transformation of 3D player position into these 2D screen coordinates happens elsewhere).
 *
 * @param   {number} glScreenX    integer X screen coordinates
 * @param   {number} glScreenY    integer Y screen coordinates (WebGL coordinate system!)
 * @param   {Entity} entity
 * @returns {number}              world height (in "meters") for position (glScreenX, glScreenY) in the heightmap
 */
Heightmap.prototype.getWorldHeightAt = function (glScreenX, glScreenY, entity)
{
  // note: glScreen shall be an integer!
  this._tmp_pixelColor = Renderer.readPixelColor (this.valueMap, this.map.width, this.map.height, glScreenX, glScreenY); // use GL coordinates, not image!!!

  return this.getWorldHeightForColor (this._tmp_pixelColor, entity);
};

/**
 * Computes world height for given entity from pixel color (where it is encoded into RGB color portions).
 *
 * @param   {number[]}   pixelColor    color of the pixel in the heightmap
 * @param   {Entity}                        entity
 * @returns {number}                                      world height (in "meters")
 */
Heightmap.prototype.getWorldHeightForColor = function (pixelColor, entity)
{
  var depth = renderer.unpackDepth24 (pixelColor);

  return this.getWorldHeightForDepth (depth);
};

/**
 * Computes world height of entity for given depth value. This height corresponds to height (i.e. Y coordinates value)
 * of models when inspected in SketchUp or similar modeling program.
 *
 * @param   {number}  depth   depth value fetched from the heightmap
 * @returns {number}          world height (in "meters")
 */
Heightmap.prototype.getWorldHeightForDepth = function (depth)
{
  var distanceInBox = depth * this.heightmapProperties.nearToFarDistance;
  var worldHeight = this.heightmapProperties.cameraNearPosition - distanceInBox;

  // this happens when we get depth = 0 (i.e. black color in the heightmap), it is the highest possible value in map -- bad for our collision system,
  // we need the behavior where black color (i.e. depth = 0) is actually the smallest possible value, otherwise we would get stuck in collisions when we get to the area where is black color
  if (Math.abs (worldHeight - this.heightmapProperties.cameraNearPosition) <= 0.1)
  {
    return this.getWorldHeightForDepth (1.0);
  }

  return worldHeight;
};

/**
 * Retrieves altitude (height) for the given entity at its current position.
 *
 * @param   {Entity}  entity
 * @returns {number}          height in the world at the player current position
 */
Heightmap.prototype.getWorldHeightAtEntity = function (entity)
{
  entity.computeHeightmapCoordinates();

  return this.getWorldHeightAt (entity.heightmapCoord.x, entity.heightmapCoord.y, entity);
};

/**
 * Shortcut function for calling averageFrom9samples() with current entity position.
 *
 * @param   {Entity}  entity
 * @returns {number}          averaged height in the world at the player current position
 */
Heightmap.prototype.getAveragedWorldHeightAtEntity = function (entity)
{
  entity.computeHeightmapCoordinates();

  return this.averageFrom9samples (entity.heightmapCoord.x, entity.heightmapCoord.y, entity, this.getWorldHeightAt.bind(this));
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// SAMPLE AVERAGING
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Makes mean average for sample at (centerSampleX, centerSampleY) from all samples around it (including that sample).
 *
 * @param   {number}    centerSampleX             X position of the sample in the heightmap
 * @param   {number}    centerSampleY             Y position of the sample in the heightmap
 * @param   {Entity}    entity
 * @param   {function}  sampleRetrievalFunction   function used for computing the sample value
 * @returns {number}                              mean average for the sample
 */
Heightmap.prototype.averageFrom9samples = function (centerSampleX, centerSampleY, entity, sampleRetrievalFunction)
{
  var sampleValue = 0;

  // |Note:
  // |There should be check for accessing samples outside of the grid. However, this situation won't happen in the application,
  // |since accessible area does not reach borders of the map. Thus I haven't bothered with it.
  // |And it is also faster like this, obviously.

  sampleValue += sampleRetrievalFunction (centerSampleX-1, centerSampleY+1, entity);
  sampleValue += sampleRetrievalFunction (centerSampleX,   centerSampleY+1, entity);
  sampleValue += sampleRetrievalFunction (centerSampleX+1, centerSampleY+1, entity);

  sampleValue += sampleRetrievalFunction (centerSampleX-1, centerSampleY, entity);
  sampleValue += 2*sampleRetrievalFunction (centerSampleX,   centerSampleY, entity);
  sampleValue += sampleRetrievalFunction (centerSampleX+1, centerSampleY, entity);

  sampleValue += sampleRetrievalFunction (centerSampleX-1, centerSampleY-1, entity);
  sampleValue += sampleRetrievalFunction (centerSampleX,   centerSampleY-1, entity);
  sampleValue += sampleRetrievalFunction (centerSampleX+1, centerSampleY-1, entity);

  sampleValue /= 9;

  return sampleValue;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// COLOR CHECKING
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Checks if color is background color of the heightmap.
 *
 * @param   {number[]} color   tested color
 * @returns {boolean}          TRUE if color is heightmap background color
 */
Heightmap.isColorBackground = function (color)
{
  if ((color[0] !== Preferences.Heightmap.BACKGROUND_COLOR_RGB.x) ||
      (color[1] !== Preferences.Heightmap.BACKGROUND_COLOR_RGB.y) ||
      (color[2] !== Preferences.Heightmap.BACKGROUND_COLOR_RGB.z))
  {
    return false;
  }

  return true;
};

/**
 * Checks if current sample at the current entity position in the heightmap has background color.
 *
 * @param   {Entity}    entity
 * @returns {boolean}   TRUE if player stands on the background color in the heightmap
 */
Heightmap.prototype.isPlayerOnBackgroundColor = function (entity)
{
  var colorAtPlayer = Renderer.readPixelColor (this.valueMap, this.map.width, this.map.height, entity.heightmapCoord.x, entity.heightmapCoord.y);

  return Heightmap.isColorBackground (colorAtPlayer);
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// COMMENTS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// |Note: WebGL vs Screen/Canvas coordinate system
// |WebGL coordinate system starts has [0,0] at the bottom-left corner of the image X Screen has [0,0] at top-left corner.
// |Map.computeScreenCoordinates() will return WebGL coordinates.
//
// Situation A: Heightmap is generated automatically via WebGL.
// - Heightmap image is framebuffer which was drawn on the screen. When saved, pixels are stored in WebGL style.
// - Possible to look into with WebGL coordinates right away.
// - When inspecting the image etc., beware of the coordinate system! When heightmap image is opened in an image program,
//   it will show coordinates in the Canvas coordinate system.
// - Thus, if player is e.g. at WebGL [485,567], which is right in front of the tunnel from 3rd Courtyard to 2nd Courtyard,
//   then this position is in the image program displayed at the Canvas [485,windowHeight-567],
//   which will be [485,393] when window (and heightmap) has resolution of 1280x960.
// - We can also flip the heightmap image vertically in the image program and then WebGL coordinates correspond to the
//   displayed image coordinates.
//
// Situation B: Heightmap is loaded from file via Canvas2D.
// - Tricky!
// - Renderer.getImageData() will first flip the canvas image vertically
// - Then we have pixels in same format as in situation A.

// ASCII Art for encoding depth into pixels:
// worldY
// 200  |          Camera        (world position: 0, 200, 0)(near: 1, far: 600)
//      |          Camera's near (world position: 0, 199, 0)
// 100  |
//      |
//   0  | pixelAtGroundLevel (world position Y: 0 -> depth 0.33)
//      |
// -100 | pixelHalfway (world position Y: -100 -> depth 0.5)
//      |
// -200 |
//      |
// -300 |
//      |
// -400 | pixelFar (world position Y: -400 -> depth 1.0)
//
// Obviously, you see, the important factor is camera's Y position. From that, we compute where camera near plane is.
// The depth of pixel is encoded in context of camera frustum distance from near to far, which is NOT 600, it is 600-1 = 599. For demonstration purposes in above ASCII, we treat the frustum distance as 600, but it shall be always far-near.
// And, well, depth of pixel is just [0,1} which tells us how how far from near it is. So we just do depth * (far-near) and we know where exactly the pixel is.

// Some other notes:
// maybe manual correction of heightmap would be good - we need to make walls "pop more into space" so the player won't be too close (otherwise, he occasionally sees through the walls)
// averaging height for pixel with 8 neighborhood pixels counters this issue (due to the average, player isn't allowed to be so close to the wall), but it may introduce new problems :)
