// Copyright HTC Corporation All Rights Reserved.

using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

namespace VIVE.OpenXR.Raycast
{
    [DisallowMultipleComponent]
    [RequireComponent(typeof(Camera))]
    public class RaycastImpl : BaseRaycaster
    {
        private StringBuilder m_sb = null;
        protected StringBuilder sb {
            get {
                if (m_sb == null) { m_sb = new StringBuilder(); }
                return m_sb;
            }
        }
        const string LOG_TAG = "VIVE.OpenXR.Raycast.RaycastImpl ";
        protected void DEBUG(StringBuilder msg) { Debug.Log(msg); }

        #region Inspector
        [SerializeField]
        private bool m_IgnoreReversedGraphics = false;
        public bool IgnoreReversedGraphics { get { return m_IgnoreReversedGraphics; } set { m_IgnoreReversedGraphics = value; } }
        [SerializeField]
        private float m_PhysicsCastDistance = 100;
        public float PhysicsCastDistance { get { return m_PhysicsCastDistance; } set { m_PhysicsCastDistance = value; } }
        [SerializeField]
        private LayerMask m_PhysicsEventMask = ~0;
        public LayerMask PhysicsEventMask { get { return m_PhysicsEventMask; } set { m_PhysicsEventMask = value; } }
        #endregion

        private Camera m_Camera = null;
        public override Camera eventCamera { get { return m_Camera; } }

        #region MonoBehaviour overrides
        protected override void OnEnable()
        {
            sb.Clear().Append(LOG_TAG).Append("OnEnable()"); DEBUG(sb);
            base.OnEnable();

            /// 1. Set up the event camera.
            m_Camera = GetComponent<Camera>();
            m_Camera.stereoTargetEye = StereoTargetEyeMask.None;
            m_Camera.enabled = false;

            /// 2. Set up the EventSystem.
            if (EventSystem.current == null)
            {
                var eventSystemObject = new GameObject("EventSystem");
                eventSystemObject.AddComponent<EventSystem>();
            }
        }
        protected override void OnDisable()
        {
            sb.Clear().Append(LOG_TAG).Append("OnDisable()"); DEBUG(sb);
            base.OnDisable();
        }

        int printFrame = 0;
        protected bool printIntervalLog = false;

        protected bool m_Interactable = true;
        protected virtual void Update()
        {
            printFrame++;
            printFrame %= 300;
            printIntervalLog = (printFrame == 0);

            if (!m_Interactable) return;

            /// Use the event camera and EventSystem to reset PointerEventData.
            ResetEventData();

            /// Update the raycast results
            resultAppendList.Clear();
            Raycast(pointerData, resultAppendList);

            pointerData.pointerCurrentRaycast = currentRaycastResult;

            /// Send events
            HandleRaycastEvent();
        }
        #endregion

        #region Raycast Result Handling
        static readonly Comparison<RaycastResult> rrComparator = RaycastResultComparator;
        private RaycastResult GetFirstRaycastResult(List<RaycastResult> results)
        {
            RaycastResult rr = default;

            results.Sort(rrComparator);
            for (int i = 0; i < results.Count; i++)
            {
                if (results[i].isValid)
                {
                    rr = results[i];
                    break;
                }
            }

            return rr;
        }
        private static int RaycastResultComparator(RaycastResult lhs, RaycastResult rhs)
        {
            if (lhs.module != rhs.module)
            {
                if (lhs.module.eventCamera != null && rhs.module.eventCamera != null && lhs.module.eventCamera.depth != rhs.module.eventCamera.depth)
                {
                    // need to reverse the standard compareTo
                    if (lhs.module.eventCamera.depth < rhs.module.eventCamera.depth) { return 1; }
                    if (lhs.module.eventCamera.depth == rhs.module.eventCamera.depth) { return 0; }
                    return -1;
                }

                if (lhs.module.sortOrderPriority != rhs.module.sortOrderPriority)
                {
                    return rhs.module.sortOrderPriority.CompareTo(lhs.module.sortOrderPriority);
                }

                if (lhs.module.renderOrderPriority != rhs.module.renderOrderPriority)
                {
                    return rhs.module.renderOrderPriority.CompareTo(lhs.module.renderOrderPriority);
                }
            }

            if (lhs.sortingLayer != rhs.sortingLayer)
            {
                // Uses the layer value to properly compare the relative order of the layers.
                var rid = SortingLayer.GetLayerValueFromID(rhs.sortingLayer);
                var lid = SortingLayer.GetLayerValueFromID(lhs.sortingLayer);
                return rid.CompareTo(lid);
            }

            if (lhs.sortingOrder != rhs.sortingOrder)
            {
                return rhs.sortingOrder.CompareTo(lhs.sortingOrder);
            }

            if (!Mathf.Approximately(lhs.distance, rhs.distance))
            {
                return lhs.distance.CompareTo(rhs.distance);
            }

            if (lhs.depth != rhs.depth)
            {
                return rhs.depth.CompareTo(lhs.depth);
            }

            return lhs.index.CompareTo(rhs.index);
        }
        #endregion

#if UNITY_EDITOR
        bool drawDebugLine = false;
#endif

        #region Raycast
        protected virtual bool UseEyeData(out Vector3 direction)
        {
            direction = Vector3.forward;
            return false;
        }
        protected PointerEventData pointerData = null;
        protected Vector3 pointerLocalOffset = Vector3.forward;
        private Vector3 physicsWorldPosition = Vector3.zero;
        private Vector2 graphicScreenPosition = Vector2.zero;
        private void UpdatePointerDataPosition()
        {
            /// 1. Calculate the pointer offset in "local" space.
            pointerLocalOffset = Vector3.forward;
            if (UseEyeData(out Vector3 direction))
            {
                pointerLocalOffset = direction;

                // Revise the offset from World space to Local space.
                // OpenXR always uses World space.
                pointerLocalOffset = Quaternion.Inverse(transform.rotation) * pointerLocalOffset;
            }

            /// 2. Calculate the pointer position in "world" space.
            Vector3 rotated_offset = transform.rotation * pointerLocalOffset;
            physicsWorldPosition = transform.position + rotated_offset;
            graphicScreenPosition = m_Camera.WorldToScreenPoint(physicsWorldPosition);
            // The graphicScreenPosition.x should be equivalent to (0.5f * Screen.width);
            // The graphicScreenPosition.y should be equivalent to (0.5f * Screen.height);
        }
        private void ResetEventData()
        {
            if (pointerData == null) { pointerData = new RaycastEventData(EventSystem.current, gameObject); }

            UpdatePointerDataPosition();
            pointerData.position = graphicScreenPosition;
        }
        List<RaycastResult> resultAppendList = new List<RaycastResult>();
        private RaycastResult currentRaycastResult = default;
        protected GameObject raycastObject = null;
        protected List<GameObject> s_raycastObjects = new List<GameObject>();
        protected GameObject raycastObjectEx = null;
        protected List<GameObject> s_raycastObjectsEx = new List<GameObject>();
        /**
		 * Call to
		 * GraphicRaycast(Canvas canvas, Camera eventCamera, Vector2 screenPosition, List<RaycastResult> resultAppendList)
		 * PhysicsRaycast(Ray ray, Camera eventCamera, List<RaycastResult> resultAppendList)
		 **/
        public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
        {
            // --------------- Previous Results ---------------
            raycastObjectEx = raycastObject;
            s_raycastObjects.Clear();

            // --------------- Graphic Raycast ---------------
            Canvas[] canvases = CanvasProvider.GetTargetCanvas();
            if (canvases.Length <= 0) { canvases = FindObjectsOfType<Canvas>(); }    // note: GC.Alloc
            for (int i = 0; i < canvases.Length; i++)
            {
                GraphicRaycast(canvases[i], m_Camera, eventData.position, resultAppendList);
            }

            // --------------- Physics Raycast ---------------
            Ray ray = new Ray(transform.position, (physicsWorldPosition - transform.position));
            PhysicsRaycast(ray, m_Camera, resultAppendList);

            currentRaycastResult = GetFirstRaycastResult(resultAppendList);

            // --------------- Current Results ---------------
            raycastObject = currentRaycastResult.gameObject;

            GameObject raycastTarget = currentRaycastResult.gameObject;
            while (raycastTarget != null)
            {
                s_raycastObjects.Add(raycastTarget);
                raycastTarget = (raycastTarget.transform.parent != null ? raycastTarget.transform.parent.gameObject : null);
            }

#if UNITY_EDITOR
            if (drawDebugLine)
            {
                Vector3 end = transform.position + (transform.forward * 100);
                Debug.DrawLine(transform.position, end, Color.red, 1);
            }
#endif
        }

        Ray ray = new Ray();
        protected virtual void GraphicRaycast(Canvas canvas, Camera eventCamera, Vector2 screenPosition, List<RaycastResult> resultAppendList)
        {
            if (canvas == null)
                return;

            IList<Graphic> foundGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
            if (foundGraphics == null || foundGraphics.Count == 0)
                return;

            int displayIndex = 0;
            var currentEventCamera = eventCamera; // Property can call Camera.main, so cache the reference

            if (canvas.renderMode == RenderMode.ScreenSpaceOverlay || currentEventCamera == null)
                displayIndex = canvas.targetDisplay;
            else
                displayIndex = currentEventCamera.targetDisplay;

            if (currentEventCamera != null)
                ray = currentEventCamera.ScreenPointToRay(screenPosition);

            // Necessary for the event system
            for (int i = 0; i < foundGraphics.Count; ++i)
            {
                Graphic graphic = foundGraphics[i];

                // -1 means it hasn't been processed by the canvas, which means it isn't actually drawn
                if (!graphic.raycastTarget || graphic.canvasRenderer.cull || graphic.depth == -1) { continue; }

                if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, screenPosition, currentEventCamera)) { continue; }

                if (currentEventCamera != null && currentEventCamera.WorldToScreenPoint(graphic.rectTransform.position).z > currentEventCamera.farClipPlane) { continue; }

                if (graphic.Raycast(screenPosition, currentEventCamera))
                {
                    var go = graphic.gameObject;
                    bool appendGraphic = true;

                    if (m_IgnoreReversedGraphics)
                    {
                        if (currentEventCamera == null)
                        {
                            // If we dont have a camera we know that we should always be facing forward
                            var dir = go.transform.rotation * Vector3.forward;
                            appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
                        }
                        else
                        {
                            // If we have a camera compare the direction against the cameras forward.
                            var cameraForward = currentEventCamera.transform.rotation * Vector3.forward * currentEventCamera.nearClipPlane;
                            appendGraphic = Vector3.Dot(go.transform.position - currentEventCamera.transform.position - cameraForward, go.transform.forward) >= 0;
                        }
                    }

                    if (appendGraphic)
                    {
                        float distance = 0;
                        Transform trans = go.transform;
                        Vector3 transForward = trans.forward;

                        if (currentEventCamera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
                            distance = 0;
                        else
                        {
                            // http://geomalgorithms.com/a06-_intersect-2.html
                            distance = (Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction));

                            // Check to see if the go is behind the camera.
                            if (distance < 0)
                                continue;
                        }

                        resultAppendList.Add(new RaycastResult
                        {
                            gameObject = go,
                            module = this,
                            distance = distance,
                            screenPosition = screenPosition,
                            displayIndex = displayIndex,
                            index = resultAppendList.Count,
                            depth = graphic.depth,
                            sortingLayer = canvas.sortingLayerID,
                            sortingOrder = canvas.sortingOrder,
                            worldPosition = ray.origin + ray.direction * distance,
                            worldNormal = -transForward
                        });
                    }
                }
            }
        }

        Vector3 hitScreenPos = Vector3.zero;
        Vector2 hitScreenPos2D = Vector2.zero;
        static readonly RaycastHit[] hits = new RaycastHit[255];
        protected virtual void PhysicsRaycast(Ray ray, Camera eventCamera, List<RaycastResult> resultAppendList)
        {
            var hitCount = Physics.RaycastNonAlloc(ray, hits, m_PhysicsCastDistance, m_PhysicsEventMask);

            for (int i = 0; i < hitCount; ++i)
            {
                hitScreenPos = eventCamera.WorldToScreenPoint(hits[i].point);
                hitScreenPos2D.x = hitScreenPos.x;
                hitScreenPos2D.y = hitScreenPos.y;

                resultAppendList.Add(new RaycastResult
                {
                    gameObject = hits[i].collider.gameObject,
                    module = this,
                    distance = hits[i].distance,
                    worldPosition = hits[i].point,
                    worldNormal = hits[i].normal,
                    screenPosition = hitScreenPos2D,
                    index = resultAppendList.Count,
                    sortingLayer = 0,
                    sortingOrder = 0
                });
            }
        }
        #endregion

        #region Event
        private void CopyList(List<GameObject> src, List<GameObject> dst)
        {
            dst.Clear();
            for (int i = 0; i < src.Count; i++)
                dst.Add(src[i]);
        }

        private void ExitEnterHandler(ref List<GameObject> enterObjects, ref List<GameObject> exitObjects)
        {
            if (exitObjects.Count > 0)
            {
                for (int i = 0; i < exitObjects.Count; i++)
                {
                    if (exitObjects[i] != null && !enterObjects.Contains(exitObjects[i]))
                    {
                        ExecuteEvents.Execute(exitObjects[i], pointerData, ExecuteEvents.pointerExitHandler);
                        sb.Clear().Append(LOG_TAG).Append("ExitEnterHandler() Exit: ").Append(exitObjects[i].name); DEBUG(sb);
                    }
                }
            }

            if (enterObjects.Count > 0)
            {
                for (int i = 0; i < enterObjects.Count; i++)
                {
                    if (enterObjects[i] != null && !exitObjects.Contains(enterObjects[i]))
                    {
                        ExecuteEvents.Execute(enterObjects[i], pointerData, ExecuteEvents.pointerEnterHandler);
                        sb.Clear().Append(LOG_TAG).Append("ExitEnterHandler() Enter: ").Append(enterObjects[i].name).Append(", camera: ").Append(pointerData.enterEventCamera); DEBUG(sb);
                    }
                }
            }

            CopyList(enterObjects, exitObjects);
        }
        private void HoverHandler()
        {
            if (raycastObject != null && (raycastObject == raycastObjectEx))
            {
                if (printIntervalLog) { sb.Clear().Append(LOG_TAG).Append("HoverHandler() Hover: ").Append(raycastObject.name); DEBUG(sb); }
                ExecuteEvents.ExecuteHierarchy(raycastObject, pointerData, RaycastEvents.pointerHoverHandler);
            }
        }
        private void DownHandler()
        {
            sb.Clear().Append(LOG_TAG).Append("DownHandler()");DEBUG(sb);
            if (raycastObject == null) { return; }

            pointerData.pressPosition = pointerData.position;
            pointerData.pointerPressRaycast = pointerData.pointerCurrentRaycast;
            pointerData.pointerPress =
                ExecuteEvents.ExecuteHierarchy(raycastObject, pointerData, ExecuteEvents.pointerDownHandler)
                ?? ExecuteEvents.GetEventHandler<IPointerClickHandler>(raycastObject);

            sb.Clear().Append(LOG_TAG).Append("DownHandler() Down: ").Append(pointerData.pointerPress).Append(", raycastObject: ").Append(raycastObject.name); DEBUG(sb);

            // If Drag Handler exists, send initializePotentialDrag event.
            pointerData.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(raycastObject);
            if (pointerData.pointerDrag != null)
            {
                sb.Clear().Append(LOG_TAG).Append("DownHandler() Send initializePotentialDrag to ").Append(pointerData.pointerDrag.name).Append(", current GameObject is ").Append(raycastObject.name); DEBUG(sb);
                ExecuteEvents.Execute(pointerData.pointerDrag, pointerData, ExecuteEvents.initializePotentialDrag);
            }

            // press happened (even not handled) object.
            pointerData.rawPointerPress = raycastObject;
            // allow to send Pointer Click event
            pointerData.eligibleForClick = true;
            // reset the screen position of press, can be used to estimate move distance
            pointerData.delta = Vector2.zero;
            // current Down, reset drag state
            pointerData.dragging = false;
            pointerData.useDragThreshold = true;
            // record the count of Pointer Click should be processed, clean when Click event is sent.
            pointerData.clickCount = 1;
            // set clickTime to current time of Pointer Down instead of Pointer Click.
            // since Down & Up event should not be sent too closely. (< kClickInterval)
            pointerData.clickTime = Time.unscaledTime;
        }
        private void UpHandler()
        {
            if (!pointerData.eligibleForClick && !pointerData.dragging)
            {
                // 1. no pending click
                // 2. no dragging
                // Mean user has finished all actions and do NOTHING in current frame.
                return;
            }

            // raycastObject may be different with pointerData.pointerDrag so we don't check null

            if (pointerData.pointerPress != null)
            {
                // In the frame of button is pressed -> unpressed, send Pointer Up
                sb.Clear().Append(LOG_TAG).Append("UpHandler() Send Pointer Up to ").Append(pointerData.pointerPress.name); DEBUG(sb);
                ExecuteEvents.Execute(pointerData.pointerPress, pointerData, ExecuteEvents.pointerUpHandler);
            }

            if (pointerData.eligibleForClick)
            {
                GameObject objectToClick = ExecuteEvents.GetEventHandler<IPointerClickHandler>(raycastObject);
                if (objectToClick != null)
                {
                    if (objectToClick == pointerData.pointerPress)
                    {
                        // In the frame of button from being pressed to unpressed, send Pointer Click if Click is pending.
                        sb.Clear().Append(LOG_TAG).Append("UpHandler() Send Pointer Click to ").Append(pointerData.pointerPress.name); DEBUG(sb);
                        ExecuteEvents.Execute(pointerData.pointerPress, pointerData, ExecuteEvents.pointerClickHandler);
                    }
                    else
                    {
                        sb.Clear().Append(LOG_TAG).Append("UpHandler() pointer down object ").Append(pointerData.pointerPress).Append(" is different with click object ").Append(objectToClick.name); DEBUG(sb);
                    }
                }
                else
                {
                    if (pointerData.dragging)
                    {
                        GameObject _pointerDrop = ExecuteEvents.GetEventHandler<IDropHandler>(raycastObject);
                        if (_pointerDrop == pointerData.pointerDrag)
                        {
                            // In next frame of button from being pressed to unpressed, send Drop and EndDrag if dragging.
                            sb.Clear().Append(LOG_TAG).Append("UpHandler() Send Pointer Drop to ").Append(pointerData.pointerDrag); DEBUG(sb);
                            ExecuteEvents.Execute(pointerData.pointerDrag, pointerData, ExecuteEvents.dropHandler);
                        }
                        sb.Clear().Append(LOG_TAG).Append("UpHandler() Send Pointer endDrag to ").Append(pointerData.pointerDrag); DEBUG(sb);
                        ExecuteEvents.Execute(pointerData.pointerDrag, pointerData, ExecuteEvents.endDragHandler);

                        pointerData.dragging = false;
                    }
                }
            }

            // initializePotentialDrag was sent when IDragHandler exists.
            pointerData.pointerDrag = null;
            // Down of pending Click object.
            pointerData.pointerPress = null;
            // press happened (even not handled) object.
            pointerData.rawPointerPress = null;
            // clear pending state.
            pointerData.eligibleForClick = false;
            // Click is processed, clearcount.
            pointerData.clickCount = 0;
            // Up is processed thus clear the time limitation of Down event.
            pointerData.clickTime = 0;
        }

        // After selecting an object over this duration, the drag action will be taken.
        const float kTimeToDrag = 0.2f;
        private void DragHandler()
        {
            if (Time.unscaledTime - pointerData.clickTime < kTimeToDrag) { return; }
            if (pointerData.pointerDrag == null) { return; }

            if (!pointerData.dragging)
            {
                sb.Clear().Append(LOG_TAG).Append("DragHandler() Send BeginDrag to ").Append(pointerData.pointerDrag.name); DEBUG(sb);
                ExecuteEvents.Execute(pointerData.pointerDrag, pointerData, ExecuteEvents.beginDragHandler);
                pointerData.dragging = true;
            }
            else
            {
                ExecuteEvents.Execute(pointerData.pointerDrag, pointerData, ExecuteEvents.dragHandler);
            }
        }

        private void SubmitHandler()
        {
            if (raycastObject == null) { return; }

            sb.Clear().Append(LOG_TAG).Append("SubmitHandler() Submit: ").Append(raycastObject.name); DEBUG(sb);
            ExecuteEvents.ExecuteHierarchy(raycastObject, pointerData, ExecuteEvents.submitHandler);
        }

        // Do NOT allow event DOWN being sent multiple times during kClickInterval
        // since UI element of Unity needs time to perform transitions.
        const float kClickInterval = 0.2f;
        private void HandleRaycastEvent()
        {
            ExitEnterHandler(ref s_raycastObjects, ref s_raycastObjectsEx);
            HoverHandler();

            bool submit = OnSubmit();
            if (submit)
            {
                SubmitHandler();
                return;
            }

            bool down = OnDown();
            bool hold = OnHold();
            if (!down && hold)
            {
                // Hold means to Drag.
                DragHandler();
            }
            else if (Time.unscaledTime - pointerData.clickTime < kClickInterval)
            {
                // Delay new events until kClickInterval has passed.
            }
            else if (down && !pointerData.eligibleForClick)
            {
                // 1. Not Down -> Down
                // 2. No pending Click should be procced.
                DownHandler();
            }
            else if (!hold)
            {
                // 1. If Down before, send Up event and clear Down state.
                // 2. If Dragging, send Drop & EndDrag event and clear Dragging state.
                // 3. If no Down or Dragging state, do NOTHING.
                UpHandler();
            }
        }
        #endregion

        #region Actions
        protected virtual bool OnDown() { return false; }
        protected virtual bool OnHold() { return false; }
        protected virtual bool OnSubmit() { return false; }
        #endregion
    }
}
