using UnityEngine;
using System.Collections;


public class ThirdPersonCamera : MonoBehaviour {

public Transform cameraTransform;
private Transform _target;

public float distance = 7.0f;

public float height = 3.0f;

public float angularSmoothLag = 0.3f;
public float angularMaxSpeed = 15.0f;

public float heightSmoothLag = 0.3f;

public float snapSmoothLag = 0.2f;
public float snapMaxSpeed = 720.0f;

public float clampHeadPositionScreenSpace = 0.75f;

public float lockCameraTimeout = 0.2f;

private Vector3 headOffset = Vector3.zero;
private Vector3 centerOffset = Vector3.zero;

private float heightVelocity = 0.0f;
private float  angleVelocity = 0.0f;
private bool snap = false;
private ThirdPersonController controller;
private float targetHeight = 100000.0f; 

void Awake ()
{
    if(!cameraTransform && Camera.main)
        cameraTransform = Camera.main.transform;
    if(!cameraTransform) {
        Debug.Log("Please assign a camera to the ThirdPersonCamera script.");
        enabled = false;    
    }

    _target = transform;
    if (_target)
    {
        controller = _target.GetComponent<ThirdPersonController>();
    }

    if (controller)
    {
        CharacterController characterController  = (CharacterController)_target.collider;
        centerOffset = characterController.bounds.center - _target.position;
        headOffset = centerOffset;
        headOffset.y = characterController.bounds.max.y - _target.position.y;
    }
    else
        Debug.Log("Please assign a target to the camera that has a ThirdPersonController script attached.");

    Cut(_target, centerOffset);
}

void DebugDrawStuff ()
{
    Debug.DrawLine(_target.position, _target.position + headOffset);

}

float  AngleDistance (float a , float b )
{
    a = Mathf.Repeat(a, 360);
    b = Mathf.Repeat(b, 360);

    return Mathf.Abs(b - a);
}

void  Apply (Transform dummyTarget, Vector3 dummyCenter)
{
    // Early out if we don't have a target
    if (!controller)
        return;

    Vector3 targetCenter = _target.position + centerOffset;
    Vector3 targetHead = _target.position + headOffset;

//    DebugDrawStuff();

    // Calculate the current & target rotation angles
    float originalTargetAngle = _target.eulerAngles.y;
    float currentAngle = cameraTransform.eulerAngles.y;

    // Adjust real target angle when camera is locked
    float targetAngle = originalTargetAngle; 

    // When pressing Fire2 (alt) the camera will snap to the target direction real quick.
    // It will stop snapping when it reaches the target
    if (Input.GetButton("Fire2"))
        snap = true;

    if (snap)
    {
        // We are close to the target, so we can stop snapping now!
        if (AngleDistance (currentAngle, originalTargetAngle) < 3.0)
            snap = false;

        currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, snapSmoothLag, snapMaxSpeed);
    }
    // Normal camera motion
    else
    {

        if (controller.GetLockCameraTimer () < lockCameraTimeout)
        {
            targetAngle = currentAngle;
        }

        // Lock the camera when moving backwards!
        // * It is really confusing to do 180 degree spins when turning around.
        if (AngleDistance (currentAngle, targetAngle) > 160 && controller.IsMovingBackwards ())
            targetAngle += 180;

        currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, angularSmoothLag, angularMaxSpeed);
    }

    // When jumping don't move camera upwards but only down!
    if (controller.IsJumping ())
    {
        // We'd be moving the camera upwards, do that only if it's really high
        float newTargetHeight = targetCenter.y + height;
        if (newTargetHeight < targetHeight || newTargetHeight - targetHeight > 5)
            targetHeight = targetCenter.y + height;
    }
    // When walking always update the target height
    else
    {
        targetHeight = targetCenter.y + height;
    }

    // Damp the height
    float currentHeight = cameraTransform.position.y;
    currentHeight = Mathf.SmoothDamp (currentHeight, targetHeight, ref heightVelocity, heightSmoothLag);

    // Convert the angle into a rotation, by which we then reposition the camera
    Quaternion currentRotation = Quaternion.Euler (0, currentAngle, 0);

    // Set the position of the camera on the x-z plane to:
    // distance meters behind the target
    cameraTransform.position = targetCenter;
    cameraTransform.position += currentRotation * Vector3.back * distance;

    // Set the height of the camera
    cameraTransform.position = new Vector3(cameraTransform.position.x,currentHeight,cameraTransform.position.z);

    // Always look at the target    
    SetUpRotation(targetCenter, targetHead);
}

void LateUpdate () {
    Apply (transform, Vector3.zero);
}

void  Cut (Transform dummyTarget , Vector3 dummyCenter)
{
    float oldHeightSmooth = heightSmoothLag;
    float oldSnapMaxSpeed = snapMaxSpeed;
    float oldSnapSmooth = snapSmoothLag;

    snapMaxSpeed = 10000;
    snapSmoothLag = 0.001f;
    heightSmoothLag = 0.001f;

    snap = true;
    Apply (transform, Vector3.zero);

    heightSmoothLag = oldHeightSmooth;
    snapMaxSpeed = oldSnapMaxSpeed;
    snapSmoothLag = oldSnapSmooth;
}

void SetUpRotation (Vector3 centerPos,Vector3  headPos)
{
    // Now it's getting hairy. The devil is in the details here, the big issue is jumping of course.
    // * When jumping up and down we don't want to center the guy in screen space.
    //  This is important to give a feel for how high you jump and avoiding large camera movements.
    //   
    // * At the same time we dont want him to ever go out of screen and we want all rotations to be totally smooth.
    //
    // So here is what we will do:
    //
    // 1. We first find the rotation around the y axis. Thus he is always centered on the y-axis
    // 2. When grounded we make him be centered
    // 3. When jumping we keep the camera rotation but rotate the camera to get him back into view if his head is above some threshold
    // 4. When landing we smoothly interpolate towards centering him on screen
    Vector3 cameraPos = cameraTransform.position;
    Vector3 offsetToCenter = centerPos - cameraPos;

    // Generate base rotation only around y-axis
    Quaternion yRotation = Quaternion.LookRotation(new Vector3(offsetToCenter.x, 0, offsetToCenter.z));

    Vector3 relativeOffset = Vector3.forward * distance + Vector3.down * height;
    cameraTransform.rotation = yRotation * Quaternion.LookRotation(relativeOffset);

    // Calculate the projected center position and top position in world space
    Ray centerRay = cameraTransform.camera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 1f));
    Ray topRay = cameraTransform.camera.ViewportPointToRay(new Vector3(0.5f, clampHeadPositionScreenSpace, 1f));

    Vector3 centerRayPos = centerRay.GetPoint(distance);
    Vector3 topRayPos = topRay.GetPoint(distance);

    float centerToTopAngle = Vector3.Angle(centerRay.direction, topRay.direction);

    float heightToAngle = centerToTopAngle / (centerRayPos.y - topRayPos.y);

    float extraLookAngle = heightToAngle * (centerRayPos.y - centerPos.y);
    if (extraLookAngle < centerToTopAngle)
    {
        extraLookAngle = 0;
    }
    else
    {
        extraLookAngle = extraLookAngle - centerToTopAngle;
        cameraTransform.rotation *= Quaternion.Euler(-extraLookAngle, 0, 0);
    }
}

    Vector3 GetCenterOffset ()
    {
        return centerOffset;
    }

}