// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.MixedReality.Toolkit.Physics;
using Microsoft.MixedReality.Toolkit.Utilities;
using System.Collections;
using Unity.Profiling;
using UnityEngine;

namespace Microsoft.MixedReality.Toolkit.Input
{
    /// <summary>
    /// This class allows for HoloLens 1 style input, using a far gaze ray
    /// for focus with hand and gesture-based input and interaction across it.
    /// </summary>
    /// <remarks>
    /// <para>GGV stands for gaze, gesture, and voice.
    /// This pointer's position is given by hand position (grip pose),
    /// and the input focus is given by head gaze.</para>
    /// </remarks>
    [AddComponentMenu("Scripts/MRTK/SDK/GGVPointer")]
    public class GGVPointer : InputSystemGlobalHandlerListener,
        IMixedRealityQueryablePointer,
        IMixedRealityInputHandler,
        IMixedRealityInputHandler<MixedRealityPose>,
        IMixedRealitySourceStateHandler
    {
        [Header("Pointer")]
        [SerializeField]
        private MixedRealityInputAction selectAction = MixedRealityInputAction.None;

        [SerializeField]
        private MixedRealityInputAction poseAction = MixedRealityInputAction.None;

        private IMixedRealityGazeProvider gazeProvider;
        private Vector3 sourcePosition;
        private bool isSelectPressed;
        private Handedness lastControllerHandedness;

        #region IMixedRealityPointer

        private IMixedRealityController controller;

        /// <inheritdoc />
        public IMixedRealityController Controller
        {
            get { return controller; }
            set
            {
                controller = value;

                if (controller != null && this.IsNotNull())
                {
                    gameObject.name = $"{Controller.ControllerHandedness}_GGVPointer";
                    pointerName = gameObject.name;
                    InputSourceParent = controller.InputSource;
                }
            }
        }

        private uint pointerId;

        /// <inheritdoc />
        public uint PointerId
        {
            get
            {
                if (pointerId == 0)
                {
                    pointerId = CoreServices.InputSystem.FocusProvider.GenerateNewPointerId();
                }

                return pointerId;
            }
        }

        private string pointerName = string.Empty;

        /// <inheritdoc />
        public string PointerName
        {
            get { return pointerName; }
            set
            {
                pointerName = value;
                if (this.IsNotNull())
                {
                    gameObject.name = value;
                }
            }
        }

        /// <inheritdoc />
        public IMixedRealityInputSource InputSourceParent { get; private set; }

        /// <inheritdoc />
        public IMixedRealityCursor BaseCursor { get; set; }

        /// <inheritdoc />
        public ICursorModifier CursorModifier { get; set; }

        /// <inheritdoc />
        public bool IsInteractionEnabled => IsActive;

        /// <inheritdoc />
        public bool IsActive { get; set; }

        /// <inheritdoc />
        public bool IsFocusLocked { get; set; }

        /// <inheritdoc />
        public bool IsTargetPositionLockedOnFocusLock { get; set; }

        public RayStep[] Rays { get; protected set; } = { new RayStep(Vector3.zero, Vector3.forward) };

        public LayerMask[] PrioritizedLayerMasksOverride { get; set; }

        public IMixedRealityFocusHandler FocusTarget { get; set; }

        /// <inheritdoc />
        public IPointerResult Result { get; set; }

        /// <inheritdoc />
        public virtual SceneQueryType SceneQueryType { get; set; } = SceneQueryType.SimpleRaycast;

        /// <inheritdoc />
        public float SphereCastRadius
        {
            get => throw new System.NotImplementedException();
            set => throw new System.NotImplementedException();
        }

        private static bool Equals(IMixedRealityPointer left, IMixedRealityPointer right)
        {
            return left != null && left.Equals(right);
        }

        /// <inheritdoc />
        bool IEqualityComparer.Equals(object left, object right)
        {
            return left.Equals(right);
        }

        /// <inheritdoc />
        public override bool Equals(object obj)
        {
            if (ReferenceEquals(null, obj)) { return false; }
            if (ReferenceEquals(this, obj)) { return true; }
            if (obj.GetType() != GetType()) { return false; }

            return Equals((IMixedRealityPointer)obj);
        }

        private bool Equals(IMixedRealityPointer other)
        {
            return other != null && PointerId == other.PointerId && string.Equals(PointerName, other.PointerName);
        }

        /// <inheritdoc />
        int IEqualityComparer.GetHashCode(object obj)
        {
            return obj.GetHashCode();
        }

        /// <inheritdoc />
        public override int GetHashCode()
        {
            unchecked
            {
                int hashCode = 0;
                hashCode = (hashCode * 397) ^ (int)PointerId;
                hashCode = (hashCode * 397) ^ (PointerName != null ? PointerName.GetHashCode() : 0);
                return hashCode;
            }
        }

        private static readonly ProfilerMarker OnPreSceneQueryPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnPreSceneQuery");

        /// <inheritdoc />
        public void OnPreSceneQuery()
        {
            using (OnPreSceneQueryPerfMarker.Auto())
            {
                Vector3 newGazeOrigin = gazeProvider.GazePointer.Rays[0].Origin;
                Vector3 endPoint = newGazeOrigin + (gazeProvider.GazePointer.Rays[0].Direction * CoreServices.InputSystem.FocusProvider.GlobalPointingExtent);
                Rays[0].UpdateRayStep(ref newGazeOrigin, ref endPoint);
            }
        }

        // Returns the hit values from the gaze provider. Gaze provider queries the scene using the perferred method.
        public bool OnSceneQuery(LayerMask[] prioritizedLayerMasks, bool focusIndividualCompoundCollider, out MixedRealityRaycastHit hitInfo, out RayStep Ray, out int rayStepIndex)
        {
            if (gazeProvider.GazePointer is IMixedRealityQueryablePointer queryPointer)
            {
                return queryPointer.OnSceneQuery(prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo, out Ray, out rayStepIndex);
            }
            else
            {
				var raycastProvider = CoreServices.InputSystem.RaycastProvider;
				bool didHit = raycastProvider.Raycast(Rays[0], prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo);
                Ray = Rays[0];
                rayStepIndex = 0;
                return didHit;
            }
        }

        public bool OnSceneQuery(LayerMask[] prioritizedLayerMasks, bool focusIndividualCompoundCollider, out GameObject hitObject, out Vector3 hitPoint, out float hitDistance)
        {
            if (gazeProvider.GazePointer is IMixedRealityQueryablePointer queryPointer)
            {
                return queryPointer.OnSceneQuery(prioritizedLayerMasks, focusIndividualCompoundCollider, out hitObject, out hitPoint, out hitDistance);
            }
            else
            {
				var raycastProvider = CoreServices.InputSystem.RaycastProvider;
				bool didHit = raycastProvider.Raycast(Rays[0], prioritizedLayerMasks, focusIndividualCompoundCollider, out MixedRealityRaycastHit physicsHit);
                if (didHit)
                {
                    hitObject = physicsHit.collider.gameObject;
                    hitPoint = physicsHit.point;
                    hitDistance = physicsHit.distance;
                    return didHit;
                }
                else
                {
                    hitObject = null;
                    hitPoint = Vector3.zero;
                    hitDistance = Mathf.Infinity;
                    return false;
                }
            }
        }

        private static readonly ProfilerMarker OnPostSceneQueryPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnPostSceneQuery");

        /// <inheritdoc />
        public void OnPostSceneQuery()
        {
            using (OnPostSceneQueryPerfMarker.Auto())
            {
                if (isSelectPressed && IsInteractionEnabled)
                {
                    CoreServices.InputSystem.RaisePointerDragged(this, MixedRealityInputAction.None, Controller.ControllerHandedness);
                }
            }
        }

        /// <inheritdoc />
        public void OnPreCurrentPointerTargetChange() { }

        /// <inheritdoc />
        public void Reset()
        {
            Controller = null;
            BaseCursor = null;
            IsActive = false;
            IsFocusLocked = false;
        }

        /// <inheritdoc />
        public virtual Vector3 Position => sourcePosition;

        /// <inheritdoc />
        public virtual Quaternion Rotation
        {
            get
            {
                // Previously we were simply returning the InternalGazeProvider rotation here.
                // This caused issues when the head rotated, but the hand stayed where it was.
                // Now we're returning a rotation based on the vector from the camera position
                // to the hand. This rotation is not affected by rotating your head.
                Vector3 look = Position - CameraCache.Main.transform.position;

                // If the input source is at the same position as the camera, assume it's the camera and return the InternalGazeProvider rotation.
                // This prevents passing Vector3.zero into Quaternion.LookRotation, which isn't possible and causes a console log.
                if (look == Vector3.zero)
                {
                    return Quaternion.LookRotation(gazeProvider.GazePointer.Rays[0].Direction);
                }

                return Quaternion.LookRotation(look);
            }
        }

        #endregion

        #region IMixedRealityInputHandler Implementation

        private static readonly ProfilerMarker OnInputUpPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnInputUp");

        /// <inheritdoc />
        public void OnInputUp(InputEventData eventData)
        {
            using (OnInputUpPerfMarker.Auto())
            {
                if (InputSourceParent.IsNotNull() && eventData.SourceId == InputSourceParent.SourceId)
                {
                    if (eventData.MixedRealityInputAction == selectAction)
                    {
                        isSelectPressed = false;
                        if (IsInteractionEnabled)
                        {
                            BaseCursor c = null;
                            if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull())
                            {
                                c = gazeProvider.GazePointer.BaseCursor as BaseCursor;
                            }

                            if (c != null)
                            {
                                c.SourceDownIds.Remove(eventData.SourceId);
                            }

                            CoreServices.InputSystem?.RaisePointerClicked(this, selectAction, 0, Controller.ControllerHandedness);
                            CoreServices.InputSystem?.RaisePointerUp(this, selectAction, Controller.ControllerHandedness);

                            // For GGV, the gaze pointer does not set this value itself.
                            // See comment in OnInputDown for more details.
                            if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull())
                            {
                                gazeProvider.GazePointer.IsFocusLocked = false;
                            }
                        }
                    }
                }
            }
        }

        private static readonly ProfilerMarker OnInputDownPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnInputDown");

        /// <inheritdoc />
        public void OnInputDown(InputEventData eventData)
        {
            using (OnInputDownPerfMarker.Auto())
            {
                if (eventData.SourceId == InputSourceParent?.SourceId)
                {
                    if (eventData.MixedRealityInputAction == selectAction)
                    {
                        isSelectPressed = true;
                        lastControllerHandedness = Controller.ControllerHandedness;
                        if (IsInteractionEnabled)
                        {
                            BaseCursor c = null;
                            if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull())
                            {
                                c = gazeProvider.GazePointer.BaseCursor as BaseCursor;
                            }

                            if (c != null)
                            {
                                c.SourceDownIds.Add(eventData.SourceId);
                            }

                            CoreServices.InputSystem?.RaisePointerDown(this, selectAction, Controller.ControllerHandedness);

                            // For GGV, the gaze pointer does not set this value itself as it does not receive input
                            // events from the hands. Because this value is important for certain gaze behavior,
                            // such as positioning the gaze cursor, it is necessary to set it here.
                            if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull())
                            {
                                gazeProvider.GazePointer.IsFocusLocked = (gazeProvider.GazePointer.Result?.Details.Object != null);
                            }
                        }
                    }
                }
            }
        }

        #endregion  IMixedRealityInputHandler Implementation

        #region MonoBehaviour Implementation

        protected override void OnEnable()
        {
            base.OnEnable();

            gazeProvider = CoreServices.InputSystem.GazeProvider;

            if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull())
            {

                BaseCursor c = gazeProvider.GazePointer.BaseCursor as BaseCursor;
                if (c.IsNotNull())
                {
                    c.VisibleSourcesCount++;
                }
            }
        }

        protected override void OnDisable()
        {
            base.OnDisable();

            if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull())
            {
                BaseCursor c = gazeProvider.GazePointer.BaseCursor as BaseCursor;
                if (c.IsNotNull())
                {
                    c.VisibleSourcesCount--;
                }
            }
        }
        #endregion MonoBehaviour Implementation

        #region InputSystemGlobalHandlerListener Implementation

        /// <inheritdoc />
        protected override void RegisterHandlers()
        {
            CoreServices.InputSystem?.RegisterHandler<IMixedRealityInputHandler>(this);
            CoreServices.InputSystem?.RegisterHandler<IMixedRealityInputHandler<MixedRealityPose>>(this);
            CoreServices.InputSystem?.RegisterHandler<IMixedRealitySourceStateHandler>(this);
        }

        /// <inheritdoc />
        protected override void UnregisterHandlers()
        {
            CoreServices.InputSystem?.UnregisterHandler<IMixedRealityInputHandler>(this);
            CoreServices.InputSystem?.UnregisterHandler<IMixedRealityInputHandler<MixedRealityPose>>(this);
            CoreServices.InputSystem?.UnregisterHandler<IMixedRealitySourceStateHandler>(this);
        }

        #endregion InputSystemGlobalHandlerListener Implementation

        #region IMixedRealitySourceStateHandler

        /// <inheritdoc />
        public void OnSourceDetected(SourceStateEventData eventData) { }

        private static readonly ProfilerMarker OnSourceLostPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnSourceLost");

        /// <inheritdoc />
        public void OnSourceLost(SourceStateEventData eventData)
        {
            using (OnSourceLostPerfMarker.Auto())
            {
                if (eventData.SourceId == InputSourceParent?.SourceId)
                {
                    BaseCursor c = null;
                    if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull())
                    {
                        c = gazeProvider.GazePointer.BaseCursor as BaseCursor;
                    }

                    if (c != null)
                    {
                        c.SourceDownIds.Remove(eventData.SourceId);
                    }

                    if (isSelectPressed)
                    {
                        // Raise OnInputUp if pointer is lost while select is pressed
                        CoreServices.InputSystem?.RaisePointerUp(this, selectAction, lastControllerHandedness);

                        // For GGV, the gaze pointer does not set this value itself.
                        // See comment in OnInputDown for more details.
                        if (gazeProvider.IsNotNull() && gazeProvider.GazePointer.IsNotNull())
                        {
                            gazeProvider.GazePointer.IsFocusLocked = false;
                        }
                    }

                    // Destroy the pointer since nobody else is destroying us
                    GameObjectExtensions.DestroyGameObject(gameObject);
                }
            }
        }

        #endregion IMixedRealitySourceStateHandler

        #region IMixedRealityInputHandler<MixedRealityPose>

        private static readonly ProfilerMarker OnInputChangedPerfMarker = new ProfilerMarker("[MRTK] GGVPointer.OnInputChanged");

        /// <inheritdoc />
        public void OnInputChanged(InputEventData<MixedRealityPose> eventData)
        {
            using (OnInputChangedPerfMarker.Auto())
            {
                if (eventData.SourceId == Controller?.InputSource.SourceId &&
                    eventData.Handedness == Controller?.ControllerHandedness &&
                    eventData.MixedRealityInputAction == poseAction)
                {
                    sourcePosition = eventData.InputData.Position;
                }
            }
        }

        #endregion IMixedRealityInputHandler<MixedRealityPose>
    }
}
