"use strict";

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

/**
 * Container holding information about world heightmap - dimensions and camera which was used to capture it.
 * This information is vital for collision detection.
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @param         {number}                    width     width of heightmaps
 * @param         {number}                    height    height of heightmaps
 * @param         {THREE.OrthographicCamera}  camera    camera used to capture the heightmap
 * @constructor
 */
var WorldHeightmapProperties = function (width, height, camera)
{
  /**
   * Width of heightmaps.
   *
   * @type {number}
   */
  this.width  = width;

  /**
   * Height of heightmaps.
   *
   * @type {number}
   */
  this.height = height;

  /**
   * Matrix used for capturing the heightmap (rendered has rendered the scene using this matrix, model matrix was identity).
   *
   * @type {THREE.Matrix4}
   */
  this.projectionViewMatrix = new THREE.Matrix4();
  this.projectionViewMatrix.multiplyMatrices (camera.projectionMatrix, camera.matrixWorldInverse);

  /**
   * Position of the camera which was used for capturing the scene.
   *
   * @type {THREE.Vector3}
   */
  this.cameraPosition = camera.position.clone();

  /**
   * Position of the camera's near plane which was used for capturing the scene.
   *
   * @type {THREE.Vector3}
   */
  this.cameraNearPosition = camera.position.y - camera.near;

  /**
   * Difference between camera's near and far.
   *
   * @type {THREE.Vector3}
   */
  this.nearToFarDistance = camera.far - camera.near;
};

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

/**
 * Collision detection using heightmaps. Contains 3 layers of heightmaps. Main heightmap is generated from top-down view, orthographic projection.
 * Color of pixel = encoded distance to the camera. It is distance from the pixel's world position
 * (or rather the vertex which projected to pixel) to the camera center. The distance is expressed in the terms of distance between near and far camera plane.
 * For example, pixel right in front of the camera will have depth of 0.03, and pixel almost at the far plane will have depth of 0.99.
 * See bottom of the Heightmap.js for ASCII art.
 *
 * This encoded distance is used to compute world height (in "meters") for current entity 3D position.
 * The other two layers are auxiliary and they store ground and ceiling level of passageways (e.g. tunnel
 * between II.Courtyard and III.Courtyard).
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @param     {Map}                       underlyingMap             contains dimensions of the heightmaps and projection-view matrix used to capture all 3 heightmap layers
 * @param     {WorldHeightmapProperties}  worldHeightmapProperties
 * @constructor
 */

var WorldHeightmap = function (underlyingMap, worldHeightmapProperties)
{
  /**
   * Information about the heightmap.
   *
   * @type {WorldHeightmapProperties}
   */
  this.worldHeightmapProperties = worldHeightmapProperties;

  ///////////////////////////////////////////////////////
  // UNDERLYING MAP OBJECT
  ///////////////////////////////////////////////////////

  /**
   * All heightmap layers share this very same underlying map object.
   *
   * @type {Map}
   */
  this.map = underlyingMap;

  ///////////////////////////////////////////////////////
  // 3 HEIGHTMAP LAYERS
  ///////////////////////////////////////////////////////

  /**
   * Primary heightmap ("Largest Height layer" in thesis). Used for computing collisions when not in a passageway, i.e.
   * in usual case.
   *
   * @type {Heightmap}
   */
  this.heightmap = null;

  /**
   * Passageway area -- ceiling height layer. Used for restricting movement inside passageways from below.
   *
   * @type {Heightmap}
   */
  this.heightmapTunnelFloor = null;

  /**
   * Passageway area -- floor height layer. Used for restricting movement inside passageways from above.
   * @type {Heightmap}
   */
  this.heightmapTunnelCeiling = null;

  ///////////////////////////////////////////////////////
  // TEMPORARY VARIABLES FOR COMPUTATIONS
  ///////////////////////////////////////////////////////

  /**
   * Maximum allowed altitude for the entity.
   *
   * @type {number}
   * @private
   */
  this._aboveLimit = null;

  /**
   * Height computed at current position of the entity.
   *
   * @type {number}
   * @private
   */
  this._currentHeight = null;

  /**
   * Difference between current position's height and entity height in last frame.
   *
   * @type {number}
   * @private
   */
  this._heightDiff = null;

  /**
   * Difference between heightmap value of obstacle (e.g. roof) and player height.
   *
   * It will has either value of some very high number, or something like 0.2, 1.6, 18.6 etc.
   *
   * The variable is not being updated everytime when we are in fly mode, only sometimes (when collision occurs).
   * We need it so we can detect if the obstacle is not much higher and we shall shift player a little upwards.
   * This allows us to slide along roofs even in fly mode.
   *
   * @type {number}
   */
  this.flyModeHeightDiff = Preferences.Collision.COLLISION_FLY_MODE_HEIGHT_DIFF_TOO_MUCH_CONSTANT;
};

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


/**
 * Loads all heightmap layers of the world heightmap.
 *
 * @returns {Asset|AssetJob}    job that makes sure that everything related to heightmap is loaded
 */
WorldHeightmap.createMap = function()
{
  ////////////////////////////////////////////////////////
  // WORLD MAP PROPERTIES INIT
  ////////////////////////////////////////////////////////

  var worldHeightmapProperties;
  if (Preferences.Heightmap.GENERATE_HEIGHTMAP)
  {
    // create world heightmap with same parameters as in default, but with resolution equal to the current window size
    worldHeightmapProperties = new WorldHeightmapProperties
    (
        renderer.getRendererThree().domElement.width,
        renderer.getRendererThree().domElement.height,
        Preferences.Heightmap.ORTHO_CAMERA
    );
  }
  else
  {
    worldHeightmapProperties = Preferences.Heightmap.WORLD_HEIGHTMAP_DEFAULT_PROPERTIES;
  }

  var worldMap = new WorldHeightmap ( new Map (worldHeightmapProperties.width, worldHeightmapProperties.height, worldHeightmapProperties.projectionViewMatrix), worldHeightmapProperties );

  // assign to global variable :)
  heightmapWorld = worldMap;

  ////////////////////////////////////////////////////////
  // JOBS
  ////////////////////////////////////////////////////////

  // finished when everything related to world heightmap is completed
  var everythingCompleted = assetLoader.makeDummyJob ("world heightmap generation (heightmap layers creation, initial collision, i.e. everything)", false);

  // finished when all heightmap layers are created
  var allHeightmapLayersCreation = assetLoader.makeDummyJob ("worldmap creation", false, function()
  {

  });

  // computing initial collision, so we know where to put the player
  var initialCollision = assetLoader.makeDummyJob ("world heightmap initial collision, player ground map init", false, function()
  {
    // no need to compute initial collision, it will be handled by changing to initial viewpoint
    //worldMap.computeCollisionFirstTime (player);

    player.currentGroundMap = worldMap.getMain();
  });
  initialCollision.waitFor (allHeightmapLayersCreation);
  everythingCompleted.waitFor (initialCollision);

  ////////////////////////////////////////////////////////
  // GENERATING A LAYER OF HEIGHTMAP
  ////////////////////////////////////////////////////////

  var generateHeightmapLayer = function (heightmapLayer)
  {
    var layerVariableName = "";

    var parsedLayerFileName;

    switch (heightmapLayer)
    {
      case Preferences.Heightmap.LayerType.LARGEST_VALUE:
        layerVariableName = "heightmap";

        // parse from filepaths saved in Preferences.js
        parsedLayerFileName = Preferences.Heightmap.LARGEST_VALUE_LAYER_FILENAME.split("/");
        break;

      case Preferences.Heightmap.LayerType.FLOOR:
        layerVariableName = "heightmapTunnelFloor";

        parsedLayerFileName = Preferences.Heightmap.FLOOR_LAYER_FILENAME.split("/");
        break;

      case Preferences.Heightmap.LayerType.CEILING:
        layerVariableName = "heightmapTunnelCeiling";

        parsedLayerFileName = Preferences.Heightmap.CEILING_LAYER_FILENAME.split("/");
        break;
    }

    // we want just the file name, not the directories before, so grab the last chunk of split
    var layerFileName = parsedLayerFileName[parsedLayerFileName.length-1];

    var layerCreation = assetLoader.makeDummyJob("heightmap layer '" + layerVariableName + "' creation", false, function()
    {
      layerCreation.waitForFinish (heightmapSnapshot);
      layerCreation.waitForFinish (heightmapSnapshotSuccessfull);

      // this is important, because nobody can start the heightmapSnapshot - layerCreation waits for it, but using .waitForFinish() so it cannot start it, we must do it manually
      heightmapSnapshot.startJob();
    });

    // this is job which is marked as finished when the scene (= heightmap) is successfully rendered and framebuffer is filled with colors
    var heightmapSnapshotSuccessfull = assetLoader.makeExternalLoadingJob(layerVariableName + " snapshot successful detection");
    var heightmapSnapshotSuccessfull_retryCount = 0;

    var heightmapSnapshot = assetLoader.makeDummyJob ("heightmap snapshot trying to generate", false, function()
    {
      if (Preferences.Application.isDevMode())
      {
        console.log(layerVariableName + ": Entering heightmapSnapshot at: " + Date.now());
      }

      switch (heightmapLayer)
      {
        case Preferences.Heightmap.LayerType.LARGEST_VALUE:
          if (Preferences.Application.isDevMode())
          {
            console.log(layerVariableName + ": chaning models to entireSceneModels at " + Date.now());
          }
          castleScene.setVisibleSceneModels (castleScene.entireSceneModels);
          break;

        case Preferences.Heightmap.LayerType.FLOOR:
            if (Preferences.Application.isDevMode())
            {
              console.log(layerVariableName + ": chaning models to floorModels at " + Date.now());
            }
          castleScene.setVisibleSceneModels (castleScene.floorModels);
          break;

        case Preferences.Heightmap.LayerType.CEILING:
              if (Preferences.Application.isDevMode())
              {
                console.log(layerVariableName + ": chaning models to ceilingModels at " + Date.now());
              }
          castleScene.setVisibleSceneModels (castleScene.ceilingModels);
          break;
      }

      var heightmap = Heightmap.createFromWorld (worldMap.map, Preferences.Heightmap.LayerNames[heightmapLayer], worldHeightmapProperties);
      worldMap[layerVariableName] = heightmap;

      // The problem: although scene SHALL be ready by now, there is a little delay of a few ms, probably caused by scene.add() function of Three.js and if we try to render the scene now,
      //              it can happen that nothing will be rendered. Solution is to wait a little, but we don't know how long, so we will periodically try to make snapshots of screen
      //              and check some "significant" pixels if they contain some values. If some of them do, it probably means that scene was rendered successfully and framebuffer contains pixel values, which is what we need.
      var heightmapSnapshotTrying = setInterval (function()
      {
        if (Preferences.Application.isDevMode())
        {
          console.log(layerVariableName + ": heightmapSnapshotTrying at: " + Date.now());
        }

        ////////////////////////////////////////////////////////////////
        // PIXEL CHECK FOR SUCCESSFUL RENDER
        ////////////////////////////////////////////////////////////////

        var snapshotNotEmpty = false;

        if (Preferences.Heightmap.TEST_FOR_HEIGHTMAP_SUCCESSFULL_SNAPSHOT)
        {
          // check color values of a few pixels

          // pixel positions in NDC (WebGL coordinate system, origin is bottom-left corner, X increases ti right, Y increases to up)
          var pixelPositions =
              [
                // main image diagonal (top-left to bottom-right corner)
                [0.98, 0.98],
                [0.75, 0.75],
                [0.50, 0.50],
                [0.25, 0.30],
                [0.02, 0.02],

                // auxiliary image diagonal
                [0.05, 0.90],
                [0.25, 0.70],
                [0.70, 0.30],
                [0.95, 0.02],

                // misc
                [0.2, 0.8],
                [0.25, 0.44]
              ];

          for (var p = 0; p < pixelPositions.length; p++)
          {
            if (Renderer.isPixelDefined
            ( Renderer.readPixelColor(heightmap.valueMap, heightmap.map.width, heightmap.map.height, Math.floor(heightmap.map.width*pixelPositions[p][0]), Math.floor(heightmap.map.height*pixelPositions[p][1])) )
                )
            {
              snapshotNotEmpty = true;
              break;
            }
          }
        }
        ////////////////////////////////////////////////////////////////

        // ok, scene was probably successfully rendered, we can stop this interval and mark heightmap snapshot job as finished by calling .startJob()
        if ((heightmapSnapshotSuccessfull_retryCount > 0 && snapshotNotEmpty) || !Preferences.Heightmap.TEST_FOR_HEIGHTMAP_SUCCESSFULL_SNAPSHOT)
        {
          clearInterval (heightmapSnapshotTrying);

          if (Preferences.Application.isDevMode())
          {
            console.log(layerVariableName + ": snapshotNotEmpty at (retry cnt="+heightmapSnapshotSuccessfull_retryCount+"): " + Date.now());
          }

          if (Preferences.Heightmap.EXPORT_HEIGHTMAP)
          {
            //renderer.makeScreenshot();

            download (canvas.toDataURL(), layerFileName, "image/png");
          }

          if (Preferences.Application.isDevMode())
          {
            console.log(layerVariableName + ": Will mark heightmapSnapshotSuccessfull: " + Date.now());
          }
          heightmapSnapshotSuccessfull.startJob();
        }
        // scene was probably not successfully rendered, so try again
        else
        {
          // note: watch out, if there are too little models in the scene, we may have missed the colored pixels in our test and the image is in fact rendered OK! (this happens during development, most of the time)

          if (Preferences.Application.isDevMode())
          {
            console.log(layerVariableName + ": try again at " + Date.now());
          }

          // if we have not yet reached retry limit, render the scene again
          if (heightmapSnapshotSuccessfull_retryCount++ <= Preferences.Heightmap.RETRY_COUNT_MAX_LIMIT_FOR_GENERATION)
          {
            renderer.renderHeightmap (heightmap);
          }
          // reach maximum limit of retries
          else
          {
            clearInterval (heightmapSnapshotTrying);

            if (Preferences.Heightmap.TERMINATE_ON_FAILED_SNAPSHOT)
            {
              throw new Error ("Reached maximum number of retries for taking snapshot of the scene for heightmap generation (" + layerVariableName + "). Try to look at the canvas image, if there is nothing, there is really some problem with rendering. If there is something, the problem is just in function which is testing canvas for emptiness.");
            }
            else
            {
              // (this is just copy of the code in some branch above):
              if (Preferences.Heightmap.EXPORT_HEIGHTMAP)
              {
                download (canvas.toDataURL(), layerFileName, "image/png");
              }

              heightmapSnapshotSuccessfull.startJob();
            }

          }
        }
      }, Preferences.Heightmap.RETRY_DELAY_FOR_GENERATION_MS);

    }.bind (this));

    return layerCreation;
  }.bind (this);

  ////////////////////////////////////////////////////////
  // GENERATING/LOADING HEIGHTMAP LAYERS
  ////////////////////////////////////////////////////////

  if (Preferences.Heightmap.GENERATE_HEIGHTMAP)
  {
    console.warn ("Heightmap will be generated on-fly. Please note that this may take several seconds.");

    var largestValueLayerCreation = generateHeightmapLayer (Preferences.Heightmap.LayerType.LARGEST_VALUE);
    var floorLayerCreation        = generateHeightmapLayer (Preferences.Heightmap.LayerType.FLOOR);
    var ceilingLayerCreation      = generateHeightmapLayer (Preferences.Heightmap.LayerType.CEILING);

    allHeightmapLayersCreation.waitFor (largestValueLayerCreation);
    allHeightmapLayersCreation.waitFor (floorLayerCreation);
    allHeightmapLayersCreation.waitFor (ceilingLayerCreation);

    floorLayerCreation.waitFor     (largestValueLayerCreation);
    ceilingLayerCreation.waitFor   (floorLayerCreation);
  }
  else
  {
    worldMap.heightmap              = Heightmap.createFromImage (Preferences.Heightmap.LARGEST_VALUE_LAYER_FILENAME, worldMap.map, Preferences.Heightmap.LayerNames[Preferences.Heightmap.LayerType.LARGEST_VALUE], worldHeightmapProperties, allHeightmapLayersCreation);
    worldMap.heightmapTunnelFloor  = Heightmap.createFromImage (Preferences.Heightmap.FLOOR_LAYER_FILENAME,         worldMap.map, Preferences.Heightmap.LayerNames[Preferences.Heightmap.LayerType.FLOOR],         worldHeightmapProperties, allHeightmapLayersCreation);
    worldMap.heightmapTunnelCeiling = Heightmap.createFromImage (Preferences.Heightmap.CEILING_LAYER_FILENAME,       worldMap.map, Preferences.Heightmap.LayerNames[Preferences.Heightmap.LayerType.CEILING],       worldHeightmapProperties, allHeightmapLayersCreation);
  }

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

  return everythingCompleted;
};

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

/**
 * [DEV] Registers onmousemover handler which displays depth of pixel (which is currently under the mouse) in the heightmap.
 *
 * @private
 */
WorldHeightmap._registerExplorer = function()
{
  var explorerDiv = $('<div style="position: absolute; cursor: default; font-family: Verdana; background-color: blue; font-size: 20px; color: white; z-index: 999999"></div>');
  $("body").append(explorerDiv);

  $(document).mousemove(function(event)
  {
    var res = heightmapWorld.testDepth (Renderer.readPixelColor(heightmapWorld.heightmap.valueMap, heightmapWorld.map.width, heightmapWorld.map.height, event.clientX, heightmapWorld.map.height - event.clientY));

    explorerDiv.html("" + res.toFixed (5) + "(" + event.clientX + "," + (heightmapWorld.map.height - event.clientY) + ")");
    explorerDiv.css("left", event.clientX + 10);
    explorerDiv.css("top", event.clientY);
  });
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// HEIGHTMAP LAYERS RETRIEVAL
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Returns main heightmap of the world - the one which was captured from orthographic view high above the world,
 * where each pixel represents height (depth) of the place.
 *
 * @returns {Heightmap}
 */
WorldHeightmap.prototype.getMain = function()
{
  return this.heightmap;
};

/**
 * Returns heightmap layer where ground levels of "tunnels" are saved.
 *
 * @returns {Heightmap}
 */
WorldHeightmap.prototype.getTunnelGround = function()
{
  return this.heightmapTunnelFloor;
};

/**
 * Returns heightmap layer where ceiling levels of "tunnels" are saved.
 *
 * @returns {Heightmap}
 */
WorldHeightmap.prototype.getTunnelCeiling = function()
{
  return this.heightmapTunnelCeiling;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// COLLISION DETECTION
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

var dbg_wasInTunnel = false;

/**
 * Method which is used to invoke internal computation for detecting collisions between the entity and the world.
 * This method is necessary, because internal collision computation changes the ground heightmap variable which affects
 * other methods. When collision occurs, we must put the application to the exactly same state as it was before we
 * tried to move the player -> we have to restore original value of the ground heightmap variable.
 *
 * @param   {Entity}    entity
 * @returns {boolean}   TRUE if collision occurred
 */
WorldHeightmap.prototype.computeCollision = function (entity, doNotAlterState)
{
  var result;
  // (has been computed earlier)
  //this.map.computeScreenCoordinates (entity.getPosition(), entity.heightmapCoord);

  // if we cannot alter state, remember current variables so we can later set them to these values
  if (doNotAlterState)
  {
    var _aboveLimit__backup = this._aboveLimit;
    var _currentHeight__backup = this._currentHeight;
    var entityCurrentGroundMap__backup = entity.currentGroundMap;
    var playerInTunnel__backup = player.inTunnel;
    var _heightDiff__backup = this._heightDiff;
  }

  var originalGroundMap = entity.currentGroundMap;

  if (this._computeCollision (entity, doNotAlterState))
  {
    if (!doNotAlterState) // ok, alter state
    {
      entity.currentGroundMap = originalGroundMap;
    }

    result = true;
  }
  else
  {
    result = false;
  }

  // restore variables to their original values if we are not allowed to alter state
  if (doNotAlterState)
  {
    this._aboveLimit = _aboveLimit__backup;
    this._currentHeight  = _currentHeight__backup;
    entity.currentGroundMap  = entityCurrentGroundMap__backup;
    player.inTunnel  = playerInTunnel__backup;
    this._heightDiff  = _heightDiff__backup;
  }

  return result;

};

/**
 * Checks if the entity is in the tunnel (passageway).
 *
 * @param   {Entity}  entity
 * @returns {boolean}         TRUE if entity is in a passageway
 */
WorldHeightmap.prototype.isInTunnel = function (entity)
{
  //  background color in the ground heightmap = no tunnel
  dbg_wasInTunnel = !(this.getTunnelGround().isPlayerOnBackgroundColor (entity));

  return dbg_wasInTunnel;
  //return !(this.getTunnelGround().isPlayerOnBackgroundColor (entity));
};

/**
 * IMPORTANT METHOD. Computes collisions between the entity and ground, walls, ceilings. This method should never be called directly,
 * only via computeCollision().
 *
 * @param   {Entity}    entity
 * @param   {boolean}   [doNotAlterState=false]         TRUE if function shall not modify properties of entity and other objects
 * @returns {boolean}                                   TRUE if collision has occurred
 * @private
 */
WorldHeightmap.prototype._computeCollision = function (entity, doNotAlterState)
{
  entity.computeHeightmapCoordinates (true);

  /////////////////////////////
  // NORMAL AREA
  /////////////////////////////
  if (!this.isInTunnel(entity))
  {
    player.inTunnel = false;

    this._aboveLimit = Preferences.Heightmap.MAXIMUM_ALLOWABLE_HEIGHT;
    this._currentHeight = this.getMain().getWorldHeightAtEntity (entity);
    entity.currentGroundMap = this.getMain();
  }
  /////////////////////////////
  // TUNNEL AREA
  /////////////////////////////
  else
  {
    player.inTunnel = true;

    this._aboveLimit = this.getTunnelCeiling().getWorldHeightAtEntity (entity);

    // this is kind of a quick fix - when we have not precisely set up our models (ceiling_ tags in SketchUp etc.) and we get something like -149 ceiling height when in tunnel, this makes it 149, which is still wrong but at least it doesnt mess it up whole
    if (this._aboveLimit < -heightmapWorld.heightmap.heightmapProperties.cameraNearPosition) // (it does not matter which heightmap's properties we take, all are same)
    {
      // quick fix for Matthias Gate from the 2nd courtyard (we shall generate a new heightmap and correctly mark ceilings in it, but no time..)
      if (new THREE.Vector3( -109.57750116637281, 4.790284491993863, 66.86401804900031 ).distanceToSquared(player.getPosition()) <= 150)
      {
        this._aboveLimit = 3.25;
      }
      // not close to the Matthias gate
      else
      {
        this._aboveLimit = heightmapWorld.heightmap.heightmapProperties.cameraNearPosition;
      }
    }

    // inside the tunnel (i.e. player is below the tunnel ceiling)
    if (entity.getY() + Preferences.Camera.PLAYER_HEAD_ABOVE_CAMERA_METERS < this._aboveLimit)
    {
      this._currentHeight = this.getTunnelGround().getWorldHeightAtEntity (entity);
      entity.currentGroundMap = this.getTunnelGround();
    }
    // above the tunnel
    else
    {
      // (same as in case of NORMAL AREA)
      this._aboveLimit = Preferences.Heightmap.MAXIMUM_ALLOWABLE_HEIGHT;
      this._currentHeight = this.getMain().getAveragedWorldHeightAtEntity (entity);
      entity.currentGroundMap = this.getMain();
    }
  }

  // ----------------------------------------------------
  // Standard WALK mode
  // ----------------------------------------------------
  if (entity.inWalkMode())
  {
    // *******************************************************************************************
    // AIR
    // *******************************************************************************************
    if (entity.inAir())
    {
      // don't allow player to fall below the ground
      if (entity.getY() <= entity.currentGroundMap.getWorldHeightAtEntity (entity))
      {
        return true;
      }

      // (improvement: also incorporate height of the avatar?)

      // don't allow player to pass up through the ceiling
      // (will not happen likely, because avatar cannot jump)
      if (entity.getY() >= this._aboveLimit)
      {
        return true;
      }

      // prevents holding onto walls during fall (spiderman bug)
      if (entity.currentGroundMap.getWorldHeightAtEntity (entity) >= entity.getY())
      {
        return true;
      }
    }
    // *******************************************************************************************
    // GROUND
    // *******************************************************************************************
    else
    {
      this._heightDiff = this._currentHeight - entity.lastHeightmapHeight;

      /////////////////////////////////////////////////
      // CHECK FALLING AND BUMPING INTO THE WALLS
      /////////////////////////////////////////////////

      // player should be falling if new position (currentHeight) is a lot more smaller than old position (lastHeightmapHeight)
      // (example of falling situation:)
      // __.57,01      (lastHeightmapHeight)
      //   |
      //   |.__ 56,00  (currentHeight)
      if (this._heightDiff < -Preferences.Collision.COLLISION_THRESHOLD_METERS)
      {
        // assign new player height (no need to tell engine here that player is falling - it will detect it immediately)
        if (!doNotAlterState) // only if we can alter state
        {
          entity.lastHeightmapHeight = entity.getY();
        }
      }
      // player cannot walk further if new position is a lot bigger than old position - typically, player has bumped into the wall)
      // or trying to access too steep terrain
      // (example of situation:)
      // (currentHeight)             51,01 .____
      //                                   |
      // (lastHeightmapHeight)   50,00 ___.|
      else if (this._heightDiff > Preferences.Collision.COLLISION_THRESHOLD_METERS)
      {
        // bumped into the wall etc., collision
        return true;
      }
      /////////////////////////////////////////////////////////
      // WALK ON THE GROUND (NO FALLING OR WALL COLLISION)
      /////////////////////////////////////////////////////////
      else
      {

        if (!doNotAlterState) // only if we can alter state
        {
          entity.lastHeightmapHeight = this._currentHeight;
          entity.camera.setY (entity.lastHeightmapHeight);
          entity._heightmapCoordComputedInFrame = -1;
        }
      }
    }

    return false; // no collision happened
  }
  // ----------------------------------------------------
  // FLY mode
  // ----------------------------------------------------
  else if (entity.inFlyMode())
  {
    if (entity.getY() < this._currentHeight)
    {
      if (!doNotAlterState)
      {
        // take note of height difference between the obstacle (e.g. roof) and player
        // (it will be positive value, which means how much below the player is)
        this.flyModeHeightDiff = this._currentHeight - entity.getY();
      }

      return true;
    }

    // improvement: also incorporate height of the avatar?
    if (entity.getY() >= this._aboveLimit)
    {
      if (!doNotAlterState)
      {
        this.flyModeHeightDiff = Preferences.Collision.COLLISION_FLY_MODE_HEIGHT_DIFF_TOO_MUCH_CONSTANT;
      }

      return true;
    }

    // no collision happened
    return false;
  }
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// AIR STATUS COMPUTATION
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Finds if player is in the air, based on the difference between the player Y position and the heightmap value at
 * the current player position.
 *
 * @param   {Entity}                entity
 * @returns {Collision.AirStatus}   enum expressing entity air state
 */
WorldHeightmap.prototype.findAirStatus = function (entity)
{
  entity.computeHeightmapCoordinates();

  // problem: we must choose appropriate heightmap for which we will call getWorldHeightAtEntity
  // when in tunnel, we need getWorldHeightAtEntity() to be called on floor heightmap

  // in air
  if (entity.jumping || (entity.getY() - entity.currentGroundMap.getWorldHeightAtEntity (entity) > Preferences.Collision.IN_AIR_THRESHOLD_METERS)) // example: 75 - 73 = 2
  {
    // just got into the air
    if (!entity.inAir ())
    {
      return Collision.AirStatus.JUST_GOT_INTO_AIR;
    }
    else
    {
      return Collision.AirStatus.IN_AIR_FOR_LONG_TIME;
    }
  }
  // not in air
  else
  {
    return Collision.AirStatus.NOT_IN_AIR;
  }
};
