/*
 * @author Valentin Simonov / http://va.lent.in/
 */

using System;
using System.Collections.Generic;
using System.Reflection;
using TouchScript.Hit;
using TouchScript.Pointers;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
using Pointer = TouchScript.Pointers.Pointer;

namespace TouchScript.Layers.UI
{
    internal sealed class TouchScriptInputModule : BaseInputModule
    {

        #region Public properties

        public static TouchScriptInputModule Instance
        {
            get
            {
				if (shuttingDown) return null;
                if (instance == null)
                {
					var es = EventSystem.current;
                    if (es == null)
					{
						es = FindObjectOfType<EventSystem>();
						if (es == null)
						{
							var go = new GameObject("EventSystem");
							es = go.AddComponent<EventSystem>();
						}
					}
                    instance = es.GetComponent<TouchScriptInputModule>();
					if (instance == null) instance = es.gameObject.AddComponent<TouchScriptInputModule>();
                }
                return instance;
            }
        }

        public string HorizontalAxis = "Horizontal";
        public string VerticalAxis = "Vertical";
        public string SubmitButton = "Submit";
        public string CancelButton = "Cancel";
        public float InputActionsPerSecond = 10;
        public float RepeatDelay = 0.5f;

        #endregion

        #region Private variables

		private static bool shuttingDown = false;
        private static TouchScriptInputModule instance;
        private static FieldInfo raycastersProp;
        private static PropertyInfo canvasProp;
        private static Dictionary<int, Canvas> raycasterCanvasCache = new Dictionary<int, Canvas>();

        private int refCount = 0;
        private UIStandardInputModule ui;

        #endregion

        #region Constructor

        private TouchScriptInputModule()
        {
            if (raycastersProp == null)
            {
                raycastersProp = Type.GetType(Assembly.CreateQualifiedName("UnityEngine.UI", "UnityEngine.EventSystems.RaycasterManager")).
                                      GetField("s_Raycasters", BindingFlags.NonPublic | BindingFlags.Static);
                canvasProp = typeof (GraphicRaycaster).GetProperty("canvas", BindingFlags.NonPublic | BindingFlags.Instance);
            }
        }

        #endregion

        #region Unity methods

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

            if (instance == null) instance = this;
            else
            {
                if (instance == this) return;
                Destroy(this);
            }
        }

        protected override void OnDisable()
        {
            disable();
            if (instance == this) instance = null;
            base.OnDisable();
        }

		private void OnApplicationQuit()
		{
			shuttingDown = true;
		}

        #endregion

        #region Public methods

        public List<BaseRaycaster> GetRaycasters()
        {
            return raycastersProp.GetValue(null) as List<BaseRaycaster>;
        }

        public Canvas GetCanvasForRaycaster(BaseRaycaster raycaster)
        {
            var id = raycaster.GetInstanceID();
            Canvas canvas;
            if (!raycasterCanvasCache.TryGetValue(id, out canvas))
            {
                canvas = canvasProp.GetValue(raycaster, null) as Canvas;
                raycasterCanvasCache.Add(id, canvas);
            }
            return canvas;
        }

        public override void Process()
        {
			if (ui != null) ui.Process();
        }

        public override bool IsPointerOverGameObject(int pointerId)
        {
            return base.IsPointerOverGameObject(pointerId);
        }

        public override bool ShouldActivateModule()
        {
            return true;
        }

        public override bool IsModuleSupported()
        {
            return true;
        }

        public override void DeactivateModule() {}

        public override void ActivateModule() {}

        public override void UpdateModule() {}

        #endregion

        #region Internal methods

        internal void INTERNAL_Retain()
        {
            refCount++;
            if (refCount == 1) enable();
        }

        internal int INTERNAL_Release()
        {
            if (--refCount <= 0) disable();
            return refCount;
        }

        #endregion

        #region Private functions

        private void enable()
        {
            ui = new UIStandardInputModule(this);
            TouchManager.Instance.PointersUpdated += ui.ProcessUpdated;
            TouchManager.Instance.PointersPressed += ui.ProcessPressed;
            TouchManager.Instance.PointersReleased += ui.ProcessReleased;
            TouchManager.Instance.PointersRemoved += ui.ProcessRemoved;
            TouchManager.Instance.PointersCancelled += ui.ProcessCancelled;
        }

        private void disable()
        {
			if (TouchManager.Instance != null && ui != null)
            {
                TouchManager.Instance.PointersUpdated -= ui.ProcessUpdated;
                TouchManager.Instance.PointersPressed -= ui.ProcessPressed;
                TouchManager.Instance.PointersReleased -= ui.ProcessReleased;
                TouchManager.Instance.PointersRemoved -= ui.ProcessRemoved;
                TouchManager.Instance.PointersCancelled -= ui.ProcessCancelled;
            }
            refCount = 0;
        }

        #endregion

        #region Event handlers

        #endregion

        #region Copypasted code from UI

        // last update: df1947cd (5.4f3)
        private class UIStandardInputModule
        {

            protected TouchScriptInputModule input;

            public UIStandardInputModule(TouchScriptInputModule input)
            {
                this.input = input;
            }

            #region Unchanged

            private int m_ConsecutiveMoveCount = 0;
            private Vector2 m_LastMoveVector;
            private float m_PrevActionTime;

            private Dictionary<int, PointerEventData> m_PointerData = new Dictionary<int, PointerEventData>();

            protected bool GetPointerData(int id, out PointerEventData data, bool create)
            {
                if (!m_PointerData.TryGetValue(id, out data) && create)
                {
                    data = new PointerEventData(input.eventSystem)
                    {
                        pointerId = id,
                    };
                    m_PointerData.Add(id, data);
                    return true;
                }
                return false;
            }

            protected void DeselectIfSelectionChanged(GameObject currentOverGo, BaseEventData pointerEvent)
            {
                // Selection tracking
                var selectHandlerGO = ExecuteEvents.GetEventHandler<ISelectHandler>(currentOverGo);
                // if we have clicked something new, deselect the old thing
                // leave 'selection handling' up to the press event though.
                if (selectHandlerGO != input.eventSystem.currentSelectedGameObject)
                    input.eventSystem.SetSelectedGameObject(null, pointerEvent);
            }

            private static bool ShouldStartDrag(Vector2 pressPos, Vector2 currentPos, float threshold, bool useDragThreshold)
            {
                if (!useDragThreshold)
                    return true;

                return (pressPos - currentPos).sqrMagnitude >= threshold * threshold;
            }

            private bool SendUpdateEventToSelectedObject()
            {
                if (input.eventSystem.currentSelectedGameObject == null)
                    return false;

                var data = input.GetBaseEventData();
                ExecuteEvents.Execute(input.eventSystem.currentSelectedGameObject, data, ExecuteEvents.updateSelectedHandler);
                return data.used;
            }

            private bool SendMoveEventToSelectedObject()
            {
                float time = Time.unscaledTime;

                Vector2 movement = GetRawMoveVector();
                if (Mathf.Approximately(movement.x, 0f) && Mathf.Approximately(movement.y, 0f))
                {
                    m_ConsecutiveMoveCount = 0;
                    return false;
                }

                // If user pressed key again, always allow event
                bool allow = Input.GetButtonDown(input.HorizontalAxis) || Input.GetButtonDown(input.VerticalAxis);
                bool similarDir = (Vector2.Dot(movement, m_LastMoveVector) > 0);
                if (!allow)
                {
                    // Otherwise, user held down key or axis.
                    // If direction didn't change at least 90 degrees, wait for delay before allowing consequtive event.
                    if (similarDir && m_ConsecutiveMoveCount == 1)
                        allow = (time > m_PrevActionTime + input.RepeatDelay);
                    // If direction changed at least 90 degree, or we already had the delay, repeat at repeat rate.
                    else
                        allow = (time > m_PrevActionTime + 1f / input.InputActionsPerSecond);
                }
                if (!allow)
                    return false;

                // Debug.Log(m_ProcessingEvent.rawType + " axis:" + m_AllowAxisEvents + " value:" + "(" + x + "," + y + ")");
                var axisEventData = input.GetAxisEventData(movement.x, movement.y, 0.6f);

                if (axisEventData.moveDir != MoveDirection.None)
                {
                    ExecuteEvents.Execute(input.eventSystem.currentSelectedGameObject, axisEventData, ExecuteEvents.moveHandler);
                    if (!similarDir)
                        m_ConsecutiveMoveCount = 0;
                    m_ConsecutiveMoveCount++;
                    m_PrevActionTime = time;
                    m_LastMoveVector = movement;
                }
                else
                {
                    m_ConsecutiveMoveCount = 0;
                }

                return axisEventData.used;
            }

            private bool SendSubmitEventToSelectedObject()
            {
                if (input.eventSystem.currentSelectedGameObject == null)
                    return false;

                var data = input.GetBaseEventData();
                if (Input.GetButtonDown(input.SubmitButton))
                    ExecuteEvents.Execute(input.eventSystem.currentSelectedGameObject, data, ExecuteEvents.submitHandler);

                if (Input.GetButtonDown(input.CancelButton))
                    ExecuteEvents.Execute(input.eventSystem.currentSelectedGameObject, data, ExecuteEvents.cancelHandler);
                return data.used;
            }

            private Vector2 GetRawMoveVector()
            {
                Vector2 move = Vector2.zero;
                move.x = Input.GetAxisRaw(input.HorizontalAxis);
                move.y = Input.GetAxisRaw(input.VerticalAxis);

                if (Input.GetButtonDown(input.HorizontalAxis))
                {
                    if (move.x < 0)
                        move.x = -1f;
                    if (move.x > 0)
                        move.x = 1f;
                }
                if (Input.GetButtonDown(input.VerticalAxis))
                {
                    if (move.y < 0)
                        move.y = -1f;
                    if (move.y > 0)
                        move.y = 1f;
                }
                return move;
            }

            #endregion

            public void Process()
            {
                bool usedEvent = SendUpdateEventToSelectedObject();

                if (input.eventSystem.sendNavigationEvents)
                {
                    if (!usedEvent)
                        usedEvent |= SendMoveEventToSelectedObject();

                    if (!usedEvent)
                        SendSubmitEventToSelectedObject();
                }

                // touch needs to take precedence because of the mouse emulation layer
                //                if (!ProcessTouchEvents() && Input.mousePresent)
                //                    ProcessMouseEvent();
            }

            #region Changed

            protected void RemovePointerData(int id)
            {
                m_PointerData.Remove(id);
            }

            #endregion

            #region Event processors

            private void convertRaycast(RaycastHitUI old, ref RaycastResult current)
            {
                current.module = old.Raycaster;
                current.gameObject = old.GameObject;
                current.depth = old.Depth;
                current.index = old.GraphicIndex;
                current.sortingLayer = old.SortingLayer;
                current.sortingOrder = old.SortingOrder;
            }
            public virtual void ProcessUpdated(object sender, PointerEventArgs pointerEventArgs)
            {
                var pointers = pointerEventArgs.Pointers;
                var raycast = new RaycastResult();
                var count = pointers.Count;
                for (var i = 0; i < count; i++)
                {
                    var pointer = pointers[i];
                    PointerEventData data;
                    GetPointerData(pointer.Id, out data, true);
                    data.Reset();

                    var over = pointer.GetOverData();
                    var target = over.Target;
                    var currentOverGo = target == null ? null : target.gameObject;

                    data.position = pointer.Position;
                    data.delta = pointer.Position - pointer.PreviousPosition;
                    convertRaycast(over.RaycastHitUI, ref raycast);
                    raycast.screenPosition = data.position;
                    data.pointerCurrentRaycast = raycast;
                    
                    input.HandlePointerExitAndEnter(data, currentOverGo);

                    bool moving = data.IsPointerMoving();

                    if (moving && data.pointerDrag != null
                        && !data.dragging
                        && ShouldStartDrag(data.pressPosition, data.position, input.eventSystem.pixelDragThreshold, data.useDragThreshold))
                    {
                        ExecuteEvents.Execute(data.pointerDrag, data, ExecuteEvents.beginDragHandler);
                        data.dragging = true;
                    }

                    // Drag notification
                    if (data.dragging && moving && data.pointerDrag != null)
                    {
                        // Before doing drag we should cancel any pointer down state
                        // And clear selection!
                        if (data.pointerPress != data.pointerDrag)
                        {
                            ExecuteEvents.Execute(data.pointerPress, data, ExecuteEvents.pointerUpHandler);

                            data.eligibleForClick = false;
                            data.pointerPress = null;
                            data.rawPointerPress = null;
                        }
                        ExecuteEvents.Execute(data.pointerDrag, data, ExecuteEvents.dragHandler);
                    }

                    var mousePointer = pointer as MousePointer;
                    if (mousePointer != null && !Mathf.Approximately(mousePointer.ScrollDelta.sqrMagnitude, 0.0f))
                    {
                        data.scrollDelta = mousePointer.ScrollDelta;
                        var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(currentOverGo);
                        ExecuteEvents.ExecuteHierarchy(scrollHandler, data, ExecuteEvents.scrollHandler);
                    }
                }
            }

            public virtual void ProcessPressed(object sender, PointerEventArgs pointerEventArgs)
            {
                var pointers = pointerEventArgs.Pointers;
                var count = pointers.Count;
                for (var i = 0; i < count; i++)
                {
                    var pointer = pointers[i];
                    PointerEventData data;
                    GetPointerData(pointer.Id, out data, true);
                    var over = pointer.GetOverData();
                    var target = over.Target;
                    var currentOverGo = target == null ? null : target.gameObject;

                    data.eligibleForClick = true;
                    data.delta = Vector2.zero;
                    data.dragging = false;
                    data.useDragThreshold = true;
                    data.pressPosition = pointer.Position;
                    data.pointerPressRaycast = data.pointerCurrentRaycast;

                    DeselectIfSelectionChanged(currentOverGo, data);

                    if (data.pointerEnter != currentOverGo)
                    {
                        // send a pointer enter to the touched element if it isn't the one to select...
                        input.HandlePointerExitAndEnter(data, currentOverGo);
                        data.pointerEnter = currentOverGo;
                    }

                    // search for the control that will receive the press
                    // if we can't find a press handler set the press
                    // handler to be what would receive a click.
                    var newPressed = ExecuteEvents.ExecuteHierarchy(currentOverGo, data, ExecuteEvents.pointerDownHandler);

                    // didnt find a press handler... search for a click handler
                    if (newPressed == null)
                        newPressed = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

                    // Debug.Log("Pressed: " + newPressed);

                    float time = Time.unscaledTime;

                    if (newPressed == data.lastPress) // ?
                    {
                        var diffTime = time - data.clickTime;
                        if (diffTime < 0.3f)
                            ++data.clickCount;
                        else
                            data.clickCount = 1;

                        data.clickTime = time;
                    }
                    else
                    {
                        data.clickCount = 1;
                    }

                    data.pointerPress = newPressed;
                    data.rawPointerPress = currentOverGo;

                    data.clickTime = time;

                    // Save the drag handler as well
                    data.pointerDrag = ExecuteEvents.GetEventHandler<IDragHandler>(currentOverGo);

                    if (data.pointerDrag != null)
                        ExecuteEvents.Execute(data.pointerDrag, data, ExecuteEvents.initializePotentialDrag);
                }
            }

            public virtual void ProcessReleased(object sender, PointerEventArgs pointerEventArgs)
            {
                var pointers = pointerEventArgs.Pointers;
                var count = pointers.Count;
                for (var i = 0; i < count; i++)
                {
                    var pointer = pointers[i];
                    PointerEventData data;
                    GetPointerData(pointer.Id, out data, true);

                    var target = pointer.GetOverData().Target;
                    var currentOverGo = target == null ? null : target.gameObject;

                    ExecuteEvents.Execute(data.pointerPress, data, ExecuteEvents.pointerUpHandler);
                    var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);
                    if (data.pointerPress == pointerUpHandler && data.eligibleForClick)
                    {
                        ExecuteEvents.Execute(data.pointerPress, data, ExecuteEvents.pointerClickHandler);
                    }
                    else if (data.pointerDrag != null && data.dragging)
                    {
                        ExecuteEvents.ExecuteHierarchy(currentOverGo, data, ExecuteEvents.dropHandler);
                    }

                    data.eligibleForClick = false;
                    data.pointerPress = null;
                    data.rawPointerPress = null;

                    if (data.pointerDrag != null && data.dragging)
                        ExecuteEvents.Execute(data.pointerDrag, data, ExecuteEvents.endDragHandler);

                    data.dragging = false;
                    data.pointerDrag = null;

                    // send exit events as we need to simulate this on touch up on touch device
                    ExecuteEvents.ExecuteHierarchy(data.pointerEnter, data, ExecuteEvents.pointerExitHandler);
                    data.pointerEnter = null;

                    // redo pointer enter / exit to refresh state
                    // so that if we moused over somethign that ignored it before
                    // due to having pressed on something else
                    // it now gets it.
                    if (currentOverGo != data.pointerEnter)
                    {
                        input.HandlePointerExitAndEnter(data, null);
                        input.HandlePointerExitAndEnter(data, currentOverGo);
                    }
                }
            }

            public virtual void ProcessCancelled(object sender, PointerEventArgs pointerEventArgs)
            {
                var pointers = pointerEventArgs.Pointers;
                var count = pointers.Count;
                for (var i = 0; i < count; i++)
                {
                    var pointer = pointers[i];
                    PointerEventData data;
                    GetPointerData(pointer.Id, out data, true);

                    var target = pointer.GetOverData().Target;
                    var currentOverGo = target == null ? null : target.gameObject;

                    ExecuteEvents.Execute(data.pointerPress, data, ExecuteEvents.pointerUpHandler);

                    if (data.pointerDrag != null && data.dragging)
                    {
                        ExecuteEvents.ExecuteHierarchy(currentOverGo, data, ExecuteEvents.dropHandler);
                    }

                    data.eligibleForClick = false;
                    data.pointerPress = null;
                    data.rawPointerPress = null;

                    if (data.pointerDrag != null && data.dragging)
                        ExecuteEvents.Execute(data.pointerDrag, data, ExecuteEvents.endDragHandler);

                    data.dragging = false;
                    data.pointerDrag = null;

                    // send exit events as we need to simulate this on touch up on touch device
                    ExecuteEvents.ExecuteHierarchy(data.pointerEnter, data, ExecuteEvents.pointerExitHandler);
                    data.pointerEnter = null;
                }
            }

            public virtual void ProcessRemoved(object sender, PointerEventArgs pointerEventArgs)
            {
                var pointers = pointerEventArgs.Pointers;
                var count = pointers.Count;
                for (var i = 0; i < count; i++)
                {
                    var pointer = pointers[i];
                    PointerEventData data;
                    GetPointerData(pointer.Id, out data, true);

                    if (data.pointerEnter) ExecuteEvents.ExecuteHierarchy(data.pointerEnter, data, ExecuteEvents.pointerExitHandler);
                    RemovePointerData(pointer.Id);
                }
            }

            #endregion

        }

        #endregion

    }
}