// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
using Unity.Profiling;
using UnityEngine;
using UInput = UnityEngine.Input;

namespace Microsoft.MixedReality.Toolkit.Input.UnityInput
{
    /// <summary>
    /// Manages joysticks using unity input system.
    /// </summary>
    [MixedRealityDataProvider(
        typeof(IMixedRealityInputSystem),
        (SupportedPlatforms)(-1),  // All platforms supported by Unity
        "Unity Joystick Manager")]
    public class UnityJoystickManager : BaseInputDeviceManager
    {
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="registrar">The <see cref="IMixedRealityServiceRegistrar"/> instance that loaded the data provider.</param>
        /// <param name="inputSystem">The <see cref="Microsoft.MixedReality.Toolkit.Input.IMixedRealityInputSystem"/> instance that receives data from this provider.</param>
        /// <param name="name">Friendly name of the service.</param>
        /// <param name="priority">Service priority. Used to determine order of instantiation.</param>
        /// <param name="profile">The service's configuration profile.</param>
        [Obsolete("This constructor is obsolete (registrar parameter is no longer required) and will be removed in a future version of the Microsoft Mixed Reality Toolkit.")]
        public UnityJoystickManager(
            IMixedRealityServiceRegistrar registrar,
            IMixedRealityInputSystem inputSystem,
            string name = null,
            uint priority = DefaultPriority,
            BaseMixedRealityProfile profile = null) : this(inputSystem, name, priority, profile)
        {
            Registrar = registrar;
        }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="inputSystem">The <see cref="Microsoft.MixedReality.Toolkit.Input.IMixedRealityInputSystem"/> instance that receives data from this provider.</param>
        /// <param name="name">Friendly name of the service.</param>
        /// <param name="priority">Service priority. Used to determine order of instantiation.</param>
        /// <param name="profile">The service's configuration profile.</param>
        public UnityJoystickManager(
            IMixedRealityInputSystem inputSystem,
            string name = null,
            uint priority = DefaultPriority,
            BaseMixedRealityProfile profile = null) : base(inputSystem, name, priority, profile) { }

        private const float DeviceRefreshInterval = 3.0f;

        protected readonly Dictionary<string, GenericJoystickController> ActiveControllers = new Dictionary<string, GenericJoystickController>();

        private float deviceRefreshTimer;
        private string[] lastDeviceList;

        private static readonly ProfilerMarker UpdatePerfMarker = new ProfilerMarker("[MRTK] UnityJoystickManager.Update");

        /// <inheritdoc />
        public override void Update()
        {
            using (UpdatePerfMarker.Auto())
            {
                base.Update();

                deviceRefreshTimer += Time.unscaledDeltaTime;

                if (deviceRefreshTimer >= DeviceRefreshInterval)
                {
                    deviceRefreshTimer = 0.0f;
                    RefreshDevices();
                }

                foreach (var controller in ActiveControllers)
                {
                    controller.Value?.UpdateController();
                }
            }
        }

        /// <inheritdoc />
        public override void Disable()
        {
            base.Disable();

            foreach (var genericJoystick in ActiveControllers)
            {
                if (genericJoystick.Value != null)
                {
                    Service?.RaiseSourceLost(genericJoystick.Value.InputSource, genericJoystick.Value);
                }
            }

            ActiveControllers.Clear();
        }

        private static readonly ProfilerMarker GetActiveControllersPerfMarker = new ProfilerMarker("[MRTK] UnityJoystickManager.GetActiveControllers");

        /// <inheritdoc/>
        public override IMixedRealityController[] GetActiveControllers()
        {
            using (GetActiveControllersPerfMarker.Auto())
            {
                IMixedRealityController[] controllers = ActiveControllers.Values.ToArray<IMixedRealityController>();
                return controllers;
            }
        }

        private static readonly ProfilerMarker RefreshDevicesPerfMarker = new ProfilerMarker("[MRTK] UnityJoystickManager.RefreshDevices");

        private void RefreshDevices()
        {
            using (RefreshDevicesPerfMarker.Auto())
            {
                var joystickNames = UInput.GetJoystickNames();

                if (joystickNames.Length <= 0)
                {
                    return;
                }

                if (lastDeviceList != null && joystickNames.Length == lastDeviceList.Length)
                {
                    for (int i = 0; i < lastDeviceList.Length; i++)
                    {
                        if (joystickNames[i].Equals(lastDeviceList[i])) { continue; }

                        if (ActiveControllers.ContainsKey(lastDeviceList[i]))
                        {
                            var controller = GetOrAddController(lastDeviceList[i]);

                            if (controller != null)
                            {
                                Service?.RaiseSourceLost(controller.InputSource, controller);
                            }

                            RemoveController(lastDeviceList[i]);
                        }
                    }
                }

                for (var i = 0; i < joystickNames.Length; i++)
                {
                    if (string.IsNullOrEmpty(joystickNames[i]))
                    {
                        continue;
                    }

                    if (!ActiveControllers.ContainsKey(joystickNames[i]))
                    {
                        var controller = GetOrAddController(joystickNames[i]);

                        if (controller != null)
                        {
                            Service?.RaiseSourceDetected(controller.InputSource, controller);
                        }
                    }
                }

                lastDeviceList = joystickNames;
            }
        }

        private static readonly ProfilerMarker GetOrAddControllerPerfMarker = new ProfilerMarker("[MRTK] UnityJoystickManager.GetOrAddController");

        /// <summary>
        /// Gets or adds a controller using the joystick name provided.
        /// </summary>
        /// <param name="joystickName">The name of the joystick from Unity's <see href="https://docs.unity3d.com/ScriptReference/Input.GetJoystickNames.html">Input.GetJoystickNames</see></param>
        /// <returns>A new controller reference.</returns>
        protected virtual GenericJoystickController GetOrAddController(string joystickName)
        {
            using (GetOrAddControllerPerfMarker.Auto())
            {
                if (ActiveControllers.ContainsKey(joystickName))
                {
                    var controller = ActiveControllers[joystickName];
                    Debug.Assert(controller != null);
                    return controller;
                }

                Type controllerType;

                switch (GetCurrentControllerType(joystickName))
                {
                    default:
                        return null;
                    case SupportedControllerType.GenericUnity:
                        controllerType = typeof(GenericJoystickController);
                        break;
                    case SupportedControllerType.Xbox:
                        controllerType = typeof(XboxController);
                        break;
                }

                IMixedRealityInputSource inputSource = Service?.RequestNewGenericInputSource($"{controllerType.Name} Controller", sourceType: InputSourceType.Controller);
                GenericJoystickController detectedController = Activator.CreateInstance(controllerType, TrackingState.NotTracked, Handedness.None, inputSource, null) as GenericJoystickController;

                if (detectedController == null || !detectedController.Enabled)
                {
                    // Controller failed to be setup correctly.
                    Debug.LogError($"Failed to create {controllerType.Name} controller");

                    // Return null so we don't raise the source detected.
                    return null;
                }

                ActiveControllers.Add(joystickName, detectedController);

                return detectedController;
            }
        }

        /// <summary>
        /// Removes a controller using the joystick name provided.
        /// </summary>
        /// <param name="joystickName">The name of the joystick from Unity's <see href="https://docs.unity3d.com/ScriptReference/Input.GetJoystickNames.html">Input.GetJoystickNames</see></param>
        protected virtual void RemoveController(string joystickName)
        {
            ActiveControllers.Remove(joystickName);
        }

        /// <summary>
        /// Gets the current controller type for the joystick name provided.
        /// </summary>
        /// <param name="joystickName">The name of the joystick from Unity's <see href="https://docs.unity3d.com/ScriptReference/Input.GetJoystickNames.html">Input.GetJoystickNames</see></param>
        /// <returns>The supported controller type</returns>
        protected virtual SupportedControllerType GetCurrentControllerType(string joystickName)
        {
            // todo: this should be using an allow list, not a disallow list
            if (string.IsNullOrEmpty(joystickName) ||
                joystickName.Contains("OpenVR") || // This catches input sources from legacy OpenVR
                joystickName.Contains("OpenXR") || // This catches input sources from OpenXR Plugin
                joystickName.Contains("Oculus") || // This catches controllers from Oculus XR Plugin
                joystickName.Contains("Hand - ") || // This catches HoloLens hands from Windows XR Plugin
                joystickName.Contains("Spatial")) // This catches controllers from Windows XR Plugin and all input sources from legacy WMR
            {
                return 0;
            }

            if (joystickName.ToLower().Contains("xbox"))
            {
                return SupportedControllerType.Xbox;
            }

            Debug.Log($"{joystickName} does not have a defined controller type, falling back to generic controller type");
            return SupportedControllerType.GenericUnity;
        }
    }
}
