using System;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;

namespace CarbonInput {
	[AddComponentMenu("Event/Carbon Input Module")]
	public class CarbonInputModule : PointerInputModule {
		private const float Threshold = 0.3f;
		public PlayerIndex Player = PlayerIndex.Any;
		public CAxis HorizontalAxis = CAxis.LX;
		public bool InvertHorizontal;
		public CAxis VerticalAxis = CAxis.LY;
		public bool InvertVertical = true;
		public CButton SubmitButton = CButton.A;
		public CButton CancelButton = CButton.B;

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

		private Vector2 m_LastMousePosition;
		private Vector2 m_MousePosition;
		private Vector2 _lastRawMove;

		private bool HorizontalPressed { get { return Math.Abs(_lastRawMove.x) < Threshold && Math.Abs(GamePad.GetAxis(HorizontalAxis, Player)) > Threshold; } }
		private bool VerticalPressed { get { return Math.Abs(_lastRawMove.y) < Threshold && Math.Abs(GamePad.GetAxis(VerticalAxis, Player)) > Threshold; } }

		protected CarbonInputModule() {
		}

		[Obsolete("Mode is no longer needed on input module as it handles both mouse and keyboard simultaneously.", false)]
		public enum InputMode {
			Mouse,
			Buttons
		}

		[Obsolete("Mode is no longer needed on input module as it handles both mouse and keyboard simultaneously.", false)]
		public InputMode inputMode {
			get { return InputMode.Mouse; }
		}

		[SerializeField]
		private float m_InputActionsPerSecond = 10;

		[SerializeField]
		private float m_RepeatDelay = 0.5f;

		[SerializeField]
		[FormerlySerializedAs("m_AllowActivationOnMobileDevice")]
		private bool m_ForceModuleActive;

		[Obsolete("allowActivationOnMobileDevice has been deprecated. Use forceModuleActive instead (UnityUpgradable) -> forceModuleActive")]
		public bool allowActivationOnMobileDevice {
			get { return m_ForceModuleActive; }
			set { m_ForceModuleActive = value; }
		}

		public bool forceModuleActive {
			get { return m_ForceModuleActive; }
			set { m_ForceModuleActive = value; }
		}

		public float inputActionsPerSecond {
			get { return m_InputActionsPerSecond; }
			set { m_InputActionsPerSecond = value; }
		}

		public float repeatDelay {
			get { return m_RepeatDelay; }
			set { m_RepeatDelay = value; }
		}

		public override void UpdateModule() {
			m_LastMousePosition = m_MousePosition;
			m_MousePosition = Input.mousePosition;
		}

		public override bool IsModuleSupported() {
			return m_ForceModuleActive || Input.mousePresent || Input.touchSupported;
		}

		public override bool ShouldActivateModule() {
			if(!base.ShouldActivateModule())
				return false;

			var state = GamePad.GetState(Player);
			var shouldActivate = m_ForceModuleActive;
			shouldActivate |= state.Pressed(SubmitButton);
			shouldActivate |= state.Pressed(CancelButton);
			shouldActivate |= GetRawMoveVector().sqrMagnitude > 0.0f;
			shouldActivate |= (m_MousePosition - m_LastMousePosition).sqrMagnitude > 0.0f;
			shouldActivate |= Input.GetMouseButtonDown(0);

			if(Input.touchCount > 0)
				shouldActivate = true;

			return shouldActivate;
		}

		public override void ActivateModule() {
			base.ActivateModule();
			m_MousePosition = Input.mousePosition;
			m_LastMousePosition = Input.mousePosition;

			var toSelect = eventSystem.currentSelectedGameObject;
			if(toSelect == null)
				toSelect = eventSystem.firstSelectedGameObject;

			eventSystem.SetSelectedGameObject(toSelect, GetBaseEventData());
		}

		public override void DeactivateModule() {
			base.DeactivateModule();
			ClearSelection();
		}

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

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

				if(!usedEvent)
					SendSubmitEventToSelectedObject();
			}

			// touch needs to take precedence because of the mouse emulation layer
			if(!ProcessTouchEvents())
				ProcessMouseEvent();
			_lastRawMove = GetRawMoveVector();
		}

		private bool ProcessTouchEvents() {
			for(int i = 0; i < Input.touchCount; ++i) {
				Touch input = Input.GetTouch(i);

				if(input.type == TouchType.Indirect)
					continue;

				bool released;
				bool pressed;
				var pointer = GetTouchPointerEventData(input, out pressed, out released);

				ProcessTouchPress(pointer, pressed, released);

				if(!released) {
					ProcessMove(pointer);
					ProcessDrag(pointer);
				} else
					RemovePointerData(pointer);
			}
			return Input.touchCount > 0;
		}

		private void ProcessTouchPress(PointerEventData pointerEvent, bool pressed, bool released) {
			var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

			// PointerDown notification
			if(pressed) {
				pointerEvent.eligibleForClick = true;
				pointerEvent.delta = Vector2.zero;
				pointerEvent.dragging = false;
				pointerEvent.useDragThreshold = true;
				pointerEvent.pressPosition = pointerEvent.position;
				pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

				DeselectIfSelectionChanged(currentOverGo, pointerEvent);

				if(pointerEvent.pointerEnter != currentOverGo) {
					// send a pointer enter to the touched element if it isn't the one to select...
					HandlePointerExitAndEnter(pointerEvent, currentOverGo);
					pointerEvent.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, pointerEvent, 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 == pointerEvent.lastPress) {
					var diffTime = time - pointerEvent.clickTime;
					if(diffTime < 0.3f)
						++pointerEvent.clickCount;
					else
						pointerEvent.clickCount = 1;

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

				pointerEvent.pointerPress = newPressed;
				pointerEvent.rawPointerPress = currentOverGo;

				pointerEvent.clickTime = time;

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

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

			// PointerUp notification
			if(released) {
				// Debug.Log("Executing pressup on: " + pointer.pointerPress);
				ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

				// Debug.Log("KeyCode: " + pointer.eventData.keyCode);

				// see if we mouse up on the same element that we clicked on...
				var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

				// PointerClick and Drop events
				if(pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick) {
					ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
				} else if(pointerEvent.pointerDrag != null && pointerEvent.dragging) {
					ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
				}

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

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

				pointerEvent.dragging = false;
				pointerEvent.pointerDrag = null;

				if(pointerEvent.pointerDrag != null)
					ExecuteEvents.Execute(pointerEvent.pointerDrag, pointerEvent, ExecuteEvents.endDragHandler);

				pointerEvent.pointerDrag = null;

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

		/// <summary>
		/// Process submit keys.
		/// </summary>
		protected bool SendSubmitEventToSelectedObject() {
			if(eventSystem.currentSelectedGameObject == null)
				return false;

			var state = GamePad.GetState(Player);
			var data = GetBaseEventData();
			if(state.Pressed(SubmitButton))
				ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.submitHandler);

			if(state.Pressed(CancelButton))
				ExecuteEvents.Execute(eventSystem.currentSelectedGameObject, data, ExecuteEvents.cancelHandler);
			return data.used;
		}

		private Vector2 GetMoveVector() {
			var x = GamePad.GetAxis(HorizontalAxis, Player);
			if(InvertHorizontal) x = -x;
			var y = GamePad.GetAxis(VerticalAxis, Player);
			if(InvertVertical) y = -y;
			return new Vector2(x, y);
		}

		private Vector2 GetRawMoveVector() {
			Vector2 move = GetMoveVector();

			if(HorizontalPressed) {
				if(move.x < 0)
					move.x = -1f;
				if(move.x > 0)
					move.x = 1f;
			}
			if(VerticalPressed) {
				if(move.y < 0)
					move.y = -1f;
				if(move.y > 0)
					move.y = 1f;
			}
			return move;
		}

		/// <summary>
		/// Process keyboard events.
		/// </summary>
		protected 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 = HorizontalPressed || VerticalPressed;
			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 + m_RepeatDelay);
				// If direction changed at least 90 degree, or we already had the delay, repeat at repeat rate.
				else
					allow = (time > m_PrevActionTime + 1f / m_InputActionsPerSecond);
			}
			if(!allow)
				return false;

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

			if(axisEventData.moveDir != MoveDirection.None) {
				ExecuteEvents.Execute(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;
		}

		protected void ProcessMouseEvent() {
			ProcessMouseEvent(0);
		}

		/// <summary>
		/// Process all mouse events.
		/// </summary>
		protected void ProcessMouseEvent(int id) {
			var mouseData = GetMousePointerEventData(id);
			var leftButtonData = mouseData.GetButtonState(PointerEventData.InputButton.Left).eventData;

			// Process the first mouse button fully
			ProcessMousePress(leftButtonData);
			ProcessMove(leftButtonData.buttonData);
			ProcessDrag(leftButtonData.buttonData);

			// Now process right / middle clicks
			ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData);
			ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Right).eventData.buttonData);
			ProcessMousePress(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData);
			ProcessDrag(mouseData.GetButtonState(PointerEventData.InputButton.Middle).eventData.buttonData);

			if(!Mathf.Approximately(leftButtonData.buttonData.scrollDelta.sqrMagnitude, 0.0f)) {
				var scrollHandler = ExecuteEvents.GetEventHandler<IScrollHandler>(leftButtonData.buttonData.pointerCurrentRaycast.gameObject);
				ExecuteEvents.ExecuteHierarchy(scrollHandler, leftButtonData.buttonData, ExecuteEvents.scrollHandler);
			}
		}

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

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

		/// <summary>
		/// Process the current mouse press.
		/// </summary>
		protected void ProcessMousePress(MouseButtonEventData data) {
			var pointerEvent = data.buttonData;
			var currentOverGo = pointerEvent.pointerCurrentRaycast.gameObject;

			// PointerDown notification
			if(data.PressedThisFrame()) {
				pointerEvent.eligibleForClick = true;
				pointerEvent.delta = Vector2.zero;
				pointerEvent.dragging = false;
				pointerEvent.useDragThreshold = true;
				pointerEvent.pressPosition = pointerEvent.position;
				pointerEvent.pointerPressRaycast = pointerEvent.pointerCurrentRaycast;

				DeselectIfSelectionChanged(currentOverGo, pointerEvent);

				// 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, pointerEvent, 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 == pointerEvent.lastPress) {
					var diffTime = time - pointerEvent.clickTime;
					if(diffTime < 0.3f)
						++pointerEvent.clickCount;
					else
						pointerEvent.clickCount = 1;

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

				pointerEvent.pointerPress = newPressed;
				pointerEvent.rawPointerPress = currentOverGo;

				pointerEvent.clickTime = time;

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

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

			// PointerUp notification
			if(data.ReleasedThisFrame()) {
				// Debug.Log("Executing pressup on: " + pointer.pointerPress);
				ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerUpHandler);

				// Debug.Log("KeyCode: " + pointer.eventData.keyCode);

				// see if we mouse up on the same element that we clicked on...
				var pointerUpHandler = ExecuteEvents.GetEventHandler<IPointerClickHandler>(currentOverGo);

				// PointerClick and Drop events
				if(pointerEvent.pointerPress == pointerUpHandler && pointerEvent.eligibleForClick) {
					ExecuteEvents.Execute(pointerEvent.pointerPress, pointerEvent, ExecuteEvents.pointerClickHandler);
				} else if(pointerEvent.pointerDrag != null && pointerEvent.dragging) {
					ExecuteEvents.ExecuteHierarchy(currentOverGo, pointerEvent, ExecuteEvents.dropHandler);
				}

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

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

				pointerEvent.dragging = false;
				pointerEvent.pointerDrag = 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 != pointerEvent.pointerEnter) {
					HandlePointerExitAndEnter(pointerEvent, null);
					HandlePointerExitAndEnter(pointerEvent, currentOverGo);
				}
			}
		}
	}
}