﻿using AsmodeeDigital.Common.Plugin.Manager.Coroutine;
using AsmodeeDigital.Common.Plugin.Manager.Event;
using AsmodeeDigital.Common.Plugin.Utils;
using AsmodeeDigital.Common.Plugin.Utils.Extensions;
using AsmodeeDigital.PlayReal.Plugin.Domain.GameState;
using AsmodeeDigital.PlayReal.Plugin.Domain.Players;
using AsmodeeDigital.PlayReal.Plugin.Logic.Gameplay;
using AsmodeeDigital.PlayReal.Plugin.Manager.Persistence;
using AsmodeeDigital.PlayReal.Plugin.Network;
using AsmodeeDigital.PlayReal.Samples.Domain.Game;
using AsmodeeDigital.PlayReal.Samples.Domain.GameState.Events;
using AsmodeeDigital.PlayReal.Samples.Domain.Message;
using com.daysofwonder.async;
using ProtoBuf.Meta;
using System;
using System.Collections.Generic;

namespace AsmodeeDigital.PlayReal.Samples.Logic.Gameplay
{
    /// <summary>
    /// Distant gameplay logic. Overrides behaviors of main gameplay logic
    /// </summary>
    [Serializable]
    public class DistantGameplayLogic : GameplayLogicBase, IDistantGameplayLogic<GameStateBase, Domain.Message.Message>
    {
        /// <summary>
        /// Instance of the generic component in charge of managing networking
        /// </summary>
        public DistantGameplayComponent<GameStateBase, Domain.Message.Message> DistantGameplayComponent { get; set; }

        #region Network events
        /// <summary>
        /// Event fired when a player presence is updated
        /// </summary>
        public event Action PlayerPresenceUpdatedEvent;

        /// <summary>
        /// Event fired when a game status is refreshed (after a WhatsNewPussycat call)
        /// </summary>
        public event Action<StatusReport> RefreshGameStatusEvent;

        /// <summary>
        /// Event fired when a game is aborted
        /// </summary>
        public event Action<GameAbortedRequest> GameAbortedEvent;

        /// <summary>
        /// Event fired when the local player leaves the game
        /// </summary>
        public event Action<IPlayerSeat, PlayerTimeoutRequest.PlayerStatus> LocalPlayerQuitGameEvent;

        /// <summary>
        /// Event fired when a distant player leaves the game
        /// </summary>
        public event Action<IPlayerSeat, PlayerTimeoutRequest.PlayerStatus> OtherPlayerQuitGameEvent;

        /// <summary>
        /// Event fired when a distant player joins the game
        /// </summary>
        public event Action<IPlayerSeat> PlayerJoinGameEvent;

        /// <summary>
        /// Event fired when the socket is disconnected from the server
        /// </summary>
        public event Action DisconnectedFromServerEvent;

        /// <summary>
        /// Event fired when the user is authenticated on the server after a disconnection
        /// </summary>
        public event Action ReconnectedToServerEvent;

        /// <summary>
        /// Event fired when the reconnection is not possible after many trials
        /// </summary>
        public event Action CantReconnectToServerEvent;
        #endregion

        /// <summary>
        /// Get all players local IDs
        /// </summary>
        public List<int> GetPlayersIds { get { return PlayersSeats.ConvertAll<int>(ps => ps.LocalId); } }

        /// <summary>
        /// Get all players local Id except of local player
        /// </summary>
        public List<int> GetPlayersIdsExceptCurrentPlayer { get { return PlayersSeats.FindAll(ps => !(ps is LocalPlayerSeat)).ConvertAll<int>(ps => ps.LocalId); } }

        public DistantGameplayLogic(ServerConnection srvCnx, Persistence persistence, List<Die> dice) : base(srvCnx, persistence, dice)
        {
            DistantGameplayComponent = new DistantGameplayComponent<GameStateBase, Domain.Message.Message>(srvCnx, persistence, this);
        }

        /// <summary>
        /// Initialize the distant gameplay logic. Initialize the distant gameplay component and call WhatsNewPussycat
        /// </summary>
        public override void Init()
        {
            base.Init();

            DistantGameplayComponent.Init();

            ServerConnection.DisconnectedFromServerEvent += ServerConnection_DisconnectedFromServerEvent;
            ServerConnection.ReconnectedToServerEvent += ServerConnection_ReconnectedToServerEvent;
            ServerConnection.CantReconnectToServerEvent += ServerConnection_CantReconnectToServerEvent;

            ServerConnection.SwitchedToGameRequest(Persistence.CurrentGameDetails.game_id);
            ServerConnection.WhatsNewPussycat();
        }

        private void ServerConnection_DisconnectedFromServerEvent()
        {
            if (DisconnectedFromServerEvent != null)
                DisconnectedFromServerEvent();
        }

        private void ServerConnection_ReconnectedToServerEvent()
        {
            ServerConnection.SwitchedToGameRequest(Persistence.CurrentGameDetails.game_id);
            ServerConnection.WhatsNewPussycat();

            if (ReconnectedToServerEvent != null)
                ReconnectedToServerEvent();
        }

        private void ServerConnection_CantReconnectToServerEvent()
        {
            if (CantReconnectToServerEvent != null)
                CantReconnectToServerEvent();
        }

        #region Network events
        /// <summary>
        /// Trigger RefreshGameStatusEvent. Force refresh of UI when a game status is updated
        /// </summary>
        /// <param name="statusReport"></param>
        public void RefreshGameStatus(StatusReport statusReport)
        {
            if (RefreshGameStatusEvent != null)
                EventManager.Instance.QueueEvent(RefreshGameStatusEvent, statusReport);
        }

        /// <summary>
        /// Trigger PlayerPresenceUpdatedEvent. Force refresh of UI when a player presence is updated
        /// </summary>
        public void PlayerPresenceUpdated()
        {
            if (PlayerPresenceUpdatedEvent != null)
                EventManager.Instance.QueueEvent(PlayerPresenceUpdatedEvent);
        }

        /// <summary>
        /// Trigger GameAbortedEvent. Show game over panel in UI
        /// </summary>
        /// <param name="gameAbortedRequest"></param>
        public void GameAborted(GameAbortedRequest gameAbortedRequest)
        {
            State = States.GameAborted;

            if (GameAbortedEvent != null)
                EventManager.Instance.QueueEvent(GameAbortedEvent, gameAbortedRequest);
        }

        /// <summary>
        /// Trigger LocalPlayerQuitGameEvent. Show the game over panel when the local player leave the game
        /// </summary>
        /// <param name="playerSeat"></param>
        /// <param name="playerStatus"></param>
        public void LocalPlayerQuitGame(IPlayerSeat playerSeat, PlayerTimeoutRequest.PlayerStatus playerStatus)
        {
            if (LocalPlayerQuitGameEvent != null)
                EventManager.Instance.QueueEvent(LocalPlayerQuitGameEvent, playerSeat, playerStatus);
        }

        /// <summary>
        /// Trigger OtherPlayerQuitGameEvent. Show a warning when a distant player leave the game
        /// </summary>
        /// <param name="playerSeat"></param>
        /// <param name="playerStatus"></param>
        public void OtherPlayerQuitGame(IPlayerSeat playerSeat, PlayerTimeoutRequest.PlayerStatus playerStatus)
        {
            if (OtherPlayerQuitGameEvent != null)
                EventManager.Instance.QueueEvent(OtherPlayerQuitGameEvent, playerSeat, playerStatus);
        }

        /// <summary>
        /// Trigger PlayerJoinGameEvent. Show a message when a player join the game
        /// </summary>
        /// <param name="distantPlayerSeat"></param>
        public void PlayerJoinGame(DistantPlayerSeat distantPlayerSeat)
        {
            if (PlayerJoinGameEvent != null)
                EventManager.Instance.QueueEvent(PlayerJoinGameEvent, distantPlayerSeat);
        }

        /// <summary>
        /// Process multicast messages received from the server
        /// </summary>
        /// <param name="message">Message received from the server</param>
        public void MessageReceived(Domain.Message.Message message)
        {
            switch (message.MessageType)
            {
                //---> Warn View if the round is ended and show winner(s)
                case MessageType.RoundEnded:

                    GameState = message.GameState;

                    CoroutineManager.StartCoroutine(EndOfRound());

                    break;

                //---> Select dice of other players
                case MessageType.SelectDice:

                    DiceSelected diceSelected = message.GameState.Events.Last() as DiceSelected;

                    for (int i = 0; i < diceSelected.SelectedDice.Count; i++)
                    {
                        ChangeDiceSelection(i, diceSelected.SelectedDice[i]);
                    }

                    break;

                //---> Roll dice of other players
                case MessageType.RollDice:

                    GameState = message.GameState;

                    DiceRolled diceRolled = message.GameState.Events.Last() as DiceRolled;

                    for (int i = 0; i < diceRolled.DiceIndicesRolled.Count; i++)
                    {
                        ChangeDiceSelection(diceRolled.DiceIndicesRolled[i], true);
                    }

                    RollDiceAnimation();

                    break;

                //---> Refresh UI when player change
                case MessageType.NextPlayer:

                    if (GameState.Events.Count <= message.GameState.Events.Count)
                        GameState = message.GameState;

                    UpdateNextPlayer(message.GameState.NextPlayers);

                    break;

                default:
                    break;
            }
        }
        #endregion

        #region Gameplay
        /// <summary>
        /// Sends a game over request with the ordered list of winners
        /// </summary>
        public override void GameOver()
        {
            List<IPlayerSeat> orderedPlayerSeatWinner = GetOrderedWinnerGame();
            if (orderedPlayerSeatWinner != null)
            {
                List<Tuple<int, int>> orderedPlayersAndScore = orderedPlayerSeatWinner.ConvertAll<Tuple<int, int>>(ow => new Tuple<int, int>(ow.LocalId, GetScore(ow.LocalId)));
                var game = Persistence.CurrentGameDetails;
                ServerConnection.GameOverRequest(game.game_id, orderedPlayersAndScore, GameState, game.configuration.rated);
            }
        }

        /// <summary>
        /// Set the State to GameOver when the local player is replaced (timeout)
        /// </summary>
        public void SetGameOver()
        {
            State = States.GameOver;
        }
        #endregion

        #region Send multicast messages
        /// <summary>
        /// Sends a multicast message for selecting the dice on each players devices
        /// </summary>
        /// <param name="diceSelectionState">List of selected state for each dice. True the dice is selected, False it is not</param>
        private void SendSelectedDiceMessage(List<bool> diceSelectionState)
        {
            //---> Send message for UI synchronization over other players
            GameStateBase gameStateTemp = new GameStateBase();
            gameStateTemp.Events = new List<IEvent>();

            //---> Create temp event in temp gamestate
            gameStateTemp.Events.Add(new DiceSelected(CurrentPlayer.LocalId, diceSelectionState));

            Domain.Message.Message message = new Domain.Message.Message(gameStateTemp, MessageType.SelectDice);
            ServerConnection.MultiCastDataRequest(Persistence.CurrentGameDetails.game_id, GetPlayersIdsExceptCurrentPlayer, message);
        }

        /// <summary>
        /// Sends a multicast message for rolling the selected dice on each players devices
        /// </summary>
        /// <param name="diceIndiceSelected">List of selected dice. If a dice's index is not in the list, so the dice is not selected</param>
        private void SendRolledDiceMessage(List<int> diceIndiceSelected)
        {
            //---> Send message for UI synchronization over other players
            Domain.Message.Message message = new Domain.Message.Message(GameState, MessageType.RollDice);
            ServerConnection.MultiCastDataRequest(Persistence.CurrentGameDetails.game_id, GetPlayersIdsExceptCurrentPlayer, message);
        }

        /// <summary>
        /// Sends a multicast message to notify all players of player change
        /// </summary>
        private void SendNextPlayerMessage()
        {
            Domain.Message.Message message = new Domain.Message.Message(GameState, MessageType.NextPlayer);
            ServerConnection.MultiCastDataRequest(Persistence.CurrentGameDetails.game_id, GetPlayersIds, message);
        }

        private void SendEndOfRoundMessage()
        {
            Domain.Message.Message message = new Domain.Message.Message(GameState, MessageType.RoundEnded);
            ServerConnection.MultiCastDataRequest(Persistence.CurrentGameDetails.game_id, GetPlayersIdsExceptCurrentPlayer, message);
        }
        #endregion

        #region Public methods accessed by view
        /// <summary>
        /// Change dice selection after the first roll then sends a multicast message to other players
        /// </summary>
        /// <param name="index">Dice index to select</param>
        /// <param name="selected">Select or deselect dice</param>
        /// <returns>True if the dice selection can change</returns>
        public override bool TryChangeDiceSelection(int index, bool selected)
        {
            bool canSelectDice = base.TryChangeDiceSelection(index, selected);

            if (canSelectDice)
            {
                //--- Get indice of selected dice
                List<bool> diceSelectionState = Dice.ConvertAll<bool>(d => d.Selected);
                //---

                SendSelectedDiceMessage(diceSelectionState);
            }

            return canSelectDice;
        }

        /// <summary>
        /// Show *main menu* view. If game state is game outcome, notify the server that the outcome has been displayed
        /// </summary>
        public override void ShowLobbyView()
        {
            if (State == States.GameOutcome)
            {
                ServerConnection.GameOutcomeConfirmationRequest(Persistence.CurrentGameDetails.game_id, Persistence.ConnectedPlayer.id);
            }
            else if (State == States.GameAborted)
            {
                ServerConnection.GameAbortedConfirmationRequest(Persistence.CurrentGameDetails.game_id, Persistence.ConnectedPlayer.id);
            }

            base.ShowLobbyView();
        }

        /// <summary>
        /// Sends a *game forfeit* request to the server for the current player
        /// </summary>
        public void Forfeit()
        {
            ServerConnection.GameForfeitRequest(Persistence.CurrentGameDetails.game_id);
        }

        /// <summary>
        /// Sends a *leave game* request then show *main menu* view
        /// </summary>
        public override void LeaveGame()
        {
            if (ServerConnection.ConnectionState >= ConnectionState.Connected)
                DistantGameplayComponent.LeaveGame();

            base.LeaveGame();
        }

        /// <summary>
        /// At the end of player turn, commit the game state to the server.
        /// Send a next player turn to all players
        /// </summary>
        public override void NextPlayer()
        {
            DistantGameplayComponent.CommitAction(true);

            SendNextPlayerMessage();
        }
        #endregion

        #region Create events
        /// <summary>
        /// Create *dice rolled* event in game state. Store the list of selected dice to roll.
        /// Then commits the game state to the server. The current player can continue to play
        /// </summary>
        /// <param name="diceIndice">List of selected dice to roll</param>
        public override void CreateEventDiceRolled(List<int> diceIndice = null)
        {
            //---> Call the GameplayLogicBase method
            base.CreateEventDiceRolled(diceIndice);

            //---> Commit the action to the **Scalable Server**
            DistantGameplayComponent.CommitAction(false);

            //---> Send a multicast message so that other players see the animation of the dice
            SendRolledDiceMessage(diceIndice);
        }

        /// <summary>
        /// Create *round ended* event.
        /// Send a end of round message to all players
        /// </summary>
        public override void CreateEventRoundEnded()
        {
            base.CreateEventRoundEnded();

            SendEndOfRoundMessage();
        }
        #endregion

        /// <summary>
        /// Dispose Distant gameplay component
        /// </summary>
        public override void Dispose()
        {
            base.Dispose();

            DistantGameplayComponent.Dispose();
        }
    }
}