using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR;
using UnityEngine.UI;

namespace DesktopVision {
/// <summary>
/// This class is used to pass XR Controller inputs, especially when interacting with a <see cref="ComputerObject"/>.
/// </summary>
public class ControllerEventArgs : EventArgs
{
	/// <summary>
	/// The coordinates of th event
	/// </summary>
	public Vector2 Coordinates { get; set; }

	/// <summary>
	/// The controller action
	/// </summary>
	public string EventName { get; set; }

	/// <summary>
	/// The controller transform which triggered the event
	/// </summary>
	public Transform controller { get; set; }

	/// <summary>
	/// If the controller is hovering over the computer
	/// </summary>
	public bool hoveringDesktop { get; set; }
}
public class DVXRController
{
	private InputDevice device;
	private Vector2 hoverPosition;
	private LayerMask objectLayer;
	private RaycastHit hit, emptyHit;
	private Transform controller;
	private string handedness;
	private bool selecting, gripping, primaryButtonDown, menuDown;
	private GameObject screen;
	private Boolean delayMove, bufferHover, bufferControls;

    /// <summary>
    /// The Event Dispatcher used to send out controller activity
    /// </summary>
	public event EventHandler<ControllerEventArgs> Dispatch;

	/// <summary>
	/// The DVXRController class is used to handle input from a XR controller
	/// </summary>
	/// <param name="hand">Handedness "left/right"</param>
	/// <param name="layer">Layer to check for inputs</param>
	/// <param name="xrController">The XR Controller Transform</param>
	/// <param name="videoScreen">The screen object to check for interactions</param>
	public DVXRController(string hand, LayerMask layer, Transform xrController, GameObject videoScreen)
	{
		handedness = hand;
		objectLayer = layer;
		controller = xrController;
		screen = videoScreen;
	}

	protected virtual void DispatchEvent(ControllerEventArgs e)
	{
		EventHandler<ControllerEventArgs> handler = Dispatch;
		handler?.Invoke(this, e);
	}

	public void Update()
	{
		if (device.isValid)
		{
			hit = emptyHit;
			handleRaycast();
			handleControllerInputs();
		}
		else
		{
			searchDevice();
		};
	}

	private void searchDevice()
	{
		var controllers = new List<UnityEngine.XR.InputDevice>();
		var leftChars = UnityEngine.XR.InputDeviceCharacteristics.Controller | UnityEngine.XR.InputDeviceCharacteristics.Left;
		var rightChars = UnityEngine.XR.InputDeviceCharacteristics.Controller | UnityEngine.XR.InputDeviceCharacteristics.Right;

		if (handedness == "right") UnityEngine.XR.InputDevices.GetDevicesWithCharacteristics(rightChars, controllers);
		else UnityEngine.XR.InputDevices.GetDevicesWithCharacteristics(leftChars, controllers);
		foreach (var controller in controllers) if (controller.isValid) device = controller;
	}

	private void handleControllerInputs()
	{
		var inputFeatures = new List<UnityEngine.XR.InputFeatureUsage>();
		if (device.TryGetFeatureUsages(inputFeatures))
		{
			foreach (var feature in inputFeatures)
			{
				if (feature.type == typeof(bool))
				{
					bool featureValue;
					device.TryGetFeatureValue(feature.As<bool>(), out featureValue);
					handleControllerButtons(feature.name, featureValue, handedness);
				}
				else
				{
					Vector2 movementVector;
					if (device.TryGetFeatureValue(CommonUsages.primary2DAxis, out movementVector))
					{
						if (movementVector.y != 0 || movementVector.x != 0)
						{
							movementVector.y = movementVector.y + 1;
							handleControllerAxis(movementVector);
						}
					}
				}
			}
		}
	}
	private void handleRaycast()
	{
		if (bufferHover)
		{
			return;
		}
		//2ms buffer raycast to protect datachannel 
		bufferHover = true;
		Action<bool> callBack = (_) => { bufferHover = false; };
		BufferCallback(5, callBack);

		Vector3 origin = controller.transform.position;
		Vector3 direction = controller.transform.forward;
		string controllerName = controller.name.ToLower();
		string handedness = controllerName.Contains("right") ? "right" : "left";

		hoverPosition = new Vector2(-1, -1);
		if (Physics.Raycast(origin, direction, out hit, 100, objectLayer))
		{
			GameObject videoScreen = hit.collider.transform.gameObject;
			int id = videoScreen.GetInstanceID();
			int screenId = screen.GetInstanceID();
			if (id == screenId)
			{
				hoverPosition = hit.textureCoord;
				handleMouseMove(hoverPosition);
			}
		}

	}

	private void handleMouseMove(Vector2 coordinate)
	{
		hoveringDesktop(out bool dispatch);
		if (!dispatch) return;

		if (!delayMove)
			dispatchEvent(coordinate, "dragMove");
	}

	private void handleControllerButtons(string name, bool value, string handedness)
	{
		if (name == "TriggerButton" && value && !selecting)
		{
			selecting = true;
			DelayMove();
			dispatchEvent(hoverPosition, "dragStart");
			return;
		}
		else if (name == "TriggerButton" && !value && selecting)
		{
			selecting = false;
			dispatchEvent(hoverPosition, "dragEnd");
			return;
		}

		if (name == "PrimaryButton" && value && !primaryButtonDown)
		{
			primaryButtonDown = true;
			DelayMove();
			dispatchEvent(hoverPosition, "rightclick");
			return;
		}
		else if (name == "PrimaryButton" && !value && primaryButtonDown)
		{
			primaryButtonDown = false;
			return;
		}
	}

	public void BufferCallback(int delay, Action<bool> callback)
	{
		var task = Task.Delay(delay).ContinueWith((task) =>
		{
			callback(true);
		});
	}

	public void DelayMove()
	{
		delayMove = true;
		var task = Task.Delay(250).ContinueWith((task) =>
		{
			delayMove = false;
		});
	}
	void handleControllerAxis(Vector2 axis)
	{
		hoveringDesktop(out bool dispatch);
		if (!dispatch) return;
		dispatchEvent(axis, "scroll");
	}

	private void hoveringDesktop(out bool hovering)
	{
		hovering = true;
		if (hoverPosition.x < 0 || hoverPosition.x == 0) hovering = false;
		if (hoverPosition.y < 0 || hoverPosition.y == 0) hovering = false;
	}

	private void dispatchEvent(Vector2 coordinate, string eventName)
	{
		ControllerEventArgs args = new ControllerEventArgs();
		args.Coordinates = coordinate;
		args.controller = controller;
		args.EventName = eventName;
		hoveringDesktop(out bool dispatch);
		args.hoveringDesktop = dispatch;
		DispatchEvent(args);
	}
}
}