using System;
using UnityEngine.Events;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.Users;
using UnityEngine.InputSystem.Utilities;
#if UNITY_EDITOR
using UnityEditor;
#endif

////REVIEW: should we automatically pool/retain up to maxPlayerCount player instances?

////REVIEW: the join/leave messages should probably give a *GameObject* rather than the PlayerInput component (which can be gotten to via a simple GetComponent(InChildren) call)

////TODO: add support for reacting to players missing devices

namespace UnityEngine.InputSystem
{
    /// <summary>
    /// Manages joining and leaving of players.
    /// </summary>
    /// <remarks>
    /// This is a singleton component. Only one instance is meant to be active in a game
    /// at any one time. To retrieve the current instance, use <see cref="instance"/>.
    ///
    /// Note that a PlayerInputManager is not strictly required to have multiple <see cref="PlayerInput"/> components.
    /// What PlayerInputManager provides is the implementation of specific player join mechanisms
    /// (<see cref="joinBehavior"/>) as well as automatic assignment of split-screen areas (<see cref="splitScreen"/>).
    /// However, you can always implement your own custom logic instead and simply instantiate multiple GameObjects with
    /// <see cref="PlayerInput"/> yourself.
    /// </remarks>
    [AddComponentMenu("Input/Player Input Manager")]
    [HelpURL(InputSystem.kDocUrl + "/manual/PlayerInputManager.html")]
    public class PlayerInputManager : MonoBehaviour
    {
        /// <summary>
        /// Name of the message that is sent when a player joins the game.
        /// </summary>
        public const string PlayerJoinedMessage = "OnPlayerJoined";

        public const string PlayerLeftMessage = "OnPlayerLeft";

        /// <summary>
        /// If enabled, each player will automatically be assigned a portion of the available screen area.
        /// </summary>
        /// <remarks>
        /// For this to work, each <see cref="PlayerInput"/> component must have an associated <see cref="Camera"/>
        /// object through <see cref="PlayerInput.camera"/>.
        ///
        /// Note that as player join, the screen may be increasingly subdivided and players may see their
        /// previous screen area getting resized.
        /// </remarks>
        public bool splitScreen
        {
            get => m_SplitScreen;
            set
            {
                if (m_SplitScreen == value)
                    return;

                m_SplitScreen = value;

                if (!m_SplitScreen)
                {
                    // Reset rects on all player cameras.
                    foreach (var player in PlayerInput.all)
                    {
                        var camera = player.camera;
                        if (camera != null)
                            camera.rect = new Rect(0, 0, 1, 1);
                    }
                }
                else
                {
                    UpdateSplitScreen();
                }
            }
        }

        ////REVIEW: we probably need support for filling unused screen areas automatically
        /// <summary>
        /// If <see cref="splitScreen"/> is enabled, this property determines whether subdividing the screen is allowed to
        /// produce screen areas that have an aspect ratio different from the screen resolution.
        /// </summary>
        /// <remarks>
        /// By default, when <see cref="splitScreen"/> is enabled, the manager will add or remove screen subdivisions in
        /// steps of two. This means that when, for example, the second player is added, the screen will be subdivided into
        /// a left and a right screen area; the left one allocated to the first player and the right one allocated to the
        /// second player.
        ///
        /// This behavior makes optimal use of screen real estate but will result in screen areas that have aspect ratios
        /// different from the screen resolution. If this is not acceptable, this property can be set to true to enforce
        /// split-screen to only create screen areas that have the same aspect ratio of the screen.
        ///
        /// This results in the screen being subdivided more aggressively. When, for example, a second player is added,
        /// the screen will immediately be divided into a four-way split-screen setup with the lower two screen areas
        /// not being used.
        ///
        /// This property is irrelevant if <see cref="fixedNumberOfSplitScreens"/> is used.
        /// </remarks>
        public bool maintainAspectRatioInSplitScreen => m_MaintainAspectRatioInSplitScreen;

        /// <summary>
        /// If <see cref="splitScreen"/> is enabled, this property determines how many screen divisions there will be.
        /// </summary>
        /// <remarks>
        /// This is only used if <see cref="splitScreen"/> is true.
        ///
        /// By default this is set to -1 which means the screen will automatically be divided to best fit the
        /// current number of players i.e. the highest player index in <see cref="PlayerInput"/>
        /// </remarks>
        public int fixedNumberOfSplitScreens => m_FixedNumberOfSplitScreens;

        /// <summary>
        /// The normalized screen rectangle available for allocating player split-screens into.
        /// </summary>
        /// <remarks>
        /// This is only used if <see cref="splitScreen"/> is true.
        ///
        /// By default it is set to <c>(0,0,1,1)</c>, i.e. the entire screen area will be used for player screens.
        /// If, for example, part of the screen should display a UI/information shared by all players, this
        /// property can be used to cut off the area and not have it used by PlayerInputManager.
        /// </remarks>
        public Rect splitScreenArea => m_SplitScreenRect;

        /// <summary>
        /// The current number of active players.
        /// </summary>
        /// <remarks>
        /// This count corresponds to all <see cref="PlayerInput"/> instances that are currently enabled.
        /// </remarks>
        public int playerCount => PlayerInput.s_AllActivePlayersCount;

        ////FIXME: this needs to be settable
        /// <summary>
        /// Maximum number of players allowed concurrently in the game.
        /// </summary>
        /// <remarks>
        /// If this limit is reached, joining is turned off automatically.
        ///
        /// By default this is set to -1. Any negative value deactivates the player limit and allows
        /// arbitrary many players to join.
        /// </remarks>
        public int maxPlayerCount => m_MaxPlayerCount;

        /// <summary>
        /// Whether new players can currently join.
        /// </summary>
        /// <remarks>
        /// While this is true, new players can join via the mechanism determined by <see cref="joinBehavior"/>.
        /// </remarks>
        /// <seealso cref="EnableJoining"/>
        /// <seealso cref="DisableJoining"/>
        public bool joiningEnabled => m_AllowJoining;

        /// <summary>
        /// Determines the mechanism by which players can join when joining is enabled (<see cref="joiningEnabled"/>).
        /// </summary>
        /// <remarks>
        /// </remarks>
        public PlayerJoinBehavior joinBehavior
        {
            get => m_JoinBehavior;
            set
            {
                if (m_JoinBehavior == value)
                    return;

                var joiningEnabled = m_AllowJoining;
                if (joiningEnabled)
                    DisableJoining();
                m_JoinBehavior = value;
                if (joiningEnabled)
                    EnableJoining();
            }
        }

        /// <summary>
        /// The input action that a player must trigger to join the game.
        /// </summary>
        /// <remarks>
        /// If the join action is a reference to an existing input action, it will be cloned when the PlayerInputManager
        /// is enabled. This avoids the situation where the join action can become disabled after the first user joins which
        /// can happen when the join action is the same as a player in-game action. When a player joins, input bindings from
        /// devices other than the device they joined with are disabled. If the join action had a binding for keyboard and one
        /// for gamepad for example, and the first player joined using the keyboard, the expectation is that the next player
        /// could still join by pressing the gamepad join button. Without the cloning behavior, the gamepad input would have
        /// been disabled.
        ///
        /// For more details about joining behavior, see <see cref="PlayerInput"/>.
        /// </remarks>
        public InputActionProperty joinAction
        {
            get => m_JoinAction;
            set
            {
                if (m_JoinAction == value)
                    return;

                ////REVIEW: should we suppress notifications for temporary disables?

                var joinEnabled = m_AllowJoining && m_JoinBehavior == PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered;
                if (joinEnabled)
                    DisableJoining();

                m_JoinAction = value;

                if (joinEnabled)
                    EnableJoining();
            }
        }

        public PlayerNotifications notificationBehavior
        {
            get => m_NotificationBehavior;
            set => m_NotificationBehavior = value;
        }

        public PlayerJoinedEvent playerJoinedEvent
        {
            get
            {
                if (m_PlayerJoinedEvent == null)
                    m_PlayerJoinedEvent = new PlayerJoinedEvent();
                return m_PlayerJoinedEvent;
            }
        }

        public PlayerLeftEvent playerLeftEvent
        {
            get
            {
                if (m_PlayerLeftEvent == null)
                    m_PlayerLeftEvent = new PlayerLeftEvent();
                return m_PlayerLeftEvent;
            }
        }

        public event Action<PlayerInput> onPlayerJoined
        {
            add
            {
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                m_PlayerJoinedCallbacks.AddCallback(value);
            }
            remove
            {
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                m_PlayerJoinedCallbacks.RemoveCallback(value);
            }
        }

        public event Action<PlayerInput> onPlayerLeft
        {
            add
            {
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                m_PlayerLeftCallbacks.AddCallback(value);
            }
            remove
            {
                if (value == null)
                    throw new ArgumentNullException(nameof(value));
                m_PlayerLeftCallbacks.RemoveCallback(value);
            }
        }

        /// <summary>
        /// Reference to the prefab that the manager will instantiate when players join.
        /// </summary>
        /// <value>Prefab to instantiate for new players.</value>
        public GameObject playerPrefab
        {
            get => m_PlayerPrefab;
            set => m_PlayerPrefab = value;
        }

        /// <summary>
        /// Singleton instance of the manager.
        /// </summary>
        /// <value>Singleton instance or null.</value>
        public static PlayerInputManager instance { get; private set; }

        /// <summary>
        /// Allow players to join the game based on <see cref="joinBehavior"/>.
        /// </summary>
        /// <seealso cref="DisableJoining"/>
        /// <seealso cref="joiningEnabled"/>
        public void EnableJoining()
        {
            switch (m_JoinBehavior)
            {
                case PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed:
                    ValidateInputActionAsset();

                    if (!m_UnpairedDeviceUsedDelegateHooked)
                    {
                        if (m_UnpairedDeviceUsedDelegate == null)
                            m_UnpairedDeviceUsedDelegate = OnUnpairedDeviceUsed;
                        InputUser.onUnpairedDeviceUsed += m_UnpairedDeviceUsedDelegate;
                        m_UnpairedDeviceUsedDelegateHooked = true;
                        ++InputUser.listenForUnpairedDeviceActivity;
                    }
                    break;

                case PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered:
                    // Hook into join action if we have one.
                    if (m_JoinAction.action != null)
                    {
                        if (!m_JoinActionDelegateHooked)
                        {
                            if (m_JoinActionDelegate == null)
                                m_JoinActionDelegate = JoinPlayerFromActionIfNotAlreadyJoined;
                            m_JoinAction.action.performed += m_JoinActionDelegate;
                            m_JoinActionDelegateHooked = true;
                        }
                        m_JoinAction.action.Enable();
                    }
                    else
                    {
                        Debug.LogError(
                            $"No join action configured on PlayerInputManager but join behavior is set to {nameof(PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered)}",
                            this);
                    }
                    break;
            }

            m_AllowJoining = true;
        }

        /// <summary>
        /// Inhibit players from joining the game.
        /// </summary>
        /// <seealso cref="EnableJoining"/>
        /// <seealso cref="joiningEnabled"/>
        public void DisableJoining()
        {
            switch (m_JoinBehavior)
            {
                case PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed:
                    if (m_UnpairedDeviceUsedDelegateHooked)
                    {
                        InputUser.onUnpairedDeviceUsed -= m_UnpairedDeviceUsedDelegate;
                        m_UnpairedDeviceUsedDelegateHooked = false;
                        --InputUser.listenForUnpairedDeviceActivity;
                    }
                    break;

                case PlayerJoinBehavior.JoinPlayersWhenJoinActionIsTriggered:
                    if (m_JoinActionDelegateHooked)
                    {
                        var joinAction = m_JoinAction.action;
                        if (joinAction != null)
                            m_JoinAction.action.performed -= m_JoinActionDelegate;
                        m_JoinActionDelegateHooked = false;
                    }
                    m_JoinAction.action?.Disable();
                    break;
            }

            m_AllowJoining = false;
        }

        ////TODO
        /// <summary>
        /// Join a new player based on input on a UI element.
        /// </summary>
        /// <remarks>
        /// This should be called directly from a UI callback such as <see cref="Button.onClick"/>. The device
        /// that the player joins with is taken from the device that was used to interact with the UI element.
        /// </remarks>
        internal void JoinPlayerFromUI()
        {
            if (!CheckIfPlayerCanJoin())
                return;

            //find used device; InputSystemUIInputModule should probably make that available

            throw new NotImplementedException();
        }

        /// <summary>
        /// Join a new player based on input received through an <see cref="InputAction"/>.
        /// </summary>
        /// <param name="context"></param>
        /// <remarks>
        /// </remarks>
        public void JoinPlayerFromAction(InputAction.CallbackContext context)
        {
            if (!CheckIfPlayerCanJoin())
                return;

            var device = context.control.device;
            JoinPlayer(pairWithDevice: device);
        }

        public void JoinPlayerFromActionIfNotAlreadyJoined(InputAction.CallbackContext context)
        {
            if (!CheckIfPlayerCanJoin())
                return;

            var device = context.control.device;
            if (PlayerInput.FindFirstPairedToDevice(device) != null)
                return;

            JoinPlayer(pairWithDevice: device);
        }

        /// <summary>
        /// Spawn a new player from <see cref="playerPrefab"/>.
        /// </summary>
        /// <param name="playerIndex">Optional explicit <see cref="PlayerInput.playerIndex"/> to assign to the player. Must be unique within
        /// <see cref="PlayerInput.all"/>. If not supplied, a player index will be assigned automatically (smallest unused index will be used).</param>
        /// <param name="splitScreenIndex">Optional <see cref="PlayerInput.splitScreenIndex"/>. If supplied, this assigns a split-screen area to the player. For example,
        /// a split-screen index of </param>
        /// <param name="controlScheme">Control scheme to activate on the player (optional). If not supplied, a control scheme will
        /// be selected based on <paramref name="pairWithDevice"/>. If no device is given either, the first control scheme that matches
        /// the currently available unpaired devices (see <see cref="InputUser.GetUnpairedInputDevices()"/>) is used.</param>
        /// <param name="pairWithDevice">Device to pair to the player. Also determines which control scheme to use if <paramref name="controlScheme"/>
        /// is not given.</param>
        /// <returns>The newly instantiated player or <c>null</c> if joining failed.</returns>
        /// <remarks>
        /// Joining must be enabled (see <see cref="joiningEnabled"/>) or the method will fail.
        ///
        /// To pair multiple devices, use <see cref="JoinPlayer(int,int,string,InputDevice[])"/>.
        /// </remarks>
        public PlayerInput JoinPlayer(int playerIndex = -1, int splitScreenIndex = -1, string controlScheme = null, InputDevice pairWithDevice = null)
        {
            if (!CheckIfPlayerCanJoin(playerIndex))
                return null;

            PlayerInput.s_DestroyIfDeviceSetupUnsuccessful = true;
            return PlayerInput.Instantiate(m_PlayerPrefab, playerIndex: playerIndex, splitScreenIndex: splitScreenIndex,
                controlScheme: controlScheme, pairWithDevice: pairWithDevice);
        }

        /// <summary>
        /// Spawn a new player from <see cref="playerPrefab"/>.
        /// </summary>
        /// <param name="playerIndex">Optional explicit <see cref="PlayerInput.playerIndex"/> to assign to the player. Must be unique within
        /// <see cref="PlayerInput.all"/>. If not supplied, a player index will be assigned automatically (smallest unused index will be used).</param>
        /// <param name="splitScreenIndex">Optional <see cref="PlayerInput.splitScreenIndex"/>. If supplied, this assigns a split-screen area to the player. For example,
        /// a split-screen index of </param>
        /// <param name="controlScheme">Control scheme to activate on the player (optional). If not supplied, a control scheme will
        /// be selected based on <paramref name="pairWithDevices"/>. If no device is given either, the first control scheme that matches
        /// the currently available unpaired devices (see <see cref="InputUser.GetUnpairedInputDevices()"/>) is used.</param>
        /// <param name="pairWithDevices">Devices to pair to the player. Also determines which control scheme to use if <paramref name="controlScheme"/>
        /// is not given.</param>
        /// <returns>The newly instantiated player or <c>null</c> if joining failed.</returns>
        /// <remarks>
        /// Joining must be enabled (see <see cref="joiningEnabled"/>) or the method will fail.
        /// </remarks>
        public PlayerInput JoinPlayer(int playerIndex = -1, int splitScreenIndex = -1, string controlScheme = null, params InputDevice[] pairWithDevices)
        {
            if (!CheckIfPlayerCanJoin(playerIndex))
                return null;

            PlayerInput.s_DestroyIfDeviceSetupUnsuccessful = true;
            return PlayerInput.Instantiate(m_PlayerPrefab, playerIndex: playerIndex, splitScreenIndex: splitScreenIndex,
                controlScheme: controlScheme, pairWithDevices: pairWithDevices);
        }

        [SerializeField] internal PlayerNotifications m_NotificationBehavior;
        [Tooltip("Set a limit for the maximum number of players who are able to join.")]
        [SerializeField] internal int m_MaxPlayerCount = -1;
        [SerializeField] internal bool m_AllowJoining = true;
        [SerializeField] internal PlayerJoinBehavior m_JoinBehavior;
        [SerializeField] internal PlayerJoinedEvent m_PlayerJoinedEvent;
        [SerializeField] internal PlayerLeftEvent m_PlayerLeftEvent;
        [SerializeField] internal InputActionProperty m_JoinAction;
        [SerializeField] internal GameObject m_PlayerPrefab;
        [SerializeField] internal bool m_SplitScreen;
        [SerializeField] internal bool m_MaintainAspectRatioInSplitScreen;
        [Tooltip("Explicitly set a fixed number of screens or otherwise allow the screen to be divided automatically to best fit the number of players.")]
        [SerializeField] internal int m_FixedNumberOfSplitScreens = -1;
        [SerializeField] internal Rect m_SplitScreenRect = new Rect(0, 0, 1, 1);

        [NonSerialized] private bool m_JoinActionDelegateHooked;
        [NonSerialized] private bool m_UnpairedDeviceUsedDelegateHooked;
        [NonSerialized] private Action<InputAction.CallbackContext> m_JoinActionDelegate;
        [NonSerialized] private Action<InputControl, InputEventPtr> m_UnpairedDeviceUsedDelegate;
        [NonSerialized] private CallbackArray<Action<PlayerInput>> m_PlayerJoinedCallbacks;
        [NonSerialized] private CallbackArray<Action<PlayerInput>> m_PlayerLeftCallbacks;

        internal static string[] messages => new[]
        {
            PlayerJoinedMessage,
            PlayerLeftMessage,
        };

        private bool CheckIfPlayerCanJoin(int playerIndex = -1)
        {
            if (m_PlayerPrefab == null)
            {
                Debug.LogError("playerPrefab must be set in order to be able to join new players", this);
                return false;
            }

            if (m_MaxPlayerCount >= 0 && playerCount >= m_MaxPlayerCount)
            {
                Debug.LogError("Have reached maximum player count of " + maxPlayerCount, this);
                return false;
            }

            // If we have a player index, make sure it's unique.
            if (playerIndex != -1)
            {
                for (var i = 0; i < PlayerInput.s_AllActivePlayersCount; ++i)
                    if (PlayerInput.s_AllActivePlayers[i].playerIndex == playerIndex)
                    {
                        Debug.LogError(
                            $"Player index #{playerIndex} is already taken by player {PlayerInput.s_AllActivePlayers[i]}",
                            PlayerInput.s_AllActivePlayers[i]);
                        return false;
                    }
            }

            return true;
        }

        private void OnUnpairedDeviceUsed(InputControl control, InputEventPtr eventPtr)
        {
            if (!m_AllowJoining)
                return;

            if (m_JoinBehavior == PlayerJoinBehavior.JoinPlayersWhenButtonIsPressed)
            {
                // Make sure it's a button that was actuated.
                if (!(control is ButtonControl))
                    return;

                // Make sure it's a device that is usable by the player's actions. We don't want
                // to join a player who's then stranded and has no way to actually interact with the game.
                if (!IsDeviceUsableWithPlayerActions(control.device))
                    return;

                ////REVIEW: should we log a warning or error when the actions for the player do not have control schemes?

                JoinPlayer(pairWithDevice: control.device);
            }
        }

        private void OnEnable()
        {
            if (instance == null)
            {
                instance = this;
            }
            else
            {
                Debug.LogWarning("Multiple PlayerInputManagers in the game. There should only be one PlayerInputManager", this);
                return;
            }

            // if the join action is a reference, clone it so we don't run into problems with the action being disabled by
            // PlayerInput when devices are assigned to individual players
            if (joinAction.reference != null && joinAction.action?.actionMap?.asset != null)
            {
                var inputActionAsset = Instantiate(joinAction.action.actionMap.asset);
                var inputActionReference = InputActionReference.Create(inputActionAsset.FindAction(joinAction.action.name));
                joinAction = new InputActionProperty(inputActionReference);
            }

            // Join all players already in the game.
            for (var i = 0; i < PlayerInput.s_AllActivePlayersCount; ++i)
                NotifyPlayerJoined(PlayerInput.s_AllActivePlayers[i]);

            if (m_AllowJoining)
                EnableJoining();
        }

        private void OnDisable()
        {
            if (instance == this)
                instance = null;

            if (m_AllowJoining)
                DisableJoining();
        }

        /// <summary>
        /// If split-screen is enabled, then for each player in the game, adjust the player's <see cref="Camera.rect"/>
        /// to fit the player's split screen area according to the number of players currently in the game and the
        /// current split-screen configuration.
        /// </summary>
        private void UpdateSplitScreen()
        {
            // Nothing to do if split-screen is not enabled.
            if (!m_SplitScreen)
                return;

            // Determine number of split-screens to create based on highest player index we have.
            var minSplitScreenCount = 0;
            foreach (var player in PlayerInput.all)
            {
                if (player.playerIndex >= minSplitScreenCount)
                    minSplitScreenCount = player.playerIndex + 1;
            }

            // Adjust to fixed number if we have it.
            if (m_FixedNumberOfSplitScreens > 0)
            {
                if (m_FixedNumberOfSplitScreens < minSplitScreenCount)
                    Debug.LogWarning(
                        $"Highest playerIndex of {minSplitScreenCount} exceeds fixed number of split-screens of {m_FixedNumberOfSplitScreens}",
                        this);

                minSplitScreenCount = m_FixedNumberOfSplitScreens;
            }

            // Determine divisions along X and Y. Usually, we have a square grid of split-screens so all we need to
            // do is make it large enough to fit all players.
            var numDivisionsX = Mathf.CeilToInt(Mathf.Sqrt(minSplitScreenCount));
            var numDivisionsY = numDivisionsX;
            if (!m_MaintainAspectRatioInSplitScreen && numDivisionsX * (numDivisionsX - 1) >= minSplitScreenCount)
            {
                // We're allowed to produce split-screens with aspect ratios different from the screen meaning
                // that we always add one more column before finally adding an entirely new row.
                numDivisionsY -= 1;
            }

            // Assign split-screen area to each player.
            foreach (var player in PlayerInput.all)
            {
                // Make sure the player's splitScreenIndex isn't out of range.
                var splitScreenIndex = player.splitScreenIndex;
                if (splitScreenIndex >= numDivisionsX * numDivisionsY)
                {
                    Debug.LogError(
                        $"Split-screen index of {splitScreenIndex} on player is out of range (have {numDivisionsX * numDivisionsY} screens); resetting to playerIndex",
                        player);
                    player.m_SplitScreenIndex = player.playerIndex;
                }

                // Make sure we have a camera.
                var camera = player.camera;
                if (camera == null)
                {
                    Debug.LogError(
                        "Player has no camera associated with it. Cannot set up split-screen. Point PlayerInput.camera to camera for player.",
                        player);
                    continue;
                }

                // Assign split-screen area based on m_SplitScreenRect.
                var column = splitScreenIndex % numDivisionsX;
                var row = splitScreenIndex / numDivisionsX;
                var rect = new Rect
                {
                    width = m_SplitScreenRect.width / numDivisionsX,
                    height = m_SplitScreenRect.height / numDivisionsY
                };
                rect.x = m_SplitScreenRect.x + column * rect.width;
                // Y is bottom-to-top but we fill from top down.
                rect.y = m_SplitScreenRect.y + m_SplitScreenRect.height - (row + 1) * rect.height;
                camera.rect = rect;
            }
        }

        private bool IsDeviceUsableWithPlayerActions(InputDevice device)
        {
            Debug.Assert(device != null);

            if (m_PlayerPrefab == null)
                return true;

            var playerInput = m_PlayerPrefab.GetComponentInChildren<PlayerInput>();
            if (playerInput == null)
                return true;

            var actions = playerInput.actions;
            if (actions == null)
                return true;

            // If the asset has control schemes, see if there's one that works with the device plus
            // whatever unpaired devices we have left.
            if (actions.controlSchemes.Count > 0)
            {
                using (var unpairedDevices = InputUser.GetUnpairedInputDevices())
                {
                    if (InputControlScheme.FindControlSchemeForDevices(unpairedDevices, actions.controlSchemes,
                        mustIncludeDevice: device) == null)
                        return false;
                }
                return true;
            }

            // Otherwise just check whether any of the maps has bindings usable with the device.
            foreach (var actionMap in actions.actionMaps)
                if (actionMap.IsUsableWithDevice(device))
                    return true;

            return false;
        }

        private void ValidateInputActionAsset()
        {
#if DEVELOPMENT_BUILD || UNITY_EDITOR
            if (m_PlayerPrefab == null || m_PlayerPrefab.GetComponentInChildren<PlayerInput>() == null)
                return;

            var actions = m_PlayerPrefab.GetComponentInChildren<PlayerInput>().actions;
            if (actions == null)
                return;

            var isValid = true;
            foreach (var controlScheme in actions.controlSchemes)
            {
                if (controlScheme.deviceRequirements.Count > 0)
                    break;

                isValid = false;
            }

            if (isValid) return;

            var assetInfo = actions.name;
#if UNITY_EDITOR
            assetInfo = AssetDatabase.GetAssetPath(actions);
#endif
            Debug.LogWarning($"The input action asset '{assetInfo}' in the player prefab assigned to PlayerInputManager has " +
                "no control schemes with required devices. The JoinPlayersWhenButtonIsPressed join behavior " +
                "will not work unless the expected input devices are listed as requirements in the input " +
                "action asset.", m_PlayerPrefab);
#endif
        }

        /// <summary>
        /// Called by <see cref="PlayerInput"/> when it is enabled.
        /// </summary>
        /// <param name="player"></param>
        internal void NotifyPlayerJoined(PlayerInput player)
        {
            Debug.Assert(player != null);

            UpdateSplitScreen();

            switch (m_NotificationBehavior)
            {
                case PlayerNotifications.SendMessages:
                    SendMessage(PlayerJoinedMessage, player, SendMessageOptions.DontRequireReceiver);
                    break;

                case PlayerNotifications.BroadcastMessages:
                    BroadcastMessage(PlayerJoinedMessage, player, SendMessageOptions.DontRequireReceiver);
                    break;

                case PlayerNotifications.InvokeUnityEvents:
                    m_PlayerJoinedEvent?.Invoke(player);
                    break;

                case PlayerNotifications.InvokeCSharpEvents:
                    DelegateHelpers.InvokeCallbacksSafe(ref m_PlayerJoinedCallbacks, player, "onPlayerJoined");
                    break;
            }
        }

        /// <summary>
        /// Called by <see cref="PlayerInput"/> when it is disabled.
        /// </summary>
        /// <param name="player"></param>
        internal void NotifyPlayerLeft(PlayerInput player)
        {
            Debug.Assert(player != null);

            UpdateSplitScreen();

            switch (m_NotificationBehavior)
            {
                case PlayerNotifications.SendMessages:
                    SendMessage(PlayerLeftMessage, player, SendMessageOptions.DontRequireReceiver);
                    break;

                case PlayerNotifications.BroadcastMessages:
                    BroadcastMessage(PlayerLeftMessage, player, SendMessageOptions.DontRequireReceiver);
                    break;

                case PlayerNotifications.InvokeUnityEvents:
                    m_PlayerLeftEvent?.Invoke(player);
                    break;

                case PlayerNotifications.InvokeCSharpEvents:
                    DelegateHelpers.InvokeCallbacksSafe(ref m_PlayerLeftCallbacks, player, "onPlayerLeft");
                    break;
            }
        }

        [Serializable]
        public class PlayerJoinedEvent : UnityEvent<PlayerInput>
        {
        }

        [Serializable]
        public class PlayerLeftEvent : UnityEvent<PlayerInput>
        {
        }
    }
}