using System;
using System.Collections.Generic;
using UnityEngine;
using TiltFive.Logging;

#if UNITY_2019_1_OR_NEWER && INPUTSYSTEM_AVAILABLE
using UnityEngine.InputSystem;
#endif

namespace TiltFive
{
    /// <summary>
    /// Provides access to player settings and functionality.
    /// </summary>
    public class Player : Singleton<Player>
    {
        #region Private Fields

        private Dictionary<PlayerIndex, PlayerCore> players = new Dictionary<PlayerIndex, PlayerCore>();
        internal static bool scanningForPlayers = false;

        #endregion


        #region Public Functions

        /// <summary>
        /// Determines whether the specified player has an associated pair of glasses connected.
        /// </summary>
        /// <param name="playerIndex"></param>
        /// <returns></returns>
        public static bool IsConnected(PlayerIndex playerIndex)
        {
            return playerIndex != PlayerIndex.None && Instance.players.TryGetValue(playerIndex, out var playerCore);
        }


        /// <summary>
        /// Attempts to get the friendly name assigned to the specified player's glasses.
        /// </summary>
        /// <param name="playerIndex"></param>
        /// <param name="friendlyName"></param>
        /// <returns></returns>
        public static bool TryGetFriendlyName(PlayerIndex playerIndex, out string friendlyName)
        {
            if (playerIndex == PlayerIndex.None || !Instance.players.TryGetValue(playerIndex, out var playerCore))
            {
                friendlyName = "";
                return false;
            }

            friendlyName = playerCore.FriendlyName;
            return true;
        }

        #endregion


        #region Internal Functions

        /// <summary>
        /// Updates the specified player.
        /// </summary>
        /// <param name="playerSettings"></param>
        internal static void Update(PlayerSettings playerSettings, SpectatorSettings spectatorSettings)
        {
            if (scanningForPlayers)
            {
                return;
            }

            if (playerSettings != null && Instance.players.TryGetValue(playerSettings.PlayerIndex, out var playerCore))
            {
                var playerIndex = playerSettings.PlayerIndex;

                // Check to make sure the glasses for this player are still connected.
                if (!Glasses.IsConnected(playerCore.GlassesHandle))
                {
                    // This player's glasses are gone, so it's time to destroy this PlayerCore.
                    Log.Info($"Player {playerIndex} was removed due to their glasses (handle: {playerCore.GlassesHandle}) being disconnected.");
                    Instance.players.Remove(playerIndex);
                    return;
                }

                playerCore.Update(playerSettings, spectatorSettings);
            }
        }

        /// <summary>
        /// Resets the internal state of the specified player.
        /// </summary>
        /// <param name="playerSettings"></param>
        internal static void Reset(PlayerSettings playerSettings, SpectatorSettings spectatorSettings)
        {
            var playerIndex = playerSettings.PlayerIndex;

            if (playerSettings != null && Instance.players.TryGetValue(playerIndex, out var playerCore))
            {
                if (Glasses.IsConnected(playerCore.GlassesHandle))
                {
                    playerCore.Reset(playerSettings, spectatorSettings);
                }
                else
                {
                    Validate(playerSettings);
                }
            }
        }

        /// <summary>
        /// Ensures that internal state information is valid.
        /// </summary>
        /// <param name="playerSettings"></param>
        internal static void Validate(PlayerSettings playerSettings)
        {
            if (playerSettings == null)
            {
                return;
            }

            var scaleSettings = playerSettings.scaleSettings;
            scaleSettings.contentScaleRatio = Mathf.Clamp(scaleSettings.contentScaleRatio, ScaleSettings.MIN_CONTENT_SCALE_RATIO, float.MaxValue);

            playerSettings.Validate();
        }


        /// <summary>
        /// Searches for newly connected hardware and creates one or more players if possible.
        /// </summary>
        internal static void ScanForNewPlayers()
        {
            if (scanningForPlayers)
            {
                return;
            }
            scanningForPlayers = true;
            Glasses.ScanForGlasses();

            Wand.ScanForWands();
            scanningForPlayers = false;
        }

        internal static bool TryGetGlassesHandle(PlayerIndex playerIndex, out GlassesHandle glassesHandle)
        {
            if (playerIndex == PlayerIndex.None)
            {
                glassesHandle = new GlassesHandle();
                return false;
            }
            var playerAvailable = Instance.players.TryGetValue(playerIndex, out var playerCore);
            glassesHandle = playerCore?.GlassesHandle ?? new GlassesHandle();
            return playerAvailable;
        }

        internal static bool TryGetPlayerIndex(GlassesHandle glassesHandle, out PlayerIndex playerIndex)
        {
            foreach (var keyValuePair in Instance.players)
            {
                var currentPlayerIndex = keyValuePair.Key;
                var currentPlayerCore = keyValuePair.Value;
                if (currentPlayerCore.GlassesHandle == glassesHandle)
                {
                    playerIndex = currentPlayerIndex;
                    return true;
                }
            }
            playerIndex = PlayerIndex.None;
            return false;
        }

        internal static bool AllSupportedPlayersConnected()
        {
            if(!TiltFiveSingletonHelper.TryGetISceneInfo(out var sceneInfo))
            {
                return false;
            }
            var supportedPlayers = sceneInfo.GetSupportedPlayerCount();

            for(PlayerIndex playerIndex = PlayerIndex.One; playerIndex <= (PlayerIndex)supportedPlayers; playerIndex++)
            {
                // If any supported player isn't connected, return false
                if(!IsConnected(playerIndex))
                {
                    return false;
                }
            }
            // Otherwise they're all connected, and we can return true.
            return true;
        }

        /// <summary>
        ///
        /// </summary>
        /// <returns>Returns true if we successfully added a player.</returns>
        internal static bool TryAddPlayer(GlassesHandle glassesHandle, out PlayerIndex playerIndex)
        {
            var players = Instance.players;
            if (players.Count >= GlassesSettings.MAX_SUPPORTED_GLASSES_COUNT)
            {
                playerIndex = PlayerIndex.None;
                return false;
            }

            if(TryGetPlayerIndex(glassesHandle, out var existingPlayerIndex))
            {
                playerIndex = PlayerIndex.None;
                return false;   // This player already exists.
            }

            foreach (PlayerIndex currentPlayerIndex in Enum.GetValues(typeof(PlayerIndex)))
            {
                if (currentPlayerIndex == PlayerIndex.None)
                {
                    continue;
                }

                // Assign the smallest (numerically) playerIndex that isn't already assigned.
                // For example, if player #1 disappeared, and player #2 is still available,
                // then we assign the specified glasses to a new player #1.
                if (!players.ContainsKey(currentPlayerIndex))
                {
                    Glasses.TryGetFriendlyName(glassesHandle, out var friendlyName);
                    players[currentPlayerIndex] = new PlayerCore(glassesHandle, friendlyName);

                    Log.Info($"Player {currentPlayerIndex} created. Glasses: {glassesHandle} (\"{friendlyName}\")");

                    // Default control should go to the lowest player index.
                    // If this playerIndex is lower than that of any other current players,
                    // reset the default glasses handle.


                    bool lowestPlayerIndex = false;

                    // TODO: Determine whether this is the lowest player index.
                    // Until then, default control stays with player 2 in the above scenario.

                    if (lowestPlayerIndex)
                    {
                        Glasses.SetDefaultGlassesHandle(glassesHandle);
                    }

                    playerIndex = currentPlayerIndex;
                    return true;
                }
            }

            // We shouldn't ever reach this.
            playerIndex = PlayerIndex.None;
            return false;
        }

        internal static void OnDisable()
        {
            Glasses.OnDisable();
            Wand.OnDisable();
        }

        internal static bool TryGetFirstConnectedPlayer(out PlayerIndex playerIndex)
        {
            for(PlayerIndex i = PlayerIndex.One; i <= PlayerIndex.Four; i++)
            {
                if(IsConnected(i))
                {
                    playerIndex = i;
                    return true;
                }
            }

            playerIndex = PlayerIndex.None;
            return false;
        }

#if UNITY_EDITOR

        /// <summary>
        /// Draws any gizmos associated with the specified player.
        /// </summary>
        /// <param name="playerSettings"></param>
        internal static void DrawGizmos(PlayerSettings playerSettings)
        {
            if (playerSettings == null)
            {
                return;
            }

            var gameBoardSettings = playerSettings.gameboardSettings;

            if (gameBoardSettings.currentGameBoard != null)
            {
                gameBoardSettings.currentGameBoard.DrawGizmo(playerSettings.scaleSettings, gameBoardSettings);
            }
        }
#endif

        #endregion


        #region Private Classes

        private class PlayerCore
        {
            public readonly GlassesHandle GlassesHandle;
            public readonly string FriendlyName;

            public PlayerCore(GlassesHandle glassesHandle, string friendlyName)
            {
                GlassesHandle = glassesHandle;
                FriendlyName = friendlyName;
            }

            internal void Update(PlayerSettings playerSettings, SpectatorSettings spectatorSettings)
            {
                if (!Glasses.Validate(playerSettings.glassesSettings, spectatorSettings, GlassesHandle))
                {
                    Glasses.Reset(playerSettings.glassesSettings, spectatorSettings, GlassesHandle);
                }
                GetLatestPoseData(playerSettings, spectatorSettings);
            }

            internal void Reset(PlayerSettings playerSettings, SpectatorSettings spectatorSettings)
            {
                Glasses.Reset(playerSettings.glassesSettings, spectatorSettings, GlassesHandle);
            }


            /// <summary>
            /// Obtains the latest pose for all trackable objects.
            /// </summary>
            private void GetLatestPoseData(PlayerSettings playerSettings, SpectatorSettings spectatorSettings)
            {
                var glassesSettings = playerSettings.glassesSettings;
                var rightWandSettings = playerSettings.rightWandSettings;
                var leftWandSettings = playerSettings.leftWandSettings;
                var scaleSettings = playerSettings.scaleSettings;
                var gameboardSettings = playerSettings.gameboardSettings;

                Glasses.Update(GlassesHandle, glassesSettings, scaleSettings, gameboardSettings, spectatorSettings);
                Wand.Update(GlassesHandle, rightWandSettings, scaleSettings, gameboardSettings);
                Wand.Update(GlassesHandle, leftWandSettings, scaleSettings, gameboardSettings);
            }
        }

        #endregion
    }
}