using Cinemachine.Utility;
using UnityEngine;

namespace Cinemachine
{
    /// <summary>
    /// This is a CinemachineComponent in the Aim section of the component pipeline.
    /// Its job is to aim the camera in response to the user's mouse or joystick input.
    ///
    /// The composer does not change the camera's position.  It will only pan and tilt the
    /// camera where it is, in order to get the desired framing.  To move the camera, you have
    /// to use the virtual camera's Body section.
    /// </summary>
    [DocumentationSorting(DocumentationSortingAttribute.Level.UserRef)]
    [AddComponentMenu("")] // Don't display in add component menu
    [SaveDuringPlay]
    public class CinemachinePOV : CinemachineComponentBase
    {
        /// <summary>
        /// Defines the recentering target: Recentering goes here
        /// </summary>
        public enum RecenterTargetMode
        {
            /// <summary>
            /// Just go to 0
            /// </summary>
            None,

            /// <summary>
            /// Axis angles are relative to Follow target's forward
            /// </summary>
            FollowTargetForward,

            /// <summary>
            /// Axis angles are relative to LookAt target's forward
            /// </summary>
            LookAtTargetForward
        }

        /// <summary>
        /// Defines the recentering target: recentering goes here
        /// </summary>
        public RecenterTargetMode m_RecenterTarget = RecenterTargetMode.None;

        /// <summary>The Vertical axis.  Value is -90..90. Controls the vertical orientation</summary>
        [Tooltip("The Vertical axis.  Value is -90..90. Controls the vertical orientation")]
        [AxisStateProperty]
        public AxisState m_VerticalAxis = new AxisState(-70, 70, false, false, 300f, 0.1f, 0.1f, "Mouse Y", true);

        /// <summary>Controls how automatic recentering of the Vertical axis is accomplished</summary>
        [Tooltip("Controls how automatic recentering of the Vertical axis is accomplished")]
        public AxisState.Recentering m_VerticalRecentering = new AxisState.Recentering(false, 1, 2);

        /// <summary>The Horizontal axis.  Value is -180..180.  Controls the horizontal orientation</summary>
        [Tooltip("The Horizontal axis.  Value is -180..180.  Controls the horizontal orientation")]
        [AxisStateProperty]
        public AxisState m_HorizontalAxis = new AxisState(-180, 180, true, false, 300f, 0.1f, 0.1f, "Mouse X", false);

        /// <summary>Controls how automatic recentering of the Horizontal axis is accomplished</summary>
        [Tooltip("Controls how automatic recentering of the Horizontal axis is accomplished")]
        public AxisState.Recentering m_HorizontalRecentering = new AxisState.Recentering(false, 1, 2);

        /// <summary>Obsolete - no longer used</summary>
        [HideInInspector]
        [Tooltip("Obsolete - no longer used")]
        public bool m_ApplyBeforeBody;

        Quaternion m_PreviousCameraRotation;

        /// <summary>True if component is enabled and has a LookAt defined</summary>
        public override bool IsValid { get { return enabled; } }

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

        private void OnValidate()
        {
            m_VerticalAxis.Validate();
            m_VerticalRecentering.Validate();
            m_HorizontalAxis.Validate();
            m_HorizontalRecentering.Validate();
        }

        private void OnEnable()
        {
            UpdateInputAxisProvider();
        }
        
        /// <summary>
        /// API for the inspector.  Internal use only
        /// </summary>
        public void UpdateInputAxisProvider()
        {
            m_HorizontalAxis.SetInputAxisProvider(0, null);
            m_VerticalAxis.SetInputAxisProvider(1, null);
            if (VirtualCamera != null)
            {
                var provider = VirtualCamera.GetInputAxisProvider();
                if (provider != null)
                {
                    m_HorizontalAxis.SetInputAxisProvider(0, provider);
                    m_VerticalAxis.SetInputAxisProvider(1, provider);
                }
            }
        }

        /// <summary>Does nothing</summary>
        /// <param name="state"></param>
        /// <param name="deltaTime"></param>
        public override void PrePipelineMutateCameraState(ref CameraState state, float deltaTime) {}

        /// <summary>Applies the axis values and orients the camera accordingly</summary>
        /// <param name="curState">The current camera state</param>
        /// <param name="deltaTime">Used for calculating damping.  Not used.</param>
        public override void MutateCameraState(ref CameraState curState, float deltaTime)
        {
            if (!IsValid)
                return;

            // Only read joystick when game is playing
            if (deltaTime >= 0 && (!VirtualCamera.PreviousStateIsValid || !CinemachineCore.Instance.IsLive(VirtualCamera)))
                deltaTime = -1;
            if (deltaTime >= 0)
            {
                if (m_HorizontalAxis.Update(deltaTime))
                    m_HorizontalRecentering.CancelRecentering();
                if (m_VerticalAxis.Update(deltaTime))
                    m_VerticalRecentering.CancelRecentering();
            }
            var recenterTarget = GetRecenterTarget();
            m_HorizontalRecentering.DoRecentering(ref m_HorizontalAxis, deltaTime, recenterTarget.x);
            m_VerticalRecentering.DoRecentering(ref m_VerticalAxis, deltaTime, recenterTarget.y);

            // If we have a transform parent, then apply POV in the local space of the parent
            Quaternion rot = Quaternion.Euler(m_VerticalAxis.Value, m_HorizontalAxis.Value, 0);
            Transform parent = VirtualCamera.transform.parent;
            if (parent != null)
                rot = parent.rotation * rot;
            else
                rot = Quaternion.FromToRotation(Vector3.up, curState.ReferenceUp) * rot;
            curState.RawOrientation = rot;

            if (VirtualCamera.PreviousStateIsValid)
                curState.PositionDampingBypass = UnityVectorExtensions.SafeFromToRotation(
                    m_PreviousCameraRotation * Vector3.forward, 
                    rot * Vector3.forward, curState.ReferenceUp).eulerAngles;
            m_PreviousCameraRotation = rot;
        }

        /// <summary>
        /// Get the horizonmtal and vertical angles that correspong to "at rest" position.
        /// </summary>
        /// <returns>X is horizontal angle (rot Y) and Y is vertical angle (rot X)</returns>
        public Vector2 GetRecenterTarget()
        {
            Transform t = null;
            switch (m_RecenterTarget)
            {
                case RecenterTargetMode.FollowTargetForward: t = VirtualCamera.Follow; break;
                case RecenterTargetMode.LookAtTargetForward: t = VirtualCamera.LookAt; break;
                default: break;
            }
            if (t != null)
            {
                var fwd = t.forward;
                Transform parent = VirtualCamera.transform.parent;
                if (parent != null)
                    fwd = parent.rotation * fwd;
                var v = Quaternion.FromToRotation(Vector3.forward, fwd).eulerAngles;
                return new Vector2(NormalizeAngle(v.y), NormalizeAngle(v.x));
            }
            return Vector2.zero;
        }

        // Normalize angle value to [-180, 180] degrees.
        static float NormalizeAngle(float angle)
        {
            return ((angle + 180) % 360) - 180; 
        }

        /// <summary>
        /// Force the virtual camera to assume a given position and orientation.  
        /// Procedural placement then takes over
        /// </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)
        {
            SetAxesForRotation(rot);
        }

        /// <summary>Notification that this virtual camera is going live.
        /// Base class implementation does nothing.</summary>
        /// <param name="fromCam">The camera being deactivated.  May be null.</param>
        /// <param name="worldUp">Default world Up, set by the CinemachineBrain</param>
        /// <param name="deltaTime">Delta time for time-based effects (ignore if less than or equal to 0)</param>
        /// <param name="transitionParams">Transition settings for this vcam</param>
        /// <returns>True if the vcam should do an internal update as a result of this call</returns>
        public override bool OnTransitionFromCamera(
            ICinemachineCamera fromCam, Vector3 worldUp, float deltaTime,
            ref CinemachineVirtualCameraBase.TransitionParams transitionParams)
        {
            m_HorizontalRecentering.DoRecentering(ref m_HorizontalAxis, -1, 0);
            m_VerticalRecentering.DoRecentering(ref m_VerticalAxis, -1, 0);
            m_HorizontalRecentering.CancelRecentering();
            m_VerticalRecentering.CancelRecentering();
            if (fromCam != null && transitionParams.m_InheritPosition  
                && !CinemachineCore.Instance.IsLiveInBlend(VirtualCamera))
            {
                SetAxesForRotation(fromCam.State.RawOrientation);
                return true;
            }
            return false;
        }
        
        /// <summary>POV is controlled by input.</summary>
        public override bool RequiresUserInput => true;

        void SetAxesForRotation(Quaternion targetRot)
        {
            Vector3 up = VcamState.ReferenceUp;
            Vector3 fwd = Vector3.forward;
            Transform parent = VirtualCamera.transform.parent;
            if (parent != null)
                fwd = parent.rotation * fwd;

            m_HorizontalAxis.Value = 0;
            m_HorizontalAxis.Reset();
            Vector3 targetFwd = targetRot * Vector3.forward;
            Vector3 a = fwd.ProjectOntoPlane(up);
            Vector3 b = targetFwd.ProjectOntoPlane(up);
            if (!a.AlmostZero() && !b.AlmostZero())
                m_HorizontalAxis.Value = Vector3.SignedAngle(a, b, up);

            m_VerticalAxis.Value = 0;
            m_VerticalAxis.Reset();
            fwd = Quaternion.AngleAxis(m_HorizontalAxis.Value, up) * fwd;
            Vector3 right = Vector3.Cross(up, fwd);
            if (!right.AlmostZero())
                m_VerticalAxis.Value = Vector3.SignedAngle(fwd, targetFwd, right);
        }
    }
}