"use strict";

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

/**
 * Object capable of moving its position and displaying the world through its
 * eyes (from 1st person perspective). Essentially, an avatar/player.
 *
 * @author smrceant (Antonin Smrcek)
 *
 * @param         {Camera}  firstPersonCamera     Camera associated with this entity
 * @param         {string}  [name]                name of the entity
 * @constructor
 */
var Entity = function (firstPersonCamera, name)
{
  this.name = Util.isInitialized (name) ? name : "unnamed";

  /**
   * Camera associated with the entity.
   *
   * @type {Camera}
   */
  this.camera = firstPersonCamera;
  this.camera.setOwnerEntity(this);

  /**
   * Computed coordinates into the heightmap (3D world entity position -> projected into 2D).
   *
   * @type {THREE.Vector2}
   */
  this.heightmapCoord = new THREE.Vector2();

  /**
   * Last frame when player's coordinates into the heightmap were computed.
   *
   * @type {number}
   * @private
   */
  this._heightmapCoordComputedInFrame = -1;

  /**
   * Last retrieved height from the heightmap for the entity.
   *
   * @type {number}
   */
  this.lastHeightmapHeight = null;

  /**
   * Which heightmap is currently used for looking up the height values of the ground. Most of the time, it is the
   * (standard) "Heightmap", but when in tunnel, it will be the "Tunnel Ground Heightmap".
   *
   * @type {Heightmap}
   */
  this.currentGroundMap = null;

  // DO NOT MODIFY THIS DIRECTLY
  /**
   * Current mode of controlling the entity - e.g. WALK, FLY. Do not modify this directly, use setters turnXxxMode().
   *
   * @type {number}
   * @private
   */
  this._avatarMode = Entity.AvatarModes.WALK;

  /**
   * The time (obtained by Date.now()) when the entity started to fall. Watch out, it is in miliseconds (ms).
   *
   * @type {number}
   */
  this.freefallStart = 0;

  /**
   * Whether the entity is currently jumping (i.e. moving up against fravity) or not.
   *
   * @type {boolean}
   */
  this.jumping = false;

  /**
   * TRUE if player is inside the tunnel.
   *
   * @type {boolean}
   */
  this.inTunnel = false;

  /**
   * Time spent currently in a tunnel. For adjusting collision detection.
   *
   * @type {number}
   */
  this.timeInTunnel = 0;

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

  Entity.entityList.push (this);
};

/**
 * List of all created entities.
 *
 * @type {Entity[]}
 */
Entity.entityList = [];

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// POSITION RETRIEVAL
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * [DEV] Returns source code representation for current position and camera orientation of the player.
 * Use this to obtain viewpoint definition for some place in the castle area.
 *
 * @returns {string}
 */
Entity.prototype.getViewpointTextDefinition = function()
{
  return "Preferences.Viewpoint.VIEWPOINT_XXX = new Viewpoint (new THREE.Vector3 ("+this.getPosition().x+", "+this.getPosition().y+", "+this.getPosition().z+"), "+this.getCamera().angleHorizontal+", "+this.getCamera().angleVertical+");";
};

/**
 * @returns {THREE.Vector3}    position of the entity in the world
 */
Entity.prototype.getPosition = function ()
{
  return this.camera.position;
};

/**
 * @returns {number}    Y position of the entity in the world (altitude)
 */
Entity.prototype.getY = function ()
{
  return this.camera.position.y;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// ENTITY AVATAR MODES
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Possible modes for the entity control.
 * WALK = walking on the terrain, possible to move only in the XZ direction (i.e. on the ground), colliding with the walls, falling when in air.
 * FLY = flying (so no falling), can move in all directions, colliding with the walls.
 *
 * @type {{WALK: number, FLY: number}}
 */
Entity.AvatarModes =
{
  WALK: 0,
  FLY:  1
};

/**
 * Activates walking mode, where entity is affected by the gravitation and terrain steepness. Default mode for the
 * player (user avatar).
 */
Entity.prototype.turnWalkMode = function ()
{
  var airStatus = collision.getAirStatus (this);

  if ((airStatus === Collision.AirStatus.IN_AIR_FOR_LONG_TIME) || (airStatus === Collision.AirStatus.JUST_GOT_INTO_AIR))
  {
    this.updateFreefallStartTime ();
  }

  this._avatarMode = Entity.AvatarModes.WALK;
};

/**
 * Activates flying mode, where the entity isn't affected by the gravitation. Default mode for the external
 * (e.g. 3rd person) camera.
 */
Entity.prototype.turnFlyMode = function ()
{
    this._avatarMode = Entity.AvatarModes.FLY;

    collision.fallAcceleration = Preferences.Collision.FALL_ACCELERATION;
    this.resetFreefallStartTime ();
};

/**
 * Switches between FLY <-> WALK mode.
 */
Entity.prototype.switchWalkFlyMode = function ()
{
    this.inWalkMode() ? this.turnFlyMode() : this.turnWalkMode();
};

/**
 * @returns {boolean}   TRUE if entity is currently in walking mode
 */
Entity.prototype.inWalkMode = function ()
{
  return (this._avatarMode === Entity.AvatarModes.WALK);
};

/**
 * @returns {boolean}   TRUE if entity is currently in flying mode
 */
Entity.prototype.inFlyMode = function ()
{
  return (this._avatarMode === Entity.AvatarModes.FLY);
};

/**
 * Entity will be in air when user switches from FLY mode to the WALK mode - at that very moment, entity will start
 * to fall.
 *
 * @returns {boolean}   TRUE if entity is currently in air (i.e. falling)
 */
Entity.prototype.inAir = function()
{
  return (this.freefallStart !== 0);
};

/**
 * Sets the time when entity has started to fall down to current time and saves position from which entity
 * has started to fall.
 */
Entity.prototype.updateFreefallStartTime = function ()
{
  this.freefallStart = clock.elapsedFrames; // (see getFreefallDuration() )

  this.camera.positionFallStart = this.getY();
};

/**
 * Terminates falling of the entity.
 */
Entity.prototype.resetFreefallStartTime = function ()
{
  this.freefallStart = 0;
};

/**
 * @returns {number}    how many ms has elapsed since the entity started to fall
 */
Entity.prototype.getFreefallDuration = function ()
{
  // return (Date.now() - this.freefallStart);
  // quick, but quite nice fix for the problem that the solution above does not work very well when we pause during the falling:
  // we will be computing with frames instead of time, but in the end we convert to the ms so we don't have to worry about
  // modificating all the stuff too much ... lets say that 1 frame is 16ms
  return (clock.elapsedFrames - this.freefallStart)*16;
};

/**
 * Makes the entity jump. Called from Controls.js.
 */
Entity.prototype.jump = function ()
{
  // if not in air (i.e. not falling, nor jumping) and not in tunnel, then entity can jump
  if (!this.inAir() && !heightmapWorld.isInTunnel(this))
  {
    this.turnJumpMode();
  }
};

/**
 * Turns on/off jump mode for the entity.
 */
Entity.prototype.turnJumpMode = function ()
{
  // turn jumping off
  if (this.jumping)
  {
    collision.fallAcceleration = Preferences.Collision.FALL_ACCELERATION;

    // makes the entity falling
    this.updateFreefallStartTime();
  }
  // turn jumping on
  else
  {
    collision.fallAcceleration = Preferences.Collision.JUMP_ACCELERATION;
  }

  this.jumping = !this.jumping;
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// GETTERS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 *
 * @returns {Camera}
 */
Entity.prototype.getCamera = function ()
{
  return this.camera;
};

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

/**
 * Sets new position for the entity.
 *
 * @param {THREE.Vector3}   newPosition
 */
Entity.prototype.setPosition = function (newPosition)
{
  this.camera.setPosition (newPosition);
};

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// HEIGHTMAP (COLLISION) RELATED METHODS
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * Computes entity's coordinates into the heightmap. Contains safety check - coordinates are computed only once in the
 * current frame.
 *
 * @param {boolean}   [forceRecompute]    if TRUE, safety check will be suppressed and coordinates will be forced to recompute
 */
Entity.prototype.computeHeightmapCoordinates = function (forceRecompute)
{
  //       IMPORTANT: if something, namely collisions seems fucked up, turn this safety check off; sometimes it can happen that during one frame, even if we have already computed heightmap coords,
  //                  entity position changes and we need to compute them again

  // ok, the heightmap coordinates are not up to date, we can recalculate them
  if ((this._heightmapCoordComputedInFrame !== clock.elapsedFrames) || forceRecompute)
  {
    heightmapWorld.map.computeScreenCoordinates (this.getPosition(), this.heightmapCoord);

    this._heightmapCoordComputedInFrame = clock.elapsedFrames;
  }
};

/**
 * Updates last heightmap height for this entity,
 */
Entity.prototype.updateLastHeightmapHeight = function ()
{
  this.computeHeightmapCoordinates(true);

  this.lastHeightmapHeight = heightmapWorld.getMain().getWorldHeightAt (this.heightmapCoord.x, this.heightmapCoord.y, this);
};
