using System;
using UnityEngine;
namespace Framework.Actors.Shooter {
  /// Manages a shooter actor moving around on a 2D plane
  public class ShooterActorController : ActorController {
    /// Direction the actor is facing in
    /// 
    ///   This is the direction the character is looking at. If
    ///   an  is present, the direction will be
    ///   that of the head. Otherwise, it will simply point north and it's your
    ///   responsibility to update it :)
    /// 
    public Vector3 LookDirection = Vector3.forward;
    /// Number of jumps the actor has done without touching the ground
    public int ConsecutiveJumpCount;
    /// Whether head movement beyond the allowed range is applied to the body
    /// 
    ///   
    ///     When the actor has one or more s added to it for
    ///     mouse look, moving the head beyond the allowed range will normally restrict
    ///     the angles the player can look at. If this is set to true, rotation beyond
    ///     the range allowed for the head will instead be applied to the body.
    ///   
    ///   
    ///     This design keeps all of the mouse look logic inside the head controller while
    ///     allowing for a wide range of character setups:
    ///   
    ///   
    ///     - 
    ///       Apply = false; YawRange = -90.0 .. 90.0
    ///       
    ///         Player can move the head in a 180 degree angle, body stays still
    ///       
    ///     ///
- 
    ///       Apply = true; YawRange = 0.0 .. 0.0
    ///       
    ///         Player can look around freely, body will change direction instead of head
    ///       
    ///     ///
- 
    ///       Apply = true; YawRange = -90.0 .. 90.0
    ///       
    ///         Player can look around freely, if player turns head more than 90 degrees,
    ///         the rotation beyond 90 degrees will be applied to the body instead.
    ///       
    ///     ///
/// 
    public bool ApplyExcessHeadMovementToBody = true;
#if false // This can be done by deriving from the ShooterActorController instead
    /// 
    ///   Whether the body aligns itself with the head after the head hit
    ///   its rotation limits
    /// 
    /// 
    ///   If this is enabled, the player can still freely rotate the head without any
    ///   response from the actor's body, until the head hits the limits of its
    ///   movement range and the body has to compensate. At that point, the body will
    ///   continue turning until it is aligned with the head again.
    /// 
    public bool AlignBodyAfterExcessHeadRotation = false;
#endif
    /// Presenter driving the Mecanim animation state machine
    public new ShooterActorPresenter Presenter {
      get {
        if(this.shooterPresenter == null) {
          this.shooterPresenter = base.Presenter as ShooterActorPresenter;
        }
        return this.shooterPresenter;
      }
    }
    /// Ability definitions that control what moves the actor can use
    public Abilities Abilities {
      get {
        this.abilities = GetComponent();
        if(this.abilities == null) {
          this.abilities = gameObject.AddComponent();
        }
        return this.abilities;
      }
    }
    /// Aligns the body with the direction the head is looking in
    /// 
    ///   Whether to do the alignment instantly within this method (true) or
    ///   by honoring the  field.
    /// 
    public void AlignBodyWithHead(bool instantaneously = true) {
      if(instantaneously) {
        RotateOnlyBodyOnYTowards(this.headController.GetGlobalLookDirection());
      } else {
        this.isAligningWithHead = true; // Rotation happens in FixedUpdate()
      }
    }
    /// 
    ///   Rotates the body while counter-rotating the head to keep its orientation
    /// 
    /// Number of degrees the body should be rotated
    public void RotateOnlyBodyOnY(float degrees) {
      // With no head controller, we'll just rotate the body alone
      if(this.headController == null) {
        transform.Rotate(transform.up, degrees);
      } else { // If there's a head controller, the head needs to counter-rotate
        Quaternion globalHeadOrientation = (
          this.headController.HeadBaseOrientation * this.headController.LocalHeadOrientation
        );
        transform.Rotate(transform.up, degrees);
        // CHECK: This produces a little bit of wobble with the mouse controller
        //   This is likely from having a not perfectly vertical spine bone (defining
        //   the head's coordinate system) and the mouse controller converting to
        //   pitch/yaw internally, thus keeping the head vertically in line with it.
        this.headController.LocalHeadOrientation = (
          Quaternion.Inverse(this.headController.HeadBaseOrientation) * globalHeadOrientation
        );
      }
    }
    /// Rotates the body towards the specified direction in on the Y axis
    /// Direction the body will rotate towards
    /// Maximum number of degrees the body will be rotated
    /// 
    ///   True if the body is now facing the desired direction completely,
    ///   false if further rotation is needed to reach the target direction.
    /// 
    public bool RotateOnlyBodyOnYTowards(Vector3 direction, float maximumDegrees = 180.0f) {
      // Figure out the number of degrees we should rotate
      float degreesToRotate;
      {
        Vector3 localTargetDirection;
        {
          Quaternion inverseActorRotation = Quaternion.Inverse(transform.rotation);
          localTargetDirection = inverseActorRotation * direction;
        }
        float targetAngle = Mathf.Atan2(localTargetDirection.x, localTargetDirection.z);
        degreesToRotate = targetAngle * Mathf.Rad2Deg;
      }
      // How far the body has to rotate to align with the head direction
      if(degreesToRotate >= 180.0f) {
        degreesToRotate -= 360.0f;
      } else if(degreesToRotate < -180.0f) {
        degreesToRotate += 360.0f;
      }
      // Limit the amount of rotation to the specified number of degrees,
      // then apply the rotation to the body (and counter-rotate the head)
      float unclampedDegreesToRotate = degreesToRotate;
      degreesToRotate = Mathf.Clamp(degreesToRotate, -maximumDegrees, +maximumDegrees);
      RotateOnlyBodyOnY(degreesToRotate);
      // Report whether the angle was cut off or the target direction was achieved
      return (degreesToRotate == unclampedDegreesToRotate);
    }
    /// Called when the component gets added to a gameObject
    protected override void Awake() {
      base.Awake();
      // CHECK: Ugly. Hardcoded preferrence for a specific component type.
      this.headController = GetComponent();
      if(this.headController == null) {
        this.headController = GetComponent();
      }
      if(this.headController != null) {
        var onExcessHeadRotationDelegate = new Action(OnExcessHeadRotation);
        this.headController.ExcessHeadRotation += onExcessHeadRotationDelegate;
      }
    }
    /// Called once per physics frame to update the positions of game object
    protected override void FixedUpdate() {
      // Update the look direction if a mouse look handler is present on the actor.
      // Do this before processing the active move so the move can respond to look direction
      // without a 1-frame lag
      updateLookDirection();
      // If the actor is currently auto-aligning its body with the head (look direction),
      // update the body orientation here (in FixedUpdate() because body orientation may
      // have an effect on various things such as hitboxes, shooting, etc.)
      if(this.isAligningWithHead) {
        if(this.headController != null) {
          Vector3 lookDirection = this.headController.GetGlobalLookDirection();
          float maximumRotation = Abilities.TurnDegreesPerSecond * Time.fixedDeltaTime;
          this.isAligningWithHead = !RotateOnlyBodyOnYTowards(lookDirection, maximumRotation);
        }
      }
      base.FixedUpdate();
    }
    /// Called each visual frame to update the visible state of the actor
    protected override void Update() {
      // Update the look direction if a mouse look handler is present on the actor.
      // Do this before processing the active move so the move can respond to look direction
      // without a 1-frame lag
      updateLookDirection();
      base.Update();
      // If a shooter presenter is present, update its velocities
      // so the correct running, strafing and/or jumping animations can play.
      ShooterActorPresenter presenter = Presenter;
      if(presenter != null) {
        // Try to obtain the velocity either from a rigidbody or an ActorPhysics component
        Vector3 velocity;
        {
          if(Rigidbody != null) {
            velocity = Rigidbody.velocity;
          } else if(ActorPhysics != null) {
            velocity = ActorPhysics.Velocity;
          } else {
            return;
          }
        }
        // Forward the directional velocities to the presenter
        float magnitude = velocity.magnitude;
        presenter.ForwardVelocity = (
          Vector3.Dot(transform.forward, velocity) * magnitude
        );
        presenter.StrafingVelocity = (
          Vector3.Dot(transform.right, velocity) * magnitude
        );
        presenter.VerticalVelocity = (
          Vector3.Dot(transform.up, velocity) * magnitude
        );
      }
    }
    /// 
    ///   Called when the player has tried to rotate the head farther than
    ///   its movement range allows
    /// 
    /// Excess rotation that was clamped off
    protected virtual void OnExcessHeadRotation(Quaternion excessRotation) {
      if(!this.ApplyExcessHeadMovementToBody) {
        return;
      }
      updateLookDirection();
      applyHeadRotationToBodyOnY(excessRotation);
    }
    /// Applies a rotation performed by the head to the body
    /// Head rotation that will be applied to the body
    /// 
    ///   This method translates the rotation from the head into an equivalent rotation
    ///   in the coordinate system of the body (because the actor might be bowing down
    ///   or be an animal whose head is at an angle relative to the body). Only
    ///   the rotation on the Y axis will be applied.
    /// 
    private void applyHeadRotationToBodyOnY(Quaternion headRotation) {
      // Calculate the clamped and unclamped head orientations in actor space
      Quaternion clampedHeadOrientation, unclampedHeadOrientation;
      {
        Quaternion inverseActorRotation = Quaternion.Inverse(transform.rotation);
        Quaternion baseOrientation = this.headController.HeadBaseOrientation;
        // Translate the current head orientation into the actor's coordinate system
        clampedHeadOrientation = inverseActorRotation * (
          baseOrientation * this.headController.LocalHeadOrientation
        );
        // Get the unclamped head orientation in the head's local coordinate system
        Quaternion localUnclampedHeadOrientation;
        {
          Vector3 unclampedHeadEulers = (
            this.headController.LocalHeadOrientation.eulerAngles + headRotation.eulerAngles
          );
          localUnclampedHeadOrientation = (
            Quaternion.AngleAxis(unclampedHeadEulers.y, Vector3.up) *
            Quaternion.AngleAxis(unclampedHeadEulers.x, Vector3.right)
          );
        }
        // Translate the unclamped head orientation into the actor's coordinate system
        unclampedHeadOrientation = inverseActorRotation * (
          baseOrientation * localUnclampedHeadOrientation
        );
      }
      Transform actorTransform = transform;
      // And finally, we rotate the actor around its own Y axis by the amount of
      // clamped-off head rotation that is present in the actor's coordinate system.
      float yawRotation = (
        unclampedHeadOrientation.eulerAngles.y - clampedHeadOrientation.eulerAngles.y
      );
      actorTransform.Rotate(actorTransform.up, yawRotation);
      // If the head controller is virtual reality based we may need to counter-rotate
      // the camera root to preserve the calibrated head position for the player.
      Transform virtualRealityCameraRoot = this.headController.VirtualRealityCameraRoot;
      if(virtualRealityCameraRoot != null) {
        // Counter-rotation of the camera root is only needed if the camera root
        // is a child of the actor (and was thus rotated /with/ the actor).
        if(virtualRealityCameraRoot.IsChildOf(actorTransform)) {
          virtualRealityCameraRoot.RotateAround(
            actorTransform.position, actorTransform.up, -yawRotation
          );
        }
      }
    }
    /// Updates the 'LookDirection' field if a head controller is present
    private void updateLookDirection() {
      if(this.headController != null) {
        this.LookDirection = (
          this.headController.HeadBaseOrientation * this.headController.LocalHeadOrientation
        ) * Vector3.forward;
      }
    }
    /// Presenter driving the Mecanim animation state machine
    private ShooterActorPresenter shooterPresenter;
    /// Abilities of the actor
    private Abilities abilities;
    /// Optional head controller managing look direction and turning
    private IHeadController headController;
    /// Whether the body is currently aligning itself with the head
    private bool isAligningWithHead;
  }
} // namespace Framework.Actors.Shooter