using System;
using Cinemachine.Utility;
using UnityEngine;

namespace Cinemachine
{
    /// <summary>
    /// This is a CinemachineComponent in the Body section of the component pipeline.
    /// Its job is to position the camera in a fixed relationship to the vcam's Follow
    /// target object, with offsets and damping.
    ///
    /// The Tansposer will only change the camera's position in space.  It will not
    /// re-orient or otherwise aim the camera.  To to that, you need to instruct
    /// the vcam in the Aim section of its pipeline.
    /// </summary>
    [DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)]
    [AddComponentMenu("")] // Don't display in add component menu
    [SaveDuringPlay]
    public class CinemachineTransposer : CinemachineComponentBase
    {
        /// <summary>
        /// The coordinate space to use when interpreting the offset from the target
        /// </summary>
        [DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)]
        public enum BindingMode
        {
            /// <summary>
            /// Camera will be bound to the Follow target using a frame of reference consisting
            /// of the target's local frame at the moment when the virtual camera was enabled,
            /// or when the target was assigned.
            /// </summary>
            LockToTargetOnAssign = 0,
            /// <summary>
            /// Camera will be bound to the Follow target using a frame of reference consisting
            /// of the target's local frame, with the tilt and roll zeroed out.
            /// </summary>
            LockToTargetWithWorldUp = 1,
            /// <summary>
            /// Camera will be bound to the Follow target using a frame of reference consisting
            /// of the target's local frame, with the roll zeroed out.
            /// </summary>
            LockToTargetNoRoll = 2,
            /// <summary>
            /// Camera will be bound to the Follow target using the target's local frame.
            /// </summary>
            LockToTarget = 3,
            /// <summary>Camera will be bound to the Follow target using a world space offset.</summary>
            WorldSpace = 4,
            /// <summary>Offsets will be calculated relative to the target, using Camera-local axes</summary>
            SimpleFollowWithWorldUp = 5
        }
        /// <summary>The coordinate space to use when interpreting the offset from the target</summary>
        [Tooltip("The coordinate space to use when interpreting the offset from the target.  This is also "
            + "used to set the camera's Up vector, which will be maintained when aiming the camera.")]
        public BindingMode m_BindingMode = BindingMode.LockToTargetWithWorldUp;

        /// <summary>The distance which the transposer will attempt to maintain from the transposer subject</summary>
        [Tooltip("The distance vector that the transposer will attempt to maintain from the Follow target")]
        public Vector3 m_FollowOffset = Vector3.back * 10f;

        /// <summary>How aggressively the camera tries to maintain the offset in the X-axis.
        /// Small numbers are more responsive, rapidly translating the camera to keep the target's
        /// x-axis offset.  Larger numbers give a more heavy slowly responding camera.
        /// Using different settings per axis can yield a wide range of camera behaviors</summary>
        [Range(0f, 20f)]
        [Tooltip("How aggressively the camera tries to maintain the offset in the X-axis.  Small numbers "
            + "are more responsive, rapidly translating the camera to keep the target's x-axis offset.  "
            + "Larger numbers give a more heavy slowly responding camera. Using different settings per "
            + "axis can yield a wide range of camera behaviors.")]
        public float m_XDamping = 1f;

        /// <summary>How aggressively the camera tries to maintain the offset in the Y-axis.
        /// Small numbers are more responsive, rapidly translating the camera to keep the target's
        /// y-axis offset.  Larger numbers give a more heavy slowly responding camera.
        /// Using different settings per axis can yield a wide range of camera behaviors</summary>
        [Range(0f, 20f)]
        [Tooltip("How aggressively the camera tries to maintain the offset in the Y-axis.  Small numbers "
            + "are more responsive, rapidly translating the camera to keep the target's y-axis offset.  "
            + "Larger numbers give a more heavy slowly responding camera. Using different settings per "
            + "axis can yield a wide range of camera behaviors.")]
        public float m_YDamping = 1f;

        /// <summary>How aggressively the camera tries to maintain the offset in the Z-axis.
        /// Small numbers are more responsive, rapidly translating the camera to keep the
        /// target's z-axis offset.  Larger numbers give a more heavy slowly responding camera.
        /// Using different settings per axis can yield a wide range of camera behaviors</summary>
        [Range(0f, 20f)]
        [Tooltip("How aggressively the camera tries to maintain the offset in the Z-axis.  "
            + "Small numbers are more responsive, rapidly translating the camera to keep the "
            + "target's z-axis offset.  Larger numbers give a more heavy slowly responding camera. "
            + "Using different settings per axis can yield a wide range of camera behaviors.")]
        public float m_ZDamping = 1f;

        /// <summary>How to calculate the angular damping for the target orientation</summary>
        public enum AngularDampingMode
        {
            /// <summary>Use Euler angles to specify damping values.
            /// Subject to gimbal-lock fwhen pitch is steep.</summary>
            Euler,
            /// <summary>
            /// Use quaternions to calculate angular damping.
            /// No per-channel control, but not susceptible to gimbal-lock</summary>
            Quaternion
        }

        /// <summary>How to calculate the angular damping for the target orientation.
        /// Use Quaternion if you expect the target to take on very steep pitches, which would
        /// be subject to gimbal lock if Eulers are used.</summary>
        public AngularDampingMode m_AngularDampingMode = AngularDampingMode.Euler;

        /// <summary>How aggressively the camera tries to track the target rotation's X angle.
        /// Small numbers are more responsive.  Larger numbers give a more heavy slowly responding camera.</summary>
        [Range(0f, 20f)]
        [Tooltip("How aggressively the camera tries to track the target rotation's X angle.  "
            + "Small numbers are more responsive.  Larger numbers give a more heavy slowly responding camera.")]
        public float m_PitchDamping = 0;

        /// <summary>How aggressively the camera tries to track the target rotation's Y angle.
        /// Small numbers are more responsive.  Larger numbers give a more heavy slowly responding camera.</summary>
        [Range(0f, 20f)]
        [Tooltip("How aggressively the camera tries to track the target rotation's Y angle.  "
            + "Small numbers are more responsive.  Larger numbers give a more heavy slowly responding camera.")]
        public float m_YawDamping = 0;

        /// <summary>How aggressively the camera tries to track the target rotation's Z angle.
        /// Small numbers are more responsive.  Larger numbers give a more heavy slowly responding camera.</summary>
        [Range(0f, 20f)]
        [Tooltip("How aggressively the camera tries to track the target rotation's Z angle.  "
            + "Small numbers are more responsive.  Larger numbers give a more heavy slowly responding camera.")]
        public float m_RollDamping = 0f;

        /// <summary>How aggressively the camera tries to track the target's orientation.
        /// Small numbers are more responsive.  Larger numbers give a more heavy slowly responding camera.</summary>
        [Range(0f, 20f)]
        [Tooltip("How aggressively the camera tries to track the target's orientation.  "
            + "Small numbers are more responsive.  Larger numbers give a more heavy slowly responding camera.")]
        public float m_AngularDamping = 0f;

        /// <summary>Derived classes should call this from their OnValidate() implementation</summary>
        protected virtual void OnValidate()
        {
            m_FollowOffset = EffectiveOffset;
        }

        /// <summary>Hide the offset in int inspector.  Used by FreeLook.</summary>
        public bool HideOffsetInInspector { get; set; }

        /// <summary>Get the target offset, with sanitization</summary>
        public Vector3 EffectiveOffset
        {
            get
            {
                Vector3 offset = m_FollowOffset;
                if (m_BindingMode == BindingMode.SimpleFollowWithWorldUp)
                {
                    offset.x = 0;
                    offset.z = -Mathf.Abs(offset.z);
                }
                return offset;
            }
        }

        /// <summary>True if component is enabled and has a valid Follow target</summary>
        public override bool IsValid { get { return enabled && FollowTarget != null; } }

        /// <summary>Get the Cinemachine Pipeline stage that this component implements.
        /// Always returns the Body stage</summary>
        public override CinemachineCore.Stage Stage { get { return CinemachineCore.Stage.Body; } }

        /// <summary>
        /// Report maximum damping time needed for this component.
        /// </summary>
        /// <returns>Highest damping setting in this component</returns>
        public override float GetMaxDampTime() 
        { 
            var d = Damping;
            var d2 = AngularDamping;
            var a = Mathf.Max(d.x, Mathf.Max(d.y, d.z)); 
            var b = Mathf.Max(d2.x, Mathf.Max(d2.y, d2.z)); 
            return Mathf.Max(a, b); 
        }

        /// <summary>Positions the virtual camera according to the transposer rules.</summary>
        /// <param name="curState">The current camera state</param>
        /// <param name="deltaTime">Used for damping.  If less than 0, no damping is done.</param>
        public override void MutateCameraState(ref CameraState curState, float deltaTime)
        {
            InitPrevFrameStateInfo(ref curState, deltaTime);
            if (IsValid)
            {
                Vector3 offset = EffectiveOffset;
                TrackTarget(deltaTime, curState.ReferenceUp, offset, out Vector3 pos, out Quaternion orient);
                offset = orient * offset;

                // Respect minimum target distance on XZ plane
                var targetPosition = FollowTargetPosition;
                pos += GetOffsetForMinimumTargetDistance(
                    pos, offset, curState.RawOrientation * Vector3.forward,
                    curState.ReferenceUp, targetPosition);
                    
                curState.RawPosition = pos + offset;
                curState.ReferenceUp = orient * Vector3.up;
            }
        }

        /// <summary>This is called to notify the us that a target got warped,
        /// so that we can update its internal state to make the camera
        /// also warp seamlessy.</summary>
        /// <param name="target">The object that was warped</param>
        /// <param name="positionDelta">The amount the target's position changed</param>
        public override void OnTargetObjectWarped(Transform target, Vector3 positionDelta)
        {
            base.OnTargetObjectWarped(target, positionDelta);
            if (target == FollowTarget)
                m_PreviousTargetPosition += positionDelta;
        }

        /// <summary>
        /// Force the virtual camera to assume a given position and orientation
        /// </summary>
        /// <param name="pos">Worldspace pposition to take</param>
        /// <param name="rot">Worldspace orientation to take</param>
        public override void ForceCameraPosition(Vector3 pos, Quaternion rot)
        {
            base.ForceCameraPosition(pos, rot);

            // Infer target pos from camera
            var targetRot = m_BindingMode == BindingMode.SimpleFollowWithWorldUp 
                ? rot : GetReferenceOrientation(VirtualCamera.State.ReferenceUp);
            m_PreviousTargetPosition = pos - targetRot * EffectiveOffset;
        }
        
        /// <summary>Initializes the state for previous frame if appropriate.</summary>
        /// <param name="curState">The current camera state</param>
        /// <param name="deltaTime">Current effective deltaTime.</param>
        protected void InitPrevFrameStateInfo(
            ref CameraState curState, float deltaTime)
        {
            bool prevStateValid = deltaTime >= 0 && VirtualCamera.PreviousStateIsValid;
            if (m_previousTarget != FollowTarget || !prevStateValid)
            {
                m_previousTarget = FollowTarget;
                m_targetOrientationOnAssign = FollowTargetRotation;
            }
            if (!prevStateValid)
            {
                m_PreviousTargetPosition = FollowTargetPosition;
                m_PreviousReferenceOrientation = GetReferenceOrientation(curState.ReferenceUp);
            }
        }

        /// <summary>Positions the virtual camera according to the transposer rules.</summary>
        /// <param name="deltaTime">Used for damping.  If less than 0, no damping is done.</param>
        /// <param name="up">Current camera up</param>
        /// <param name="desiredCameraOffset">Where we want to put the camera relative to the follow target</param>
        /// <param name="outTargetPosition">Resulting camera position</param>
        /// <param name="outTargetOrient">Damped target orientation</param>
        protected void TrackTarget(
            float deltaTime, Vector3 up, Vector3 desiredCameraOffset,
            out Vector3 outTargetPosition, out Quaternion outTargetOrient)
        {
            var targetOrientation = GetReferenceOrientation(up);
            var dampedOrientation = targetOrientation;
            bool prevStateValid = deltaTime >= 0 && VirtualCamera.PreviousStateIsValid;
            if (prevStateValid)
            {
                if (m_AngularDampingMode == AngularDampingMode.Quaternion
                    && m_BindingMode == BindingMode.LockToTarget)
                {
                    float t = VirtualCamera.DetachedFollowTargetDamp(1, m_AngularDamping, deltaTime);
                    dampedOrientation = Quaternion.Slerp(
                        m_PreviousReferenceOrientation, targetOrientation, t);
                }
                else
                {
                    var relative = (Quaternion.Inverse(m_PreviousReferenceOrientation)
                        * targetOrientation).eulerAngles;
                    for (int i = 0; i < 3; ++i)
                    {
                        if (Mathf.Abs(relative[i]) < 0.01f) // correct for precision drift
                            relative[i] = 0;
                        else if (relative[i] > 180)
                            relative[i] -= 360;
                    }
                    relative = VirtualCamera.DetachedFollowTargetDamp(relative, AngularDamping, deltaTime);
                    dampedOrientation = m_PreviousReferenceOrientation * Quaternion.Euler(relative);
                }
            }
            m_PreviousReferenceOrientation = dampedOrientation;

            var targetPosition = FollowTargetPosition;
            var currentPosition = m_PreviousTargetPosition;
            var previousOffset = prevStateValid ? m_PreviousOffset : desiredCameraOffset;
            var offsetDelta = desiredCameraOffset - previousOffset;
            if (offsetDelta.sqrMagnitude > 0.01f)
            {
                var q = UnityVectorExtensions.SafeFromToRotation(
                    m_PreviousOffset.ProjectOntoPlane(up), 
                    desiredCameraOffset.ProjectOntoPlane(up), up);
                currentPosition = targetPosition + q * (m_PreviousTargetPosition - targetPosition);
            }
            m_PreviousOffset = desiredCameraOffset;

            // Adjust for damping, which is done in camera-offset-local coords
            var positionDelta = targetPosition - currentPosition;
            if (prevStateValid)
            {
                Quaternion dampingSpace;
                if (desiredCameraOffset.AlmostZero())
                    dampingSpace = VcamState.RawOrientation;
                else
                    dampingSpace = Quaternion.LookRotation(dampedOrientation * desiredCameraOffset, up);
                var localDelta = Quaternion.Inverse(dampingSpace) * positionDelta;
                localDelta = VirtualCamera.DetachedFollowTargetDamp(localDelta, Damping, deltaTime);
                positionDelta = dampingSpace * localDelta;
            }
            currentPosition += positionDelta;

            outTargetPosition = m_PreviousTargetPosition = currentPosition;
            outTargetOrient = dampedOrientation;
        }

        /// <summary>Return a new damped target position that respects the minimum 
        /// distance from the real target</summary>
        /// <param name="dampedTargetPos">The effective position of the target, after damping</param>
        /// <param name="cameraOffset">Desired camera offset from target</param>
        /// <param name="cameraFwd">Current camera local +Z direction</param>
        /// <param name="up">Effective world up</param>
        /// <param name="actualTargetPos">The real undamped target position</param>
        /// <returns>New camera offset, potentially adjusted to respect minimum distance from target</returns>
        protected Vector3 GetOffsetForMinimumTargetDistance(
            Vector3 dampedTargetPos, Vector3 cameraOffset, 
            Vector3 cameraFwd, Vector3 up, Vector3 actualTargetPos)
        {
            var posOffset = Vector3.zero;
            if (VirtualCamera.FollowTargetAttachment > 1 - Epsilon)
            {
                cameraOffset = cameraOffset.ProjectOntoPlane(up);
                var minDistance = cameraOffset.magnitude * 0.2f;
                if (minDistance > 0)
                {
                    actualTargetPos = actualTargetPos.ProjectOntoPlane(up);
                    dampedTargetPos = dampedTargetPos.ProjectOntoPlane(up);
                    var cameraPos = dampedTargetPos + cameraOffset;
                    var d = Vector3.Dot(
                        actualTargetPos - cameraPos,
                        (dampedTargetPos - cameraPos).normalized);
                    if (d < minDistance)
                    {
                        var dir = actualTargetPos - dampedTargetPos;
                        var len = dir.magnitude;
                        if (len < 0.01f)
                            dir = -cameraFwd.ProjectOntoPlane(up);
                        else
                            dir /= len;
                        posOffset = dir * (minDistance - d);
                    }
                    m_PreviousTargetPosition += posOffset;
                }
            }
            return posOffset;
        }

        /// <summary>
        /// Damping speeds for each of the 3 axes of the offset from target
        /// </summary>
        protected Vector3 Damping
        {
            get
            {
                switch (m_BindingMode)
                {
                    case BindingMode.SimpleFollowWithWorldUp:
                        return new Vector3(0, m_YDamping, m_ZDamping);
                    default:
                        return new Vector3(m_XDamping, m_YDamping, m_ZDamping);
                }
            }
        }

        /// <summary>
        /// Damping speeds for each of the 3 axes of the target's rotation
        /// </summary>
        protected Vector3 AngularDamping
        {
            get
            {
                switch (m_BindingMode)
                {
                    case BindingMode.LockToTargetNoRoll:
                        return new Vector3(m_PitchDamping, m_YawDamping, 0);
                    case BindingMode.LockToTargetWithWorldUp:
                        return new Vector3(0, m_YawDamping, 0);
                    case BindingMode.LockToTargetOnAssign:
                    case BindingMode.WorldSpace:
                    case BindingMode.SimpleFollowWithWorldUp:
                        return Vector3.zero;
                    default:
                        return new Vector3(m_PitchDamping, m_YawDamping, m_RollDamping);
                }
            }
        }

        /// <summary>Internal API for the Inspector Editor, so it can draw a marker at the target</summary>
        /// <param name="worldUp">Current effective world up</param>
        /// <returns>The position of the Follow target</returns>
        public virtual Vector3 GetTargetCameraPosition(Vector3 worldUp)
        {
            if (!IsValid)
                return Vector3.zero;
            return FollowTargetPosition + GetReferenceOrientation(worldUp) * EffectiveOffset;
        }

        /// <summary>State information for damping</summary>
        Vector3 m_PreviousTargetPosition = Vector3.zero;
        Quaternion m_PreviousReferenceOrientation = Quaternion.identity;
        Quaternion m_targetOrientationOnAssign = Quaternion.identity;
        Vector3 m_PreviousOffset;
        Transform m_previousTarget = null;

        /// <summary>Internal API for the Inspector Editor, so it can draw a marker at the target</summary>
        /// <param name="worldUp">Current effective world up</param>
        /// <returns>The rotation of the Follow target, as understood by the Transposer.  
        /// This is not necessarily the same thing as the actual target rotation</returns>
        public Quaternion GetReferenceOrientation(Vector3 worldUp)
        {
            if (m_BindingMode == BindingMode.WorldSpace)
                return Quaternion.identity;
            if (FollowTarget != null)
            {
                Quaternion targetOrientation = FollowTarget.rotation;
                switch (m_BindingMode)
                {
                    case BindingMode.LockToTargetOnAssign:
                        return m_targetOrientationOnAssign;
                    case BindingMode.LockToTargetWithWorldUp:
                    {
                        Vector3 fwd = (targetOrientation * Vector3.forward).ProjectOntoPlane(worldUp);
                        if (fwd.AlmostZero())
                            break;
                        return Quaternion.LookRotation(fwd, worldUp);
                    }
                    case BindingMode.LockToTargetNoRoll:
                        return Quaternion.LookRotation(targetOrientation * Vector3.forward, worldUp);
                    case BindingMode.LockToTarget:
                        return targetOrientation;
                    case BindingMode.SimpleFollowWithWorldUp:
                    {
                        Vector3 fwd = (FollowTargetPosition - VcamState.RawPosition).ProjectOntoPlane(worldUp);
                        if (fwd.AlmostZero())
                            break;
                        return Quaternion.LookRotation(fwd, worldUp);
                    }
                }
            }
            // Gimbal lock situation - use previous orientation if it exists
#if UNITY_2019_1_OR_NEWER
            return m_PreviousReferenceOrientation.normalized;
#else
            return m_PreviousReferenceOrientation.Normalized();
#endif
        }
    }
}