using System.Collections.Generic;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.Controls;
namespace UnityEngine.XR.Content.Interaction
{
    /// 
    /// Allows for actions to 'lock' the input controls they use so that others actions using the same controls will not receive input at the same time.
    /// InputMediator works by injecting input processors on every binding in active action maps.
    /// Usage is via  and .
    /// 
    public static class InputMediator
    {
        /// 
        /// Substring of the names that all of the input processors that are injected have.
        /// 
        /// 
        const string k_ConsumeKey = "Consume";
        static bool s_Updating;
        // Data associated with each control, storing if an action has locked it,
        // and other actions that are allowed to make use of this control at the same time
        class ConsumptionState
        {
            public int m_LockedAction = -1;
            public int m_AllowedAction1 = -1;
            public int m_AllowedAction2 = -1;
            public bool m_Automatic;
        }
        /// 
        /// Generic Consumption processor - handles all the aspects of looking up actions that have locked a control
        /// Implementations merely need to implement the methods to determine if a control has returned to rest (and thus should reset)
        /// And the 'identity' value of a control, which is the value it should have when the control is locked
        /// 
        /// 
        abstract class ConsumeProcessor : InputProcessor where TValue : struct
        {
            public int m_ActionIndex = -1;
            public override TValue Process(TValue value, InputControl control)
            {
                // Check the dictionary for this control
                // If it does not exist, proceed unhindered
                if (control == null || !s_ConsumedControls.TryGetValue(control, out var currentState))
                    return value;
                // If there is no locked action, also proceed unhindered
                if (currentState.m_LockedAction == -1)
                    return value;
                // Check for an action match
                var actionMatched = (currentState.m_LockedAction == m_ActionIndex) || (currentState.m_AllowedAction1 == m_ActionIndex) || (currentState.m_AllowedAction2 == m_ActionIndex);
                // Check if we should automatically release
                if (actionMatched)
                {
                    if (currentState.m_Automatic && ValueNearZero(value))
                        currentState.m_LockedAction = -1;
                    return value;
                }
                return IdentityValue();
            }
            public abstract bool ValueNearZero(TValue value);
            public abstract TValue IdentityValue();
        }
        class ConsumeFloat : ConsumeProcessor
        {
            public override bool ValueNearZero(float value)
            {
                return value < float.Epsilon;
            }
            public override float IdentityValue()
            {
                return 0.0f;
            }
        }
        class ConsumeVector2 : ConsumeProcessor
        {
            public override bool ValueNearZero(Vector2 value)
            {
                return value.sqrMagnitude < float.Epsilon;
            }
            public override Vector2 IdentityValue()
            {
                return Vector2.zero;
            }
        }
        class ConsumeVector3 : ConsumeProcessor
        {
            public override bool ValueNearZero(Vector3 value)
            {
                return value.sqrMagnitude < float.Epsilon;
            }
            public override Vector3 IdentityValue()
            {
                return Vector3.zero;
            }
        }
        class ConsumeQuaternion : ConsumeProcessor
        {
            public override bool ValueNearZero(Quaternion value)
            {
                return Quaternion.Angle(value, Quaternion.identity) < float.Epsilon;
            }
            public override Quaternion IdentityValue()
            {
                return Quaternion.identity;
            }
        }
        static Dictionary s_ConsumedControls = new Dictionary();
        static Dictionary s_ActionIndices = new Dictionary();
        static HashSet s_InitializedActions = new HashSet();
        [RuntimeInitializeOnLoadMethod]
        static void Initialize()
        {
            InputSystem.InputSystem.RegisterProcessor(nameof(ConsumeFloat));
            InputSystem.InputSystem.RegisterProcessor(nameof(ConsumeVector2));
            InputSystem.InputSystem.RegisterProcessor(nameof(ConsumeVector3));
            InputSystem.InputSystem.RegisterProcessor(nameof(ConsumeQuaternion));
            Application.quitting += OnApplicationQuitting;
            InputSystem.InputSystem.onActionChange += OnActionChange;
            InitializeConsumeProcessors();
        }
        static void OnApplicationQuitting()
        {
            InputSystem.InputSystem.onActionChange -= OnActionChange;
        }
        /// 
        /// Attempts to 'lock' the controls belonging to an action - which means other actions using the same control will only get zero/identity values during this time
        /// 
        /// The action that should lock their controls
        /// If the control lock should release automatically when the controls go to a resting state
        /// If the action should forcefully take a lock from another consuming action
        /// An additional action that can access these controls at this time
        /// An additional action that can access these controls at this time
        /// False if _any_ of the associated controls were unable to be locked 
        public static bool ConsumeControl(InputAction source, bool automaticRelease, bool force = false, InputAction friendAction1 = null, InputAction friendAction2 = null)
        {
            if (source == null)
                return false;
            var actionIndex1 = GetActionIndex(source);
            var actionIndex2 = GetActionIndex(friendAction1);
            var actionIndex3 = GetActionIndex(friendAction2);
            var lockCount = 0;
            var sourceControls = source.controls;
            foreach (var currentControl in sourceControls)
            {
                // Check to see if it is in the list already
                // If not, make an entry for it
                if (!s_ConsumedControls.TryGetValue(currentControl, out var controlState))
                {
                    var parent = currentControl.parent;
                    if (currentControl is AxisControl && parent is Vector2Control)
                    {
                        if (!s_ConsumedControls.TryGetValue(parent, out controlState))
                        {
                            controlState = new ConsumptionState { m_Automatic = automaticRelease };
                            s_ConsumedControls.Add(parent, controlState);
                        }
                    }
                    else
                    {
                        controlState = new ConsumptionState { m_Automatic = automaticRelease };
                    }
                    s_ConsumedControls.Add(currentControl, controlState);
                }
                if (force || controlState.m_LockedAction == -1)
                {
                    controlState.m_LockedAction = actionIndex1;
                    controlState.m_AllowedAction1 = actionIndex2;
                    controlState.m_AllowedAction2 = actionIndex3;
                    lockCount++;
                }
            }
            return (lockCount == sourceControls.Count);
        }
        /// 
        /// Releases an action's lock over its associated controls. Other actions using the same controls will begin receiving input again
        /// 
        /// The action that is attempting to release its lock
        /// If this input lock should be released regardless of requesting action
        /// False if _any_ of the associated controls were unable to be released 
        public static bool ReleaseControl(InputAction source, bool force = false)
        {
            if (source == null)
                return false;
            var actionIndex = GetActionIndex(source);
            var lockCount = 0;
            var sourceControls = source.controls;
            foreach (var currentControl in sourceControls)
            {
                // Check to see if it is in the list already
                // If not, nothing to release
                if (!s_ConsumedControls.TryGetValue(currentControl, out var controlState))
                {
                    lockCount++;
                    continue;
                }
                if (force || controlState.m_LockedAction == actionIndex)
                {
                    controlState.m_LockedAction = -1;
                    lockCount++;
                }
            }
            return (lockCount == sourceControls.Count);
        }
        static void InitializeConsumeProcessors()
        {
            s_Updating = true;
            var actionList = InputSystem.InputSystem.ListEnabledActions();
            foreach (var action in actionList)
            {
                EnsureConsumeProcessorAdded(action);
                // Since this list only contains currently enabled actions,
                // any actions that are enabled later will need to
                // have the consume processor added. Since those actions may not
                // trigger a BoundControlsChanged change, the OnActionChange event handler
                // will check against this list and append to it as actions are enabled.
                // This set is checked against for performance reasons
                // to avoid the more costly EnsureConsumeProcessorAdded(InputAction) method.
                s_InitializedActions.Add(action);
            }
            s_Updating = false;
        }
        static void OnActionChange(object actionSource, InputActionChange change)
        {
            if (s_Updating)
                return;
            s_Updating = true;
            if (change == InputActionChange.ActionEnabled)
            {
                var action = (InputAction)actionSource;
                if (s_InitializedActions.Add(action))
                    EnsureConsumeProcessorAdded(action);
            }
            else if (change == InputActionChange.ActionMapEnabled)
            {
                var actionMap = (InputActionMap)actionSource;
                foreach (var action in actionMap.actions)
                {
                    if (s_InitializedActions.Add(action))
                        EnsureConsumeProcessorAdded(action);
                }
            }
            else if (change == InputActionChange.BoundControlsChanged)
            {
                // We skip pure actions here as they can get into an invalid state if bindings were changed
                if (actionSource is InputActionMap actionMap)
                {
                    EnsureConsumeProcessorAdded(actionMap);
                }
                else if (actionSource is InputActionAsset actionAsset)
                {
                    EnsureConsumeProcessorAdded(actionAsset);
                }
            }
            s_Updating = false;
        }
        static string ControlTypeToConsumeType(string controlType)
        {
            switch (controlType)
            {
                case "Single":
                case "Button":
                case "float":
                    return nameof(ConsumeFloat);
                case "Vector2":
                    return nameof(ConsumeVector2);
                case "Vector3":
                    return nameof(ConsumeVector3);
                case "Quaternion":
                    return nameof(ConsumeQuaternion);
            }
            return "";
        }
        static string ProcessBindingControl(string bindingPath)
        {
            var control = InputSystem.InputSystem.FindControl(bindingPath);
            var consumeType = "";
            if (control != null)
                consumeType = ControlTypeToConsumeType(control.valueType.Name);
            else
            {
                // Try to fall back based on path keywords
                var bindingLower = bindingPath.ToLower();
                if (bindingLower.EndsWith("position"))
                    consumeType = ControlTypeToConsumeType("Vector3");
                if (bindingLower.EndsWith("rotation"))
                    consumeType = ControlTypeToConsumeType("Quaternion");
                if (bindingLower.EndsWith("x"))
                    consumeType = ControlTypeToConsumeType("float");
                if (bindingLower.EndsWith("y"))
                    consumeType = ControlTypeToConsumeType("float");
                if (bindingLower.EndsWith("axis"))
                    consumeType = ControlTypeToConsumeType("Vector2");
            }
            if (string.IsNullOrEmpty(consumeType))
                return "";
            return consumeType;
        }
        static void EnsureConsumeProcessorAdded(InputAction action)
        {
            var bindingCount = action.bindings.Count;
            for (var i = 0; i < bindingCount; i++)
            {
                var currentBinding = action.bindings[i];
                // Ignore composites, but not parts of composites
                if (currentBinding.isComposite)
                    continue;
                // Ignore bindings that aren't ready yet
                if (currentBinding.effectiveProcessors == null)
                    continue;
                var actionIndex = GetActionIndex(action);
                if (!currentBinding.effectiveProcessors.Contains(k_ConsumeKey))
                {
                    // Ignore unused bindings
                    if (string.IsNullOrEmpty(currentBinding.path))
                        continue;
                    // Get the binding's control type and cache it in the control lookup
                    var bindingType = ProcessBindingControl(currentBinding.path);
                    // If the composite can't figure out its type, then skip it
                    if (string.IsNullOrEmpty(bindingType))
                    {
                        //Debug.LogWarning($"Could not add consume processor for binding { currentBinding.path }, in {action.name}");
                        continue;
                    }
                    if (currentBinding.processors.Length > 0)
                        action.ApplyBindingOverride(i, new InputBinding { overrideProcessors = $"{bindingType}(m_ActionIndex={actionIndex}), {currentBinding.processors}" });
                    else
                        action.ApplyBindingOverride(i, new InputBinding { overrideProcessors = $"{bindingType}(m_ActionIndex={actionIndex})" });
                }
            }
        }
        static void EnsureConsumeProcessorAdded(InputActionMap actionMap)
        {
            foreach (var action in actionMap.actions)
            {
                EnsureConsumeProcessorAdded(action);
            }
        }
        static void EnsureConsumeProcessorAdded(InputActionAsset actionAsset)
        {
            foreach (var map in actionAsset.actionMaps)
            {
                EnsureConsumeProcessorAdded(map);
            }
        }
        static int GetActionIndex(InputAction source)
        {
            if (source == null)
                return -1;
            if (!s_ActionIndices.TryGetValue(source, out var actionIndex))
            {
                actionIndex = s_ActionIndices.Count;
                s_ActionIndices.Add(source, actionIndex);
            }
            return actionIndex;
        }
    }
}