"use strict";

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

/**
 * Mainly interface for calling collision detection methods, but also does some simple collision checks
 * (ie if the avatar is inside the world) and gravity simulation.
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @constructor
 */
var Collision = function (collisionEngine)
{
  /**
   * @type {Heightmap}
   */
  this.collisionEngine = collisionEngine; // collision engine in this application is Heightmap

  /**
   * Speed of falling.
   *
   * @type {number}
   */
  this.fallAcceleration = Preferences.Collision.FALL_ACCELERATION;

  /**
   * Id number of last frame when collision has occurred. Used for displaying virtual keyboard in red colors
   * for signalizing that avatar cannot move further in desired direction.
   *
   * @type {number}
   */
  this.lastCollisionFrameId = -1;

  /**
   * Number of frame where collision from mouse wheel move happened.
   *
   * @type {number}
   */
  this.keepForbiddenKeyboardForFrameId = 0;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// COLLISION DETECTION FOR MOVING
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 *
 *
 * @type {number}
 */
//Collision.MOVE_COLLISION_ITERATION_BUDGET = Preferences.Application.isDevMode() ? 1 : 4;


//function shiftEntityForward (moveVector, distance)
//{
//  var m = moveVector.z / moveVector.x;
//
//  //console.log("m: " + m);
//
//  var x = Math.sqrt ((distance*distance) / (m*m + 1));
//
//  //console.log("x: " + x);
//
//  var z = m*x;
//
//  //console.log("z: " + z);
//
//  var forwardShiftVector = new THREE.Vector3 (x, 0, z);
//  //var forwardShiftVector2d = new THREE.Vector2 (x, z);
//  //console.log(forwardShiftVector.toArray() + " (length: " + forwardShiftVector2d.length() + ")");
//
//  return forwardShiftVector;
//}

/**
 * Performs collision test when moving player around.
 *
 * When in WALK mode: Test repeats specified number of times, each time with smaller value of the move
 * parameter (the smaller the parameter, the smaller the increase of the player position due to the move). Testing stops
 * when the budget is reached or whenever some test terminates with a 'no collision' result.
 *
 * When in FLY mode: Test is performed only once, with the move parameter = 1, indicating that the increase of the
 * player position is the initial value.
 *
 * @param {*}         self              class with the actionToPerform method, will be binded as 'this'
 * @param {Entity}    entity
 * @param {function}  actionToPerform   actual move action (computing new player position)
 * @param {*}         param             parameter passed into the move action
 */
Collision.prototype.move = function (self, entity, actionToPerform, param)
{

  // when in air, we perform the test only once
    // otherwise we would end up in the wall - probably because some pixels
    // in the heightmap have different height (maybe just an issue with the development version where heightmap is generated
    // automatically.. who knows)
  if (entity.inAir())
  {
    return this._checkCollision ( entity, actionToPerform.bind (self, param, 1.0) );
  }
  else
  {

    // this is a relict from the past - It had bothered me that collision can occur even if player can obviously move a little bit forward,
    // so my solution was: if we get a collision with full amount of move, we would then try half amount of the move, then half of that half etc., for like 3-4 iterations,
    // and we will be either sure that we really can't move forward, or we will be closer to the wall than originally
    // however, since I have reworked the collisions a little after I have submitted the thesis, the mechanism didn't quite work,
    // and now I don't have enough time to look into it, so I removed it
    var moveParamValue = 1;

    this._checkCollision (entity, actionToPerform.bind (self, param, moveParamValue));
  }
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// COLLISION DETECTION COMPUTATION, COLLISION WITH THE WORLD BOX
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Performs collision check against the boundary of the world and against the objects in the world.
 * If collision occurs, position is not changed. A specified action is perfomed, typically it changes entity posiition.
 * Then we check collisions and if we detect any, we revert to the old entity position before it was modified by the
 * given action.
 *
 * @param   {Entity}    entity
 * @param   {function}  actionToPerform   function performed before the collision detection is executed
 * @returns {boolean}                     TRUE if all is ok (no collision), FALSE if collision happened
 */
Collision.prototype._checkCollision = function (entity, actionToPerform)
{
  entity.camera.saveCurrentPosition();

  // perform action after which we check if collision happened
  if (actionToPerform !== null)
  {
    actionToPerform();
  }

  if (Preferences.Collision.__COLLISION_DETECTION)
  {
    // (note that we should have current information about player air status, i.e. _checkAirStatus() should have been called recently)

    var collisionHappened = false;

    // (important to remember before the first collision check)
    var inTunnelLastFrame = player.inTunnel;

    // we do regular collision check on the new player's position
     // (this may seem useless, the important is to check the further collision /see below/, but we NEED this so the computeCollision() can set variables on player such as if he's in a tunnel and similar)
    var regularCollision = this.collisionEngine.computeCollision (entity);
    var positionAfterRegularCollision = entity.camera.position.clone();

    collisionHappened = regularCollision;

    // regular collision has not occurred, need to do additional collision checks
    if (!collisionHappened)
    {
      // player wasn't in the tunnel last frame and now he is
      if (!inTunnelLastFrame && player.inTunnel)
      {
        // this is special case, we don't check for additional collisions since that would actually prevent us from entering the tunnel
        collisionHappened = false;
      }
      // not in a tunnel (usual case)
      else
      {
        // we know how much distance we want to move along the direction of move vector, so compute parameter for it
        var tAdditionMove = entity.camera.moveVectorLength ? (Preferences.Collision.FORWARD_DIRECTION_DISTANCE_LIMIT / entity.camera.moveVectorLength) : 0;

        if (!Collision.noForwardCollisionCheck)
        {
          // now, we check for a collision a little bit further in the direction which player has moved (see Preferences.Collision for further explanation)
          entity.camera.position.add (player.camera.moveVector.clone().multiplyScalar(tAdditionMove));
          collisionHappened = this.collisionEngine.computeCollision (entity, true); // we pass true flag so the computeCollision() won't change any variables on player etc. - THIS IS IMPORTANT
          entity.camera.position.copy (positionAfterRegularCollision); // restore to the player's new position
        }


        if (!collisionHappened && !Collision.noRightLeftCollisionCheck)
        {
          // note: we don't compute parameters along the direction of move vector for right and left collisions

          // check for collision slightly to the right
          entity.camera.position.add(entity.camera.rightVector.clone().multiplyScalar(Preferences.Collision.RIGHT_DIRECTION_DISTANCE_LIMIT).multiplyScalar(1));
          var collisionRight = this.collisionEngine.computeCollision (entity, true);
          entity.camera.position.copy (positionAfterRegularCollision); // restore to the player's new position

          //if (!collisionHappened)
          //{
            // check for collision slightly to the left
            entity.camera.position.add(entity.camera.rightVector.clone().multiplyScalar(Preferences.Collision.RIGHT_DIRECTION_DISTANCE_LIMIT).multiplyScalar(-1));
            var collisionLeft = this.collisionEngine.computeCollision (entity, true);
            entity.camera.position.copy (positionAfterRegularCollision); // restore to the player's new position

          if (!collisionLeft && !collisionRight)
          {
            collisionHappened = false;
          }
          else if (collisionLeft && collisionRight)
          {
            collisionHappened = false;
            //console.log("collision both sides");
          }
          else
          {
            //console.log("collision: collisionRight " + collisionRight + ", collisionLeft: " + collisionLeft);
            collisionHappened = true;
          }

          //} // else: slightly to the right collision occurred, no need to do additional checks
        }
        // else: further collision occurred, no need to do additional checks

      }
    }
    // else: regular collision occurred, no need to do additional checks

    // player is in bounding box of the world (no wandering off!), and no collision happened -> changes are OK
    if (this.isAvatarInsideTheWorld (entity) && (!collisionHappened))
    {
      entity.camera._flagViewChanged();
    }
    else
    {
      // player outside of the world or collided with something -> revert to the old camera position

      entity.camera.restoreOldPosition();

      // remember id of current frame
      this.lastCollisionFrameId = clock.elapsedFrames;

      // player shall not be stopped by small obstacles during the fly mode, try to shift him up a little
      if (entity.inFlyMode() && (this.collisionEngine.flyModeHeightDiff < Preferences.Collision.COLLISION_THRESHOLD_FLY_METERS))
      {
        // shift camera a little up (I wanted to do additional collision check for position above, but it doesn't work with it, so don't do it..)
        entity.camera.position.y += Preferences.Collision.COLLISION_THRESHOLD_FLY_METERS;
        entity.camera._flagViewChanged();
      }
      // this prevents flashing keyboard red when entity starts to jump (don't know how to fix it otherwise and there is no time to investigate)
      // (only if we haven't moved shifted in the fly mode)
      else if (!(entity.jumping && entity.getFreefallDuration() < 100))
      {
        gui.drawVirtualKeyboardAsForbidden();
      }

      return false;
    }
  }
  else
  {
    entity.camera._flagViewChanged();
  }

  return true;
};

/**
 * Makes sure that avatar does not wander off the world area defined by bounding box.
 *
 * @param   {Entity}    entity      avatar (player) reference
 * @returns {boolean}   TRUE if avatar is inside the world bounding box
 */
Collision.prototype.isAvatarInsideTheWorld = function (entity)
{
  //entity.computeHeightmapCoordinates();

  // we check if player is in the world bounding box (no need to check min Y)
  if ((entity.getPosition().x <= Preferences.Heightmap.WORLD_3D_BOUNDING_BOX.MIN_X) ||
      (entity.getPosition().x >= Preferences.Heightmap.WORLD_3D_BOUNDING_BOX.MAX_X) ||
      (entity.getPosition().z <= Preferences.Heightmap.WORLD_3D_BOUNDING_BOX.MIN_Z) ||
      (entity.getPosition().z >= Preferences.Heightmap.WORLD_3D_BOUNDING_BOX.MAX_Z) ||
       entity.getY() >= Preferences.Heightmap.MAXIMUM_ALLOWABLE_HEIGHT
      )
  {
    //console.log("not allowed isAvatarInsideTheWorld: " + entity.heightmapCoord.x + "," + entity.heightmapCoord.y + " |  avatar Y: " + entity.getY() + ", max allow height: " + Preferences.Heightmap.MAXIMUM_ALLOWABLE_HEIGHT);
    return false;
  }

  return true;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// AIR STATUS RETRIEVAL
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Status describing position of the player.
 *
 * @readonly
 * @enum {number}
 */
Collision.AirStatus =
{
  IN_AIR_FOR_LONG_TIME: 0,
  JUST_GOT_INTO_AIR:    1,
  NOT_IN_AIR:           2
};

/**
 * Checks if player is in the air.
 *
 * @returns {Collision.AirStatus}   one of the 3 possible statuses (IN_AIR_FOR_LONG_TIME, JUST_GOT_INTO_AIR, NOT_IN_AIR)
 */
Collision.prototype.getAirStatus = function (entity)
{
  return this.collisionEngine.findAirStatus (entity);
};

/**
 * Checks if player is in the air and perform actions.
 *
 * @param {Entity}  entity
 */
Collision.prototype._checkAirStatus = function (entity)
{
  switch (this.collisionEngine.findAirStatus (entity))
  {
    case Collision.AirStatus.JUST_GOT_INTO_AIR:
      entity.updateFreefallStartTime ();
      break;

    case Collision.AirStatus.NOT_IN_AIR:
      entity.resetFreefallStartTime ();
      break;
  }
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FREEFALL (GRAVITY) COMPUTATION
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Performs freefall (gravity) simulation.
 *
 * @param {Entity}  entity
 */
Collision.prototype.freefall = function (entity)
{
  this._checkAirStatus (entity);

  if (entity.inAir() && entity.inWalkMode())
  {
    this._checkCollision ( entity, this._checkFreefall.bind (this, entity) );
  }
};

/**
 * Computation of freefall amount (how much should be avatar Y position altered).
 *
 * @param {Entity}  entity
 */
Collision.prototype._checkFreefall = function (entity)
{
  // (we already know that entity is in air, and in WALK mode)

  // if the entity has exceeded maximum jumping duration, it shall start to fall
  if (entity.jumping && entity.inAir() && (entity.getFreefallDuration() > Preferences.Collision.MAX_JUMP_DURATION))
  {
    // terminate jump and restore normal falling acceleration
    entity.turnJumpMode();

    // we don't want to perform rest of the method
    return;
  }

  // (added recently because of the jump feature - otherwise, the player won't be able to move further if it lands e.g. on the roof, because player would have saved some small lastHeightmapHeight)
  player.lastHeightmapHeight = heightmapWorld.getMain().getWorldHeightAtEntity(player);

  // note: in case of freefall, there are no alternations of the distance with state.getLastFrameDeltaRatio() like with regular moving around
  // (it is kinda unnecessary and frankly, kinda pain to do...)
  entity.camera.setY (entity.camera.positionFallStart - this.computeFallAmount( entity.getFreefallDuration() ) );

  // check if air status has changed (maybe player has already landed onto the ground!)
  this._checkAirStatus (entity);
};

/**
 * Computes distance of the fall given duration. Uses basic physics equation s = 1/2 * a * t^2.
 *
 * @param   {number} fallDuration   for how long is player in the fall
 * @returns {number}
 */
Collision.prototype.computeFallAmount = function (fallDuration)
{
  return (0.5 * this.fallAcceleration * fallDuration * fallDuration);
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// UGLY FIXES
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// unfortunately, using the mechanism with checking positions slighly to the left and right (see Preferences.Collision.RIGHT_DIRECTION_DISTANCE_LIMIT),
// will prevent us from walking in a very narrow streets (tunnels are alright)
// since I have no time to fix it now, there is no other choice than ugly fix as this :o)

// there is one such street in the current model, and that is the path down to the deers in the Stag Moat location
// so we will check if we are close to the 3D point placed in that path and if we are, we will turn off the collision checking for right and left points,
// which allows us to go easily through
Collision.noForwardCollisionCheck = false;
Collision.noRightLeftCollisionCheck = false;

// when closer than this distance, we stop checking right and left collisions
Collision.stagMoatDistEnterSq = 125;

// when closer than this distance and BELOW stag moat y, we also do not check right and left collisions (it is by chance that Stag Moat is the place with lowest y)
// so we do it like this so user can walk that path which goes beyond the Arcibishops palace
Collision.stagMoatDistBelowSq = 2700;

// 3D reference point on the Stag Moat path
Collision.stagMoatPos = new THREE.Vector3(-149.0069119398264, -1.2286435561563565, -49.121142931314104);

Collision.prototype._stagMoatFix = function()
{
  var distToStagMoatSq = Collision.stagMoatPos.distanceToSquared(player.getPosition());

  if ((distToStagMoatSq <= Collision.stagMoatDistEnterSq) || (distToStagMoatSq <= 1700 && player.getPosition().y < Collision.stagMoatPos.y))
  {
    Collision.noRightLeftCollisionCheck = true;
  }
  else
  {
    Collision.noRightLeftCollisionCheck = false;
  }
};
