using System;
using UnityEngine.InputSystem.LowLevel;

namespace UnityEngine.InputSystem.XR
{
    /// <summary>
    /// The <see cref="TrackedPoseDriver"/> component applies the current pose value of a tracked device
    /// to the <see cref="Transform"/> of the <see cref="GameObject"/>.
    /// <see cref="TrackedPoseDriver"/> can track multiple types of devices including XR HMDs, controllers, and remotes.
    /// </summary>
    /// <remarks>
    /// For <see cref="positionInput"/> and <see cref="rotationInput"/>, if an action is directly defined
    /// in the <see cref="InputActionProperty"/>, as opposed to a reference to an action externally defined
    /// in an <see cref="InputActionAsset"/>, the action will automatically be enabled and disabled by this
    /// behavior during <see cref="OnEnable"/> and <see cref="OnDisable"/>. The enabled state for actions
    /// externally defined must be managed externally from this behavior.
    /// </remarks>
    [Serializable]
    [AddComponentMenu("XR/Tracked Pose Driver (Input System)")]
    public class TrackedPoseDriver : MonoBehaviour, ISerializationCallbackReceiver
    {
        /// <summary>
        /// Options for which <see cref="Transform"/> properties to update.
        /// </summary>
        /// <seealso cref="trackingType"/>
        public enum TrackingType
        {
            /// <summary>
            /// Update both rotation and position.
            /// </summary>
            RotationAndPosition,

            /// <summary>
            /// Update rotation only.
            /// </summary>
            RotationOnly,

            /// <summary>
            /// Update position only.
            /// </summary>
            PositionOnly,
        }

        /// <summary>
        /// These bit flags correspond with <c>UnityEngine.XR.InputTrackingState</c>
        /// but that enum is not used to avoid adding a dependency to the XR module.
        /// Only the Position and Rotation flags are used by this class, so velocity and acceleration flags are not duplicated here.
        /// </summary>
        [Flags]
        enum TrackingStates
        {
            /// <summary>
            /// Position and rotation are not valid.
            /// </summary>
            None,

            /// <summary>
            /// Position is valid.
            /// See <c>InputTrackingState.Position</c>.
            /// </summary>
            Position = 1 << 0,

            /// <summary>
            /// Rotation is valid.
            /// See <c>InputTrackingState.Rotation</c>.
            /// </summary>
            Rotation = 1 << 1,
        }

        [SerializeField, Tooltip("Which Transform properties to update.")]
        TrackingType m_TrackingType;
        /// <summary>
        /// The tracking type being used by the Tracked Pose Driver
        /// to control which <see cref="Transform"/> properties to update.
        /// </summary>
        /// <seealso cref="TrackingType"/>
        public TrackingType trackingType
        {
            get => m_TrackingType;
            set => m_TrackingType = value;
        }

        /// <summary>
        /// Options for which phases of the player loop will update <see cref="Transform"/> properties.
        /// </summary>
        /// <seealso cref="updateType"/>
        /// <seealso cref="InputSystem.onAfterUpdate"/>
        public enum UpdateType
        {
            /// <summary>
            /// Update after the Input System has completed an update and right before rendering.
            /// This is the recommended and default option to minimize lag for XR tracked devices.
            /// </summary>
            /// <seealso cref="InputUpdateType.BeforeRender"/>
            UpdateAndBeforeRender,

            /// <summary>
            /// Update after the Input System has completed an update except right before rendering.
            /// </summary>
            /// <remarks>
            /// This may be dynamic update, fixed update, or a manual update depending on the Update Mode
            /// project setting for Input System.
            /// </remarks>
            Update,

            /// <summary>
            /// Update after the Input System has completed an update right before rendering.
            /// </summary>
            /// <remarks>
            /// Note that this update mode may not trigger if there are no XR devices added which use before render timing.
            /// </remarks>
            /// <seealso cref="InputUpdateType.BeforeRender"/>
            /// <seealso cref="InputDevice.updateBeforeRender"/>
            BeforeRender,
        }

        [SerializeField, Tooltip("Updates the Transform properties after these phases of Input System event processing.")]
        UpdateType m_UpdateType = UpdateType.UpdateAndBeforeRender;
        /// <summary>
        /// The update type being used by the Tracked Pose Driver
        /// to control which phases of the player loop will update <see cref="Transform"/> properties.
        /// </summary>
        /// <seealso cref="UpdateType"/>
        public UpdateType updateType
        {
            get => m_UpdateType;
            set => m_UpdateType = value;
        }

        [SerializeField, Tooltip("Ignore Tracking State and always treat the input pose as valid.")]
        bool m_IgnoreTrackingState;
        /// <summary>
        /// Ignore tracking state and always treat the input pose as valid when updating the <see cref="Transform"/> properties.
        /// The recommended value is <see langword="false"/> so the tracking state input is used.
        /// </summary>
        /// <seealso cref="trackingStateInput"/>
        public bool ignoreTrackingState
        {
            get => m_IgnoreTrackingState;
            set => m_IgnoreTrackingState = value;
        }

        [SerializeField, Tooltip("The input action to read the position value of a tracked device. Must be a Vector 3 control type.")]
        InputActionProperty m_PositionInput;
        /// <summary>
        /// The input action to read the position value of a tracked device.
        /// Must support reading a value of type <see cref="Vector3"/>.
        /// </summary>
        /// <seealso cref="rotationInput"/>
        public InputActionProperty positionInput
        {
            get => m_PositionInput;
            set
            {
                if (Application.isPlaying)
                    UnbindPosition();

                m_PositionInput = value;

                if (Application.isPlaying && isActiveAndEnabled)
                    BindPosition();
            }
        }

        [SerializeField, Tooltip("The input action to read the rotation value of a tracked device. Must be a Quaternion control type.")]
        InputActionProperty m_RotationInput;
        /// <summary>
        /// The input action to read the rotation value of a tracked device.
        /// Must support reading a value of type <see cref="Quaternion"/>.
        /// </summary>
        /// <seealso cref="positionInput"/>
        public InputActionProperty rotationInput
        {
            get => m_RotationInput;
            set
            {
                if (Application.isPlaying)
                    UnbindRotation();

                m_RotationInput = value;

                if (Application.isPlaying && isActiveAndEnabled)
                    BindRotation();
            }
        }

        [SerializeField, Tooltip("The input action to read the tracking state value of a tracked device. Identifies if position and rotation have valid data. Must be an Integer control type.")]
        InputActionProperty m_TrackingStateInput;
        /// <summary>
        /// The input action to read the tracking state value of a tracked device.
        /// Identifies if position and rotation have valid data.
        /// Must support reading a value of type <see cref="int"/>.
        /// </summary>
        /// <remarks>
        /// See [InputTrackingState](xref:UnityEngine.XR.InputTrackingState) enum for values the input action represents.
        /// <list type="bullet">
        /// <item>
        /// <term>[InputTrackingState.None](xref:UnityEngine.XR.InputTrackingState.None) (0)</term>
        /// <description>to indicate neither position nor rotation is valid.</description>
        /// </item>
        /// <item>
        /// <term>[InputTrackingState.Position](xref:UnityEngine.XR.InputTrackingState.Position) (1)</term>
        /// <description>to indicate position is valid.</description>
        /// </item>
        /// <item>
        /// <term>[InputTrackingState.Rotation](xref:UnityEngine.XR.InputTrackingState.Rotation) (2)</term>
        /// <description>to indicate rotation is valid.</description>
        /// </item>
        /// <item>
        /// <term>[InputTrackingState.Position](xref:UnityEngine.XR.InputTrackingState.Position) <c>|</c> [InputTrackingState.Rotation](xref:UnityEngine.XR.InputTrackingState.Rotation) (3)</term>
        /// <description>to indicate position and rotation is valid.</description>
        /// </item>
        /// </list>
        /// </remarks>
        /// <seealso cref="ignoreTrackingState"/>
        public InputActionProperty trackingStateInput
        {
            get => m_TrackingStateInput;
            set
            {
                if (Application.isPlaying)
                    UnbindTrackingState();

                m_TrackingStateInput = value;

                if (Application.isPlaying && isActiveAndEnabled)
                    BindTrackingState();
            }
        }

        Vector3 m_CurrentPosition = Vector3.zero;
        Quaternion m_CurrentRotation = Quaternion.identity;
        TrackingStates m_CurrentTrackingState = TrackingStates.Position | TrackingStates.Rotation;
        bool m_RotationBound;
        bool m_PositionBound;
        bool m_TrackingStateBound;
        bool m_IsFirstUpdate = true;

        void BindActions()
        {
            BindPosition();
            BindRotation();
            BindTrackingState();
        }

        void UnbindActions()
        {
            UnbindPosition();
            UnbindRotation();
            UnbindTrackingState();
        }

        void BindPosition()
        {
            if (m_PositionBound)
                return;

            var action = m_PositionInput.action;
            if (action == null)
                return;

            action.performed += OnPositionPerformed;
            action.canceled += OnPositionCanceled;
            m_PositionBound = true;

            if (m_PositionInput.reference == null)
            {
                action.Rename($"{gameObject.name} - TPD - Position");
                action.Enable();
            }
        }

        void BindRotation()
        {
            if (m_RotationBound)
                return;

            var action = m_RotationInput.action;
            if (action == null)
                return;

            action.performed += OnRotationPerformed;
            action.canceled += OnRotationCanceled;
            m_RotationBound = true;

            if (m_RotationInput.reference == null)
            {
                action.Rename($"{gameObject.name} - TPD - Rotation");
                action.Enable();
            }
        }

        void BindTrackingState()
        {
            if (m_TrackingStateBound)
                return;

            var action = m_TrackingStateInput.action;
            if (action == null)
                return;

            action.performed += OnTrackingStatePerformed;
            action.canceled += OnTrackingStateCanceled;
            m_TrackingStateBound = true;

            if (m_TrackingStateInput.reference == null)
            {
                action.Rename($"{gameObject.name} - TPD - Tracking State");
                action.Enable();
            }
        }

        void UnbindPosition()
        {
            if (!m_PositionBound)
                return;

            var action = m_PositionInput.action;
            if (action == null)
                return;

            if (m_PositionInput.reference == null)
                action.Disable();

            action.performed -= OnPositionPerformed;
            action.canceled -= OnPositionCanceled;
            m_PositionBound = false;
        }

        void UnbindRotation()
        {
            if (!m_RotationBound)
                return;

            var action = m_RotationInput.action;
            if (action == null)
                return;

            if (m_RotationInput.reference == null)
                action.Disable();

            action.performed -= OnRotationPerformed;
            action.canceled -= OnRotationCanceled;
            m_RotationBound = false;
        }

        void UnbindTrackingState()
        {
            if (!m_TrackingStateBound)
                return;

            var action = m_TrackingStateInput.action;
            if (action == null)
                return;

            if (m_TrackingStateInput.reference == null)
                action.Disable();

            action.performed -= OnTrackingStatePerformed;
            action.canceled -= OnTrackingStateCanceled;
            m_TrackingStateBound = false;
        }

        void OnPositionPerformed(InputAction.CallbackContext context)
        {
            m_CurrentPosition = context.ReadValue<Vector3>();
        }

        void OnPositionCanceled(InputAction.CallbackContext context)
        {
            m_CurrentPosition = Vector3.zero;
        }

        void OnRotationPerformed(InputAction.CallbackContext context)
        {
            m_CurrentRotation = context.ReadValue<Quaternion>();
        }

        void OnRotationCanceled(InputAction.CallbackContext context)
        {
            m_CurrentRotation = Quaternion.identity;
        }

        void OnTrackingStatePerformed(InputAction.CallbackContext context)
        {
            m_CurrentTrackingState = (TrackingStates)context.ReadValue<int>();
        }

        void OnTrackingStateCanceled(InputAction.CallbackContext context)
        {
            m_CurrentTrackingState = TrackingStates.None;
        }

        /// <summary>
        /// This function is called when the user hits the Reset button in the Inspector's context menu
        /// or when adding the component the first time. This function is only called in editor mode.
        /// </summary>
        protected void Reset()
        {
            m_PositionInput = new InputActionProperty(new InputAction("Position", expectedControlType: "Vector3"));
            m_RotationInput = new InputActionProperty(new InputAction("Rotation", expectedControlType: "Quaternion"));
            m_TrackingStateInput = new InputActionProperty(new InputAction("Tracking State", expectedControlType: "Integer"));
        }

        /// <summary>
        /// This function is called when the script instance is being loaded.
        /// </summary>
        protected virtual void Awake()
        {
#if UNITY_INPUT_SYSTEM_ENABLE_VR && ENABLE_VR
            if (HasStereoCamera(out var cameraComponent))
            {
                UnityEngine.XR.XRDevice.DisableAutoXRCameraTracking(cameraComponent, true);
            }
#endif
        }

        /// <summary>
        /// This function is called when the object becomes enabled and active.
        /// </summary>
        protected void OnEnable()
        {
            InputSystem.onAfterUpdate += UpdateCallback;
            BindActions();

            // Read current input values when becoming enabled,
            // but wait until after the input update so the input is read at a consistent time
            m_IsFirstUpdate = true;
        }

        /// <summary>
        /// This function is called when the object becomes disabled or inactive.
        /// </summary>
        protected void OnDisable()
        {
            UnbindActions();
            InputSystem.onAfterUpdate -= UpdateCallback;
        }

        /// <summary>
        /// This function is called when the <see cref="MonoBehaviour"/> will be destroyed.
        /// </summary>
        protected virtual void OnDestroy()
        {
#if UNITY_INPUT_SYSTEM_ENABLE_VR && ENABLE_VR
            if (HasStereoCamera(out var cameraComponent))
            {
                UnityEngine.XR.XRDevice.DisableAutoXRCameraTracking(cameraComponent, false);
            }
#endif
        }

        /// <summary>
        /// The callback method called after the Input System has completed an update and processed all pending events.
        /// </summary>
        /// <seealso cref="InputSystem.onAfterUpdate"/>
        protected void UpdateCallback()
        {
            if (m_IsFirstUpdate)
            {
                // Update current input values if this is the first update since becoming enabled
                // since the performed callbacks may not have been executed
                if (m_PositionInput.action != null)
                    m_CurrentPosition = m_PositionInput.action.ReadValue<Vector3>();

                if (m_RotationInput.action != null)
                    m_CurrentRotation = m_RotationInput.action.ReadValue<Quaternion>();

                ReadTrackingState();

                m_IsFirstUpdate = false;
            }

            if (InputState.currentUpdateType == InputUpdateType.BeforeRender)
                OnBeforeRender();
            else
                OnUpdate();
        }

        void ReadTrackingState()
        {
            var trackingStateAction = m_TrackingStateInput.action;
            if (trackingStateAction != null && !trackingStateAction.enabled)
            {
                // Treat a disabled action as the default None value for the ReadValue call
                m_CurrentTrackingState = TrackingStates.None;
                return;
            }

            if (trackingStateAction == null || trackingStateAction.m_BindingsCount == 0)
            {
                // Treat an Input Action Reference with no reference the same as
                // an enabled Input Action with no authored bindings, and allow driving the Transform pose.
                m_CurrentTrackingState = TrackingStates.Position | TrackingStates.Rotation;
                return;
            }

            // Grab state.
            var actionMap = trackingStateAction.GetOrCreateActionMap();
            actionMap.ResolveBindingsIfNecessary();
            var state = actionMap.m_State;

            // Get list of resolved controls to determine if a device actually has tracking state.
            var hasResolvedControl = false;
            if (state != null)
            {
                var actionIndex = trackingStateAction.m_ActionIndexInState;
                var totalBindingCount = state.totalBindingCount;
                for (var i = 0; i < totalBindingCount; ++i)
                {
                    unsafe
                    {
                        ref var bindingState = ref state.bindingStates[i];
                        if (bindingState.actionIndex != actionIndex)
                            continue;
                        if (bindingState.isComposite)
                            continue;

                        if (bindingState.controlCount > 0)
                        {
                            hasResolvedControl = true;
                            break;
                        }
                    }
                }
            }

            // Retain the current value if there is no resolved binding.
            // Since the field initializes to allowing position and rotation,
            // this allows for driving the Transform pose always when the device
            // doesn't support reporting the tracking state.
            if (hasResolvedControl)
                m_CurrentTrackingState = (TrackingStates)trackingStateAction.ReadValue<int>();
        }

        /// <summary>
        /// This method is called after the Input System has completed an update and processed all pending events
        /// when the type of update is not <see cref="InputUpdateType.BeforeRender"/>.
        /// </summary>
        protected virtual void OnUpdate()
        {
            if (m_UpdateType == UpdateType.Update ||
                m_UpdateType == UpdateType.UpdateAndBeforeRender)
            {
                PerformUpdate();
            }
        }

        /// <summary>
        /// This method is called after the Input System has completed an update and processed all pending events
        /// when the type of update is <see cref="InputUpdateType.BeforeRender"/>.
        /// </summary>
        protected virtual void OnBeforeRender()
        {
            if (m_UpdateType == UpdateType.BeforeRender ||
                m_UpdateType == UpdateType.UpdateAndBeforeRender)
            {
                PerformUpdate();
            }
        }

        /// <summary>
        /// Updates <see cref="Transform"/> properties with the current input pose values that have been read,
        /// constrained by tracking type and tracking state.
        /// </summary>
        /// <seealso cref="SetLocalTransform"/>
        protected virtual void PerformUpdate()
        {
            SetLocalTransform(m_CurrentPosition, m_CurrentRotation);
        }

        /// <summary>
        /// Updates <see cref="Transform"/> properties, constrained by tracking type and tracking state.
        /// </summary>
        /// <param name="newPosition">The new local position to possibly set.</param>
        /// <param name="newRotation">The new local rotation to possibly set.</param>
        protected virtual void SetLocalTransform(Vector3 newPosition, Quaternion newRotation)
        {
            var positionValid = m_IgnoreTrackingState || (m_CurrentTrackingState & TrackingStates.Position) != 0;
            var rotationValid = m_IgnoreTrackingState || (m_CurrentTrackingState & TrackingStates.Rotation) != 0;

#if HAS_SET_LOCAL_POSITION_AND_ROTATION
            if (m_TrackingType == TrackingType.RotationAndPosition && rotationValid && positionValid)
            {
                transform.SetLocalPositionAndRotation(newPosition, newRotation);
                return;
            }
#endif

            if (rotationValid &&
                (m_TrackingType == TrackingType.RotationAndPosition ||
                 m_TrackingType == TrackingType.RotationOnly))
            {
                transform.localRotation = newRotation;
            }

            if (positionValid &&
                (m_TrackingType == TrackingType.RotationAndPosition ||
                 m_TrackingType == TrackingType.PositionOnly))
            {
                transform.localPosition = newPosition;
            }
        }

        bool HasStereoCamera(out Camera cameraComponent)
        {
            return TryGetComponent(out cameraComponent) && cameraComponent.stereoEnabled;
        }

        #region DEPRECATED

        // Disable warnings that these fields are never assigned to. They are set during Unity deserialization and migrated.
        // ReSharper disable UnassignedField.Local
#pragma warning disable 0649
        [Obsolete]
        [SerializeField, HideInInspector]
        InputAction m_PositionAction;
        /// <summary>
        /// (Deprecated) The action to read the position value of a tracked device.
        /// Must support reading a value of type <see cref="Vector3"/>.
        /// </summary>
        /// <seealso cref="positionInput"/>
        public InputAction positionAction
        {
            get => m_PositionInput.action;
            set => positionInput = new InputActionProperty(value);
        }

        [Obsolete]
        [SerializeField, HideInInspector]
        InputAction m_RotationAction;
        /// <summary>
        /// (Deprecated) The action to read the rotation value of a tracked device.
        /// Must support reading a value of type <see cref="Quaternion"/>.
        /// </summary>
        /// <seealso cref="rotationInput"/>
        public InputAction rotationAction
        {
            get => m_RotationInput.action;
            set => rotationInput = new InputActionProperty(value);
        }
#pragma warning restore 0649
        // ReSharper restore UnassignedField.Local

        /// <inheritdoc />
        void ISerializationCallbackReceiver.OnBeforeSerialize()
        {
        }

        /// <inheritdoc />
        void ISerializationCallbackReceiver.OnAfterDeserialize()
        {
#pragma warning disable 0612 // Type or member is obsolete -- Deprecated fields are migrated to new properties.
#pragma warning disable UNT0029 // Pattern matching with null on Unity objects -- Using true null is intentional, not operator== evaluation.
            // We're checking for true null here since we don't want to migrate if the new field is already being used, even if the reference is missing.
            // Migrate the old fields to the new properties added in Input System 1.1.0-pre.6.
            if (m_PositionInput.serializedReference is null && m_PositionInput.serializedAction is null && !(m_PositionAction is null))
                m_PositionInput = new InputActionProperty(m_PositionAction);

            if (m_RotationInput.serializedReference is null && m_RotationInput.serializedAction is null && !(m_RotationAction is null))
                m_RotationInput = new InputActionProperty(m_RotationAction);
#pragma warning restore UNT0029
#pragma warning restore 0612
        }

        #endregion
    }
}