using UnityEngine;
using System.Collections;

[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
    enum CharacterState
    {
        Idle,
        Walking,
        Trotting,
        Running,
        Jumping
    }

    public AnimationClip idleAnimation;
    public AnimationClip walkAnimation;
    public AnimationClip runAnimation;
    public AnimationClip jumpAnimation;

    public float walkMaxAnimationSpeed = .75f;
    public float trotMaxAnimationSpeed = 1f;
    public float runMaxAnimationSpeed = 1f;
    public float jumpAnimationSpeed = 1.15f;
    public float landAnimationSpeed = 1f;

    private Animation _animation;

    private CharacterState _characterState;

    public float walkSpeed = 2f;
    public float trotSpeed = 4f;
    public float runSpeed = 6f;

    public float inAirControlAcceleration = 3f;

    public float jumpHeight = .5f;

    public float gravity = 20f;

    public float speedSmoothing = 10f;
    public float rotateSpeed = 500f;
    public float trotAfterSeconds = 3f;

    public bool canJump = true;

    private float jumpRepeatTime = .05f;
    private float jumpTimeout = .15f;
    private float groundedTimeout = .25f;
    private bool doubleJumped = false;

    // The camera doesnt start following the target immediately but waits for a split second to avoid too much waving around.
    private float lockCameraTimer = 0f;

    // The current move direction in x-z
    private Vector3 moveDirection = Vector3.zero;
    // The current vertical speed
    private float verticalSpeed = 0f;
    // The current x-z move speed
    private float moveSpeed = 0f;

    // The last collision flags returned from controller.Move
    private CollisionFlags collisionFlags;

    // Are we jumping? (Initiated with jump button and not grounded yet)
    private bool jumping = false;
    private bool jumpingReachedApex = false;

    // Are we moving backwards (This locks the camera to not do a 180 degree spin)
    private bool movingBack = false;
    // Is the user pressing any keys?
    private bool isMoving = false;
    // When did the user start walking (Used for going into trot after a while)
    private float walkTimeStart = 0f;
    // Last time the jump button was clicked down
    private float lastJumpButtonTime = -10f;
    // Last time we performed a jump
    private float lastJumpTime = -1f;

    // the height we jumped from (Used to determine for how long to apply extra jump power after jumping.)
    private float lastJumpStartHeight = 0f;

    private bool inDraftZone = false;

    private Vector3 inAirVelocity = Vector3.zero;

    //private Vector3 movingPlatformVelocity = Vector3.zero;
    private bool _onMovingPlatform = false;

    private float lastGroundedTime = 0f;

    private bool isControllable = true;

    private void Awake()
    {
        moveDirection = transform.TransformDirection(Vector3.forward);

        _animation = GetComponent<Animation>();
        if (!_animation)
            Debug.Log("The character you would like to control doesn't have animations. Moving her might look weird.");

        if (!idleAnimation)
        {
            _animation = null;
            Debug.Log("No idle animation found. Turning off animations.");
        }
        if (!walkAnimation)
        {
            _animation = null;
            Debug.Log("No walk animation found. Turning off animations.");
        }
        if (!runAnimation)
        {
            _animation = null;
            Debug.Log("No run animation found. Turning off animations.");
        }
        if (!jumpAnimation && canJump)
        {
            _animation = null;
            Debug.Log("No jump animation found and the character has canJump enabled. Turning off animations.");
        }
    }

    private void UpdateSmoothedMovementDirection()
    {
        Transform cameraTransform = Camera.main.transform;
        bool grounded = IsGrounded();

        // Forward vector relative to the camera along the x-z plane	
        Vector3 forward = cameraTransform.TransformDirection(Vector3.forward);
        forward.y = 0;
        forward = forward.normalized;

        // Right vector relative to the camera
        // Always orthogonal to the forward vector
        Vector3 right = new Vector3(forward.z, 0, -forward.x);

        float v = Input.GetAxisRaw("Vertical");
        float h = Input.GetAxisRaw("Horizontal");

        // Are we moving backwards or looking backwards
        if (v < -0.2)
            movingBack = true;
        else
            movingBack = false;

        bool wasMoving = isMoving;
        isMoving = Mathf.Abs(h) > 0.1 || Mathf.Abs(v) > 0.1;

        // Target direction relative to the camera
        Vector3 targetDirection = h * right + v * forward;

        // Grounded controls
        if (grounded)
        {
            // Lock camera for short period when transitioning moving & standing still
            lockCameraTimer += Time.deltaTime;
            if (isMoving != wasMoving)
                lockCameraTimer = 0f;

            // We store speed and direction seperately,
            // so that when the character stands still we still have a valid forward direction
            // moveDirection is always normalized, and we only update it if there is user input.
            if (targetDirection != Vector3.zero)
            {
                // If we are really slow, just snap to the target direction
                if (moveSpeed < walkSpeed * 0.9 && grounded)
                {
                    moveDirection = targetDirection.normalized;
                }
                // Otherwise smoothly turn towards it
                else
                {
                    moveDirection = Vector3.RotateTowards(moveDirection, targetDirection, rotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000);

                    moveDirection = moveDirection.normalized;
                }
            }

            // Smooth the speed based on the current target direction
            float curSmooth = speedSmoothing * Time.deltaTime;

            // Choose target speed
            //* We want to support analog input but make sure you cant walk faster diagonally than just forward or sideways
            float targetSpeed = Mathf.Min(targetDirection.magnitude, 1f);

            _characterState = CharacterState.Idle;

            // Pick speed modifier
            if (Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))
            {
                targetSpeed *= runSpeed;
                _characterState = CharacterState.Running;
            }
            else if (Time.time - trotAfterSeconds > walkTimeStart)
            {
                targetSpeed *= trotSpeed;
                _characterState = CharacterState.Trotting;
            }
            else
            {
                targetSpeed *= walkSpeed;
                _characterState = CharacterState.Walking;
            }

            moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);

            // Reset walk time start when we slow down
            if (moveSpeed < walkSpeed * 0.3)
                walkTimeStart = Time.time;
        }
        // In air controls
        else
        {
            // Lock camera while in air
            if (jumping)
                lockCameraTimer = 0f;

            if (isMoving)
                inAirVelocity += targetDirection.normalized * Time.deltaTime * inAirControlAcceleration;
        }
    }

    private void ApplyJumping()
    {
        // Prevent jumping too fast after each other
        if (lastJumpTime + jumpRepeatTime > Time.time)
            return;

        if (IsGrounded())
        {
            // Jump
            // - Only when pressing the button down
            // - With a timeout so you can press the button slightly before landing		
            if (canJump && Time.time < lastJumpButtonTime + jumpTimeout)
            {
                verticalSpeed = CalculateJumpVerticalSpeed(jumpHeight);
                DidJump();
                doubleJumped = true;
            }
        }
        else if (canJump && doubleJumped && Input.GetButtonDown("Jump"))
        {
            verticalSpeed = CalculateJumpVerticalSpeed(jumpHeight);
            DidJump();
            doubleJumped = false;
        }
    }

    private void ApplyGravity()
    {
        if (isControllable)	// don't move player at all if not controllable.
        {
            // Apply gravity
            bool jumpButton = Input.GetButton("Jump");

            // When we reach the apex of the jump we send out a message
            if (jumping && !jumpingReachedApex && verticalSpeed <= 0.0)
            {
                jumpingReachedApex = true;
                //SendMessage("DidJumpReachApex", SendMessageOptions.DontRequireReceiver);
            }

            if (IsGrounded() && !inDraftZone)
                verticalSpeed = 0f;
            else
                verticalSpeed -= gravity * Time.deltaTime;
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.tag == "DraftZone")
        {
            inDraftZone = true;
            DraftZone draft = other.gameObject.GetComponent<DraftZone>();
            verticalSpeed += Time.fixedDeltaTime * draft.GetDraft(transform.position).y;
        }
    }

    private void OnTriggerStay(Collider other)
    {
        if (other.tag == "DraftZone")
        {
            DraftZone draft = other.gameObject.GetComponent<DraftZone>();
            verticalSpeed += Time.fixedDeltaTime * draft.GetDraft(transform.position).y;
            jumpingReachedApex = false;
            jumping = true;
            _characterState = CharacterState.Jumping;
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.tag == "DraftZone")
            inDraftZone = false;
    }

    private float CalculateJumpVerticalSpeed(float targetJumpHeight)
    {
        // From the jump height and gravity we deduce the upwards speed 
        // for the character to reach at the apex.
        return Mathf.Sqrt(2 * targetJumpHeight * gravity);
    }

    private void DidJump()
    {
        jumping = true;
        jumpingReachedApex = false;
        lastJumpTime = Time.time;
        lastJumpStartHeight = transform.position.y;
        lastJumpButtonTime = -10;

        _characterState = CharacterState.Jumping;
    }

    private void Update()
    {
        if (!isControllable)
        {
            // kill all inputs if not controllable.
            Input.ResetInputAxes();
        }

        if (Input.GetButtonDown("Jump"))
        {
            lastJumpButtonTime = Time.time;
        }

        UpdateSmoothedMovementDirection();

        // Apply gravity
        // - extra power jump modifies gravity
        // - controlledDescent mode modifies gravity
        ApplyGravity();

        // Apply jumping logic
        ApplyJumping();

        // Calculate actual motion
        Vector3 movement = moveDirection * moveSpeed + new Vector3(0, verticalSpeed, 0) + inAirVelocity;
        movement *= Time.deltaTime;

        // Move the controller
        CharacterController controller = GetComponent<CharacterController>();
        collisionFlags = controller.Move(movement);

        // ANIMATION sector
        if (_animation)
        {
            if (_characterState == CharacterState.Jumping)
            {
                if (!jumpingReachedApex)
                {
                    _animation[jumpAnimation.name].speed = jumpAnimationSpeed;
                    _animation[jumpAnimation.name].wrapMode = WrapMode.ClampForever;
                    _animation.CrossFade(jumpAnimation.name);
                }
                else
                {
                    _animation[jumpAnimation.name].speed = -landAnimationSpeed;
                    _animation[jumpAnimation.name].wrapMode = WrapMode.ClampForever;
                    _animation.CrossFade(jumpAnimation.name);
                }
            }
            else
            {
                if (controller.velocity.sqrMagnitude < 0.1)
                {
                    _animation.CrossFade(idleAnimation.name);
                }
                else
                {
                    if (_characterState == CharacterState.Running)
                    {
                        _animation[runAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0f, runMaxAnimationSpeed);
                        _animation.CrossFade(runAnimation.name);
                    }
                    else if (_characterState == CharacterState.Trotting)
                    {
                        _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0f, trotMaxAnimationSpeed);
                        _animation.CrossFade(walkAnimation.name);
                    }
                    else if (_characterState == CharacterState.Walking)
                    {
                        _animation[walkAnimation.name].speed = Mathf.Clamp(controller.velocity.magnitude, 0f, walkMaxAnimationSpeed);
                        _animation.CrossFade(walkAnimation.name);
                    }

                }
            }
        }
        // ANIMATION sector

        // Set rotation to the move direction
        if (IsGrounded())
        {
            transform.rotation = Quaternion.LookRotation(moveDirection);
        }
        else
        {
            //movingPlatformVelocity = Vector3.zero;
            transform.parent = null;
            _onMovingPlatform = false;
            Vector3 xzMove = movement;
            xzMove.y = 0;
            if (xzMove.sqrMagnitude > 0.001)
            {
                transform.rotation = Quaternion.LookRotation(xzMove);
            }
        }

        // We are in jump mode but just became grounded
        if (IsGrounded())
        {
            lastGroundedTime = Time.time;
            inAirVelocity = Vector3.zero;
            if (jumping)
            {
                jumping = false;
                doubleJumped = false;
                //SendMessage("DidLand", SendMessageOptions.DontRequireReceiver);
            }
        }
    }

    private void OnControllerColliderHit(ControllerColliderHit hit)
    {
        if (hit.gameObject.tag == "DestroyablePlatform")
        {
            hit.gameObject.GetComponent<DestroyablePlatform>().TriggerDestruction();
            return;
        }

        if (hit.gameObject.tag == "MovingPlatform")
        {
            //movingPlatformVelocity = hit.gameObject.GetComponent<MoveAlongPath>().Velocity;
            _onMovingPlatform = true;
            transform.parent = hit.gameObject.transform;
            return;
        }

        //	Debug.DrawRay(hit.point, hit.normal);
        if (hit.moveDirection.y > 0.01)
            return;
    }

    public float GetSpeed()
    {
        return moveSpeed;
    }

    public bool IsJumping()
    {
        return jumping;
    }

    public bool IsGrounded()
    {
        return (collisionFlags & CollisionFlags.CollidedBelow) != 0;
    }

    public Vector3 GetDirection()
    {
        return moveDirection;
    }

    public bool IsMovingBackwards()
    {
        return movingBack;
    }

    public float GetLockCameraTimer()
    {
        return lockCameraTimer;
    }

    public bool IsMoving()
    {
        return Mathf.Abs(Input.GetAxisRaw("Vertical")) + Mathf.Abs(Input.GetAxisRaw("Horizontal")) > 0.5;
    }

    public bool HasJumpReachedApex()
    {
        return jumpingReachedApex;
    }

    public bool IsGroundedWithTimeout()
    {
        return lastGroundedTime + groundedTimeout > Time.time;
    }

    private void Reset()
    {
        gameObject.tag = "Player";
    }
}
