// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See LICENSE in the project root for license information.

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

namespace HoloToolkit.Unity.InputModule
{
    /// <summary>
    /// Focus manager is the bridge that handles different types of pointing sources like gaze cursor
    /// or pointing ray enabled motion controllers.
    /// If you don't have pointing ray enabled controllers, it defaults to GazeManager.
    /// </summary>
    public class FocusManager : Singleton<FocusManager>
    {
        #region MonoBehaviour Implementation

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

            if (registeredPointers != null)
            {
                for (int iPointer = 0; iPointer < registeredPointers.Length; iPointer++)
                {
                    GameObject owner = registeredPointers[iPointer];

                    if (owner == null)
                    {
                        Debug.LogError("AutoRegisteredPointers contains a null (\"None\") object.");
                        break;
                    }

                    IPointingSource pointingSource = owner.GetComponent<IPointingSource>();

                    if (pointingSource == null)
                    {
                        Debug.LogErrorFormat("AutoRegisteredPointers contains object \"{0}\" which is missing its {1} component.",
                            owner.name,
                            typeof(IPointingSource).Name
                        );
                        break;
                    }

                    RegisterPointer(pointingSource);
                }
            }
        }

        private void Start()
        {
            if (pointers.Count == 0 && autoRegisterGazePointerIfNoPointersRegistered && GazeManager.IsInitialized)
            {
                RegisterPointer(GazeManager.Instance);
            }
        }

        private void Update()
        {
            UpdatePointers();
            UpdateFocusedObjects();
        }

        #endregion

        #region Settings

        /// <summary>
        /// Maximum distance at which the pointer can collide with an object.
        /// </summary>
        [SerializeField]
        private float pointingExtent = 10f;

        /// <summary>
        /// The LayerMasks, in prioritized order, that are used to determine the HitObject when raycasting.
        ///
        /// Example Usage:
        ///
        /// Allow the cursor to hit SR, but first prioritize any DefaultRaycastLayers (potentially behind SR)
        ///
        /// int sr = LayerMask.GetMask("SR");
        /// int nonSR = Physics.DefaultRaycastLayers &amp; ~sr;
        /// GazeManager.Instance.RaycastLayerMasks = new LayerMask[] { nonSR, sr };
        /// </summary>
        [SerializeField]
        [Tooltip("The LayerMasks, in prioritized order, that are used to determine the HitObject when raycasting.")]
        private LayerMask[] pointingRaycastLayerMasks = { Physics.DefaultRaycastLayers };

        [SerializeField]
        private GameObject[] registeredPointers = null;

        [SerializeField]
        private bool autoRegisterGazePointerIfNoPointersRegistered = true;

        [SerializeField]
        private bool debugDrawPointingRays = false;

        [SerializeField]
        private Color[] debugDrawPointingRayColors = null;

        #endregion

        #region Data

        private class PointerData : PointerResult
        {
            public readonly IPointingSource PointingSource;

            private PointerInputEventData pointerData;
            public PointerInputEventData UnityUIPointerData
            {
                get
                {
                    if (pointerData == null)
                    {
                        pointerData = new PointerInputEventData(EventSystem.current);
                    }

                    return pointerData;
                }
            }

            public PointerData(IPointingSource pointingSource)
            {
                PointingSource = pointingSource;
            }

            [Obsolete("Use UpdateHit(RaycastHit hit, RayStep sourceRay, int rayStepIndex) or UpdateHit (float extent)")]
            public void UpdateHit(RaycastHit hit)
            {
                throw new NotImplementedException();
            }

            public void UpdateHit(RaycastHit hit, RayStep sourceRay, int rayStepIndex)
            {
                LastRaycastHit = hit;
                PreviousEndObject = End.Object;
                RayStepIndex = rayStepIndex;

                StartPoint = sourceRay.Origin;
                End = new FocusDetails
                {
                    Point = hit.point,
                    Normal = hit.normal,
                    Object = hit.transform.gameObject
                };
            }

            public void UpdateHit(RaycastResult result, RaycastHit hit, RayStep sourceRay, int rayStepIndex)
            {
                // We do not update the PreviousEndObject here because
                // it's already been updated in the first physics raycast.

                RayStepIndex = rayStepIndex;
                StartPoint = sourceRay.Origin;
                End = new FocusDetails
                {
                    Point = hit.point,
                    Normal = hit.normal,
                    Object = result.gameObject
                };
            }

            public void UpdateHit(float extent)
            {
                PreviousEndObject = End.Object;

                RayStep firstStep = PointingSource.Rays[0];
                RayStep finalStep = PointingSource.Rays[PointingSource.Rays.Length - 1];
                RayStepIndex = 0;

                StartPoint = firstStep.Origin;
                End = new FocusDetails
                {
                    Point = finalStep.Terminus,
                    Normal = (-finalStep.Direction),
                    Object = null
                };
            }

            public void ResetFocusedObjects(bool clearPreviousObject = true)
            {
                if (clearPreviousObject)
                {
                    PreviousEndObject = null;
                }

                End = new FocusDetails
                {
                    Point = End.Point,
                    Normal = End.Normal,
                    Object = null
                };
            }
        }

        private readonly List<PointerData> pointers = new List<PointerData>(0);

        /// <summary>
        /// GazeManager is a little special, so we keep track of it even if it's not a registered pointer. For the sake
        /// of StabilizationPlaneModifier and potentially other components that care where the user's looking, we need
        /// to do a gaze raycast even if gaze isn't used for focus.
        /// </summary>
        private PointerData gazeManagerPointingData;

        [Obsolete("Use GetGazePointerEventData or GetSpecificPointerEventData")]
        public PointerInputEventData UnityUIPointerEvent { get; private set; }

        private readonly HashSet<GameObject> pendingOverallFocusEnterSet = new HashSet<GameObject>();
        private readonly HashSet<GameObject> pendingOverallFocusExitSet = new HashSet<GameObject>();
        private readonly List<PointerData> pendingPointerSpecificFocusChange = new List<PointerData>();

        /// <summary>
        /// Cached vector 3 reference to the new raycast position.
        /// <remarks>Only used to update UI raycast results.</remarks>
        /// </summary>
        private Vector3 newUiRaycastPosition = Vector3.zero;

        /// <summary>
        /// Private uiRaycastCamera used primarily for UI pointer data.
        /// </summary>
        [SerializeField]
        private Camera uiRaycastCamera;

        /// <summary>
        /// The Camera the Event System uses to raycast against.
        /// <remarks>Every uGUI canvas in your scene should use this camera as its event camera.</remarks>
        /// </summary>
        public Camera UIRaycastCamera
        {
            get
            {
                if (uiRaycastCamera == null)
                {
                    Debug.LogWarning("No UIRaycastCamera assigned! Falling back to the RaycastCamera.\n" +
                                     "It's highly recommended to use the RaycastCamera found on the EventSystem of this InputManager.");
                    uiRaycastCamera = GetComponentInChildren<Camera>();
                }

                return uiRaycastCamera;
            }
        }

        #endregion

        #region Accessors

        public void RegisterPointer(IPointingSource pointingSource)
        {
            Debug.Assert(pointingSource != null, "Can't register a pointer if you give us one.");

            int pointerIndex;
            PointerData pointer;

            if (TryGetPointerIndex(pointingSource, out pointerIndex))
            {
                // This pointing source is already registered and active.
                return;
            }

            if (pointingSource is GazeManager)
            {
                if (gazeManagerPointingData == null)
                {
                    if (GazeManager.IsInitialized)
                    {
                        gazeManagerPointingData = new PointerData(GazeManager.Instance);
                    }
                }
                else
                {
                    Debug.Assert(ReferenceEquals(gazeManagerPointingData.PointingSource, GazeManager.Instance));
                    gazeManagerPointingData.ResetFocusedObjects();
                }

                Debug.Assert(gazeManagerPointingData != null);
                pointer = gazeManagerPointingData;
            }
            else
            {
                pointer = new PointerData(pointingSource);
            }

            pointers.Add(pointer);
        }

        public void UnregisterPointer(IPointingSource pointingSource)
        {
            Debug.Assert(pointingSource != null, "Can't unregister a pointer if you give us one.");

            int pointerIndex;
            TryGetPointerIndex(pointingSource, out pointerIndex);
            Debug.Assert(pointerIndex >= 0, "Invalid pointer index!");

            PointerData pointer;
            GetPointerData(pointingSource, out pointer);
            Debug.Assert(pointer != null, "Attempting to unregister a pointer that was never registered!");

            // Should we be protecting against unregistering the GazeManager?

            pointers.RemoveAt(pointerIndex);

            // Raise focus events if needed:

            if (pointer.End.Object != null)
            {
                GameObject unfocusedObject = pointer.End.Object;

                bool objectIsStillFocusedByOtherPointer = false;

                for (int iOther = 0; iOther < pointers.Count; iOther++)
                {
                    if (pointers[iOther].End.Object == unfocusedObject)
                    {
                        objectIsStillFocusedByOtherPointer = true;
                        break;
                    }
                }

                if (!objectIsStillFocusedByOtherPointer)
                {
                    RaiseFocusExitedEvents(unfocusedObject);
                }

                RaisePointerSpecificFocusChangedEvents(pointer.PointingSource, unfocusedObject, null);
            }
        }

        public FocusDetails? TryGetFocusDetails(BaseEventData eventData)
        {
            for (int i = 0; i < pointers.Count; i++)
            {
                if (pointers[i].PointingSource.OwnsInput(eventData))
                {
                    return pointers[i].End;
                }
            }

            return null;
        }

        public GameObject TryGetFocusedObject(BaseEventData eventData)
        {
            IPointingSource pointingSource;
            TryGetPointingSource(eventData, out pointingSource);
            PointerInputEventData pointerInputEventData = GetSpecificPointerEventData(pointingSource);

            // GetSpecificPointerEventData can return null. Be sure to handle that case.
            if (pointerInputEventData == null)
            {
                return null;
            }
            return pointerInputEventData.selectedObject;
        }

        public bool TryGetPointingSource(BaseEventData eventData, out IPointingSource pointingSource)
        {
            // pre-initialize pointingSource to null, assuming we will return false
            pointingSource = null;
            if (eventData == null) { return false; }

            for (int i = 0; i < pointers.Count; i++)
            {
                if (pointers[i].PointingSource.OwnsInput(eventData))
                {
                    pointingSource = pointers[i].PointingSource;
                    return true;
                }
            }

            return false;
        }

        public FocusDetails GetFocusDetails(IPointingSource pointingSource)
        {
            PointerData pointerData;
            FocusDetails details = default(FocusDetails);

            if (GetPointerData(pointingSource, out pointerData))
            {
                details = pointerData.End;
            }

            return details;
        }

        public GameObject GetFocusedObject(IPointingSource pointingSource)
        {
            PointerData pointerData;
            GameObject focusedObject = null;

            if (GetPointerData(pointingSource, out pointerData))
            {
                focusedObject = pointerData.End.Object;
            }

            return focusedObject;
        }

        /// <summary>
        /// Checks if exactly one pointer is registered and returns it if so.
        /// </summary>
        /// <returns>The registered pointer if exactly one is registered, null otherwise.</returns>
        public bool TryGetSinglePointer(out IPointingSource pointingSource)
        {
            if (pointers.Count == 1)
            {
                pointingSource = pointers[0].PointingSource;
                return true;
            }

            pointingSource = null;
            return false;
        }

        public delegate void FocusEnteredMethod(GameObject focusedObject);
        public event FocusEnteredMethod FocusEntered;

        public delegate void FocusExitedMethod(GameObject unfocusedObject);
        public event FocusExitedMethod FocusExited;

        public delegate void PointerSpecificFocusChangedMethod(IPointingSource pointer, GameObject oldFocusedObject, GameObject newFocusedObject);
        public event PointerSpecificFocusChangedMethod PointerSpecificFocusChanged;

        [Obsolete("Use either GetGazePointerEventData or GetSpecificPointerEventData")]
        public PointerInputEventData GetPointerEventData()
        {
            return GetGazePointerEventData();
        }

        public PointerInputEventData GetGazePointerEventData()
        {
            return gazeManagerPointingData.UnityUIPointerData;
        }

        public PointerInputEventData GetSpecificPointerEventData(IPointingSource pointer)
        {
            PointerData pointerEventData;

            if (!GetPointerData(pointer, out pointerEventData)) { return null; }

            pointerEventData.UnityUIPointerData.selectedObject = GetFocusedObject(pointer);
            return pointerEventData.UnityUIPointerData;
        }

        public float GetPointingExtent(IPointingSource pointingSource)
        {
            return pointingSource.ExtentOverride ?? pointingExtent;
        }

        #endregion

        #region Utilities

        private void UpdatePointers()
        {
            bool gazeManagerIsRegistered = false;

            for (int iPointer = 0; iPointer < pointers.Count; iPointer++)
            {
                PointerData pointer = pointers[iPointer];

                if (pointer == gazeManagerPointingData)
                {
                    gazeManagerIsRegistered = true;
                }

                UpdatePointer(pointer);

                if (debugDrawPointingRays)
                {
                    Color rayColor;

                    if ((debugDrawPointingRayColors != null) && (debugDrawPointingRayColors.Length > 0))
                    {
                        rayColor = debugDrawPointingRayColors[iPointer % debugDrawPointingRayColors.Length];
                    }
                    else
                    {
                        rayColor = Color.green;
                    }

                    Debug.DrawRay(pointer.StartPoint, (pointer.End.Point - pointer.StartPoint), rayColor);
                }
            }

            if (gazeManagerPointingData != null)
            {
                Debug.Assert(ReferenceEquals(gazeManagerPointingData.PointingSource, GazeManager.Instance));

                if (!gazeManagerIsRegistered)
                {
                    UpdatePointer(gazeManagerPointingData);
                }

                GazeManager.Instance.UpdateHitDetails(gazeManagerPointingData.End, gazeManagerPointingData.LastRaycastHit, gazeManagerIsRegistered);
            }
        }

        private void UpdatePointer(PointerData pointer)
        {
            // Call the pointer's OnPreRaycast function
            // This will give it a chance to prepare itself for raycasts
            // eg, by building its Rays array
            pointer.PointingSource.OnPreRaycast();

            // If pointer interaction isn't enabled, clear its result object and return
            if (!pointer.PointingSource.InteractionEnabled)
            {
                // Don't clear the previous focused object since we still want to trigger FocusExit events
                pointer.ResetFocusedObjects(false);
            }
            else
            {
                // If the pointer is locked
                // Keep the focus objects the same
                // This will ensure that we execute events on those objects
                // even if the pointer isn't pointing at them
                if (!pointer.PointingSource.FocusLocked)
                {
                    // Otherwise, continue
                    var prioritizedLayerMasks = (pointer.PointingSource.PrioritizedLayerMasksOverride ?? pointingRaycastLayerMasks);

                    // Perform raycast to determine focused object
                    RaycastPhysics(pointer, prioritizedLayerMasks);

                    // If we have a unity event system, perform graphics raycasts as well to support Unity UI interactions
                    if (EventSystem.current != null)
                    {
                        // NOTE: We need to do this AFTER RaycastPhysics so we use the current hit point to perform the correct 2D UI Raycast.
                        RaycastUnityUI(pointer, prioritizedLayerMasks);
                    }

                    // Set the pointer's result last
                    pointer.PointingSource.Result = pointer;
                }
            }

            // Call the pointer's OnPostRaycast function
            // This will give it a chance to respond to raycast results
            // eg by updating its appearance
            pointer.PointingSource.OnPostRaycast();
        }

        /// <summary>
        /// Perform a Unity physics Raycast to determine which scene objects with a collider is currently being gazed at, if any.
        /// </summary>
        private void RaycastPhysics(PointerData pointer, LayerMask[] prioritizedLayerMasks)
        {
            bool isHit = false;
            int rayStepIndex = 0;
            RayStep rayStep = default(RayStep);
            RaycastHit physicsHit = default(RaycastHit);

            // Comment back in GetType() only when debugging for a specific pointer.
            Debug.Assert(pointer.PointingSource.Rays != null, "No valid rays for pointer "/* + pointer.GetType()*/);
            Debug.Assert(pointer.PointingSource.Rays.Length > 0, "No valid rays for pointer "/* + pointer.GetType()*/);

            // Check raycast for each step in the pointing source
            for (int i = 0; i < pointer.PointingSource.Rays.Length; i++)
            {
                if (RaycastPhysicsStep(pointer.PointingSource.Rays[i], prioritizedLayerMasks, out physicsHit))
                {
                    // Set the pointer source's origin ray to this step
                    isHit = true;
                    rayStep = pointer.PointingSource.Rays[i];
                    rayStepIndex = i;
                    // No need to continue once we've hit something
                    break;
                }
            }

            if (isHit)
            {
                pointer.UpdateHit(physicsHit, rayStep, rayStepIndex);
            }
            else
            {
                pointer.UpdateHit(GetPointingExtent(pointer.PointingSource));
            }
        }

        private bool RaycastPhysicsStep(RayStep step, LayerMask[] prioritizedLayerMasks, out RaycastHit physicsHit)
        {
            bool isHit = false;
            physicsHit = default(RaycastHit);

            // If there is only one priority, don't prioritize
            if (prioritizedLayerMasks.Length == 1)
            {
                isHit = Physics.Raycast(step.Origin, step.Direction, out physicsHit, step.Length, prioritizedLayerMasks[0]);
            }
            else
            {
                // Raycast across all layers and prioritize
                RaycastHit? hit = PrioritizeHits(Physics.RaycastAll(step.Origin, step.Direction, step.Length, Physics.AllLayers), prioritizedLayerMasks);
                isHit = hit.HasValue;

                if (isHit)
                {
                    physicsHit = hit.Value;
                }
            }

            return isHit;
        }

        private void RaycastUnityUI(PointerData pointer, LayerMask[] prioritizedLayerMasks)
        {
            Debug.Assert(pointer.End.Point != Vector3.zero, "No pointer source end point found to raycast against!");
            Debug.Assert(UIRaycastCamera != null, "You must assign a UIRaycastCamera on the FocusManager before you can process uGUI raycasting.");

            RaycastResult uiRaycastResult = default(RaycastResult);
            bool overridePhysicsRaycast = false;
            RayStep rayStep = default(RayStep);
            int rayStepIndex = 0;

            // Comment back in GetType() only when debugging for a specific pointer.
            Debug.Assert(pointer.PointingSource.Rays != null, "No valid rays for pointer "/* + pointer.GetType()*/);
            Debug.Assert(pointer.PointingSource.Rays.Length > 0, "No valid rays for pointer "/* + pointer.GetType()*/);

            // Cast rays for every step until we score a hit
            for (int i = 0; i < pointer.PointingSource.Rays.Length; i++)
            {
                if (RaycastUnityUIStep(pointer, pointer.PointingSource.Rays[i], prioritizedLayerMasks, out overridePhysicsRaycast, out uiRaycastResult))
                {
                    rayStepIndex = i;
                    rayStep = pointer.PointingSource.Rays[i];
                    break;
                }
            }

            // Check if we need to overwrite the physics raycast info
            if ((pointer.End.Object == null || overridePhysicsRaycast) && uiRaycastResult.isValid &&
                 uiRaycastResult.module != null && uiRaycastResult.module.eventCamera == UIRaycastCamera)
            {
                newUiRaycastPosition.x = uiRaycastResult.screenPosition.x;
                newUiRaycastPosition.y = uiRaycastResult.screenPosition.y;
                newUiRaycastPosition.z = uiRaycastResult.distance;

                Vector3 worldPos = UIRaycastCamera.ScreenToWorldPoint(newUiRaycastPosition);

                var hitInfo = new RaycastHit
                {
                    point = worldPos,
                    normal = -uiRaycastResult.gameObject.transform.forward
                };

                pointer.UpdateHit(uiRaycastResult, hitInfo, rayStep, rayStepIndex);
            }
        }

        private bool RaycastUnityUIStep(PointerData pointer, RayStep step, LayerMask[] prioritizedLayerMasks, out bool overridePhysicsRaycast, out RaycastResult uiRaycastResult)
        {
            // Move the uiRaycast camera to the current pointer's position.
            UIRaycastCamera.transform.position = step.Origin;
            UIRaycastCamera.transform.forward = step.Direction;

            // We always raycast from the center of the camera.
            pointer.UnityUIPointerData.position = new Vector2(UIRaycastCamera.pixelWidth * 0.5f, UIRaycastCamera.pixelHeight * 0.5f);

            // Graphics raycast
            uiRaycastResult = EventSystem.current.Raycast(pointer.UnityUIPointerData, prioritizedLayerMasks);
            pointer.UnityUIPointerData.pointerCurrentRaycast = uiRaycastResult;

            overridePhysicsRaycast = false;

            // If we have a raycast result, check if we need to overwrite the physics raycast info
            if (uiRaycastResult.gameObject != null)
            {
                if (pointer.End.Object != null)
                {
                    // Check layer prioritization
                    if (prioritizedLayerMasks.Length > 1)
                    {
                        // Get the index in the prioritized layer masks
                        int uiLayerIndex = uiRaycastResult.gameObject.layer.FindLayerListIndex(prioritizedLayerMasks);
                        int threeDLayerIndex = pointer.LastRaycastHit.collider.gameObject.layer.FindLayerListIndex(prioritizedLayerMasks);

                        if (threeDLayerIndex > uiLayerIndex)
                        {
                            overridePhysicsRaycast = true;
                        }
                        else if (threeDLayerIndex == uiLayerIndex)
                        {
                            if (pointer.LastRaycastHit.distance > uiRaycastResult.distance)
                            {
                                overridePhysicsRaycast = true;
                            }
                        }
                    }
                    else
                    {
                        if (pointer.LastRaycastHit.distance > uiRaycastResult.distance)
                        {
                            overridePhysicsRaycast = true;
                        }
                    }
                }
                // If we've hit something, no need to go further
                return true;
            }
            // If we haven't hit something, keep going
            return false;
        }

        private void UpdateFocusedObjects()
        {
            Debug.Assert(pendingPointerSpecificFocusChange.Count == 0);
            Debug.Assert(pendingOverallFocusExitSet.Count == 0);
            Debug.Assert(pendingOverallFocusEnterSet.Count == 0);

            // NOTE: We compute the set of events to send before sending the first event
            //       just in case someone responds to the event by adding/removing a
            //       pointer which would change the structures we're iterating over.

            for (int iPointer = 0; iPointer < pointers.Count; iPointer++)
            {
                PointerData pointer = pointers[iPointer];

                if (pointer.PreviousEndObject != pointer.End.Object)
                {
                    pendingPointerSpecificFocusChange.Add(pointer);

                    // Initially, we assume all pointer-specific focus changes will result
                    // also result in an overall focus change...

                    if (pointer.PreviousEndObject != null)
                    {
                        pendingOverallFocusExitSet.Add(pointer.PreviousEndObject);
                    }

                    if (pointer.End.Object != null)
                    {
                        pendingOverallFocusEnterSet.Add(pointer.End.Object);
                    }
                }
            }

            // ... but now we trim out objects whose overall focus was maintained the same by a different pointer:

            for (int iPointer = 0; iPointer < pointers.Count; iPointer++)
            {
                PointerData pointer = pointers[iPointer];

                pendingOverallFocusExitSet.Remove(pointer.End.Object);

                pendingOverallFocusEnterSet.Remove(pointer.PreviousEndObject);
            }

            // Now we raise the events:

            foreach (GameObject exit in pendingOverallFocusExitSet)
            {
                RaiseFocusExitedEvents(exit);
            }

            foreach (GameObject enter in pendingOverallFocusEnterSet)
            {
                RaiseFocusEnteredEvents(enter);
            }

            for (int iChange = 0; iChange < pendingPointerSpecificFocusChange.Count; iChange++)
            {
                PointerData change = pendingPointerSpecificFocusChange[iChange];

                RaisePointerSpecificFocusChangedEvents(change.PointingSource, change.PreviousEndObject, change.End.Object);
            }

            pendingOverallFocusEnterSet.Clear();
            pendingOverallFocusExitSet.Clear();
            pendingPointerSpecificFocusChange.Clear();
        }

        private void RaiseFocusExitedEvents(GameObject unfocusedObject)
        {
            InputManager.Instance.RaiseFocusExit(unfocusedObject);
            //Debug.Log("Focus Exit: " + unfocusedObject.name);
            if (FocusExited != null)
            {
                FocusExited(unfocusedObject);
            }
        }

        private void RaiseFocusEnteredEvents(GameObject focusedObject)
        {
            InputManager.Instance.RaiseFocusEnter(focusedObject);
            //Debug.Log("Focus Enter: " + focusedObject.name);
            if (FocusEntered != null)
            {
                FocusEntered(focusedObject);
            }
        }

        private void RaisePointerSpecificFocusChangedEvents(IPointingSource pointer, GameObject oldFocusedObject, GameObject newFocusedObject)
        {
            InputManager.Instance.RaisePointerSpecificFocusChangedEvents(pointer, oldFocusedObject, newFocusedObject);

            if (PointerSpecificFocusChanged != null)
            {
                PointerSpecificFocusChanged(pointer, oldFocusedObject, newFocusedObject);
            }
        }

        private bool GetPointerData(IPointingSource pointingSource, out PointerData pointerData)
        {
            int pointerIndex;

            if (TryGetPointerIndex(pointingSource, out pointerIndex))
            {
                pointerData = pointers[pointerIndex];
                return true;
            }

            pointerData = null;
            return false;
        }

        private bool TryGetPointerIndex(IPointingSource pointingSource, out int pointerIndex)
        {
            for (int i = 0; i < pointers.Count; i++)
            {
                if (pointingSource == pointers[i].PointingSource)
                {
                    pointerIndex = i;
                    return true;
                }
            }

            pointerIndex = -1;
            return false;
        }

        private RaycastHit? PrioritizeHits(RaycastHit[] hits, LayerMask[] layerMasks)
        {
            if (hits.Length == 0)
            {
                return null;
            }

            // Return the minimum distance hit within the first layer that has hits.
            // In other words, sort all hit objects first by layerMask, then by distance.
            for (int layerMaskIdx = 0; layerMaskIdx < layerMasks.Length; layerMaskIdx++)
            {
                RaycastHit? minHit = null;

                for (int hitIdx = 0; hitIdx < hits.Length; hitIdx++)
                {
                    RaycastHit hit = hits[hitIdx];
                    if (hit.transform.gameObject.layer.IsInLayerMask(layerMasks[layerMaskIdx]) &&
                        (minHit == null || hit.distance < minHit.Value.distance))
                    {
                        minHit = hit;
                    }
                }

                if (minHit != null)
                {
                    return minHit;
                }
            }

            return null;
        }

        /// <summary>
        /// Helper for assigning world space canvases event cameras.
        /// <remarks>Warning! Very expensive. Use sparingly at runtime.</remarks>
        /// </summary>
        public void UpdateCanvasEventSystems()
        {
            Debug.Assert(UIRaycastCamera != null, "You must assign a UIRaycastCamera on the FocusManager before updating your canvases.");

            // This will also find disabled GameObjects in the scene.
            // Warning! this look up is very expensive!
            var sceneCanvases = Resources.FindObjectsOfTypeAll<Canvas>();

            for (var i = 0; i < sceneCanvases.Length; i++)
            {
                if (sceneCanvases[i].isRootCanvas && sceneCanvases[i].renderMode == RenderMode.WorldSpace)
                {
                    sceneCanvases[i].worldCamera = UIRaycastCamera;
                }
            }
        }

        #endregion
    }
}
