﻿using AsmodeeDigital.Common.Plugin.Manager.Coroutine;
using AsmodeeDigital.Common.Plugin.Manager.Event;
using AsmodeeDigital.Common.Plugin.Manager.Random;
using AsmodeeDigital.Common.Plugin.Manager.Scene;
using AsmodeeDigital.Common.Plugin.Utils;
using AsmodeeDigital.Common.Plugin.Utils.Extensions;
using AsmodeeDigital.PlayReal.Plugin.Domain.GameConfiguration;
using AsmodeeDigital.PlayReal.Plugin.Domain.GameState;
using AsmodeeDigital.PlayReal.Plugin.Domain.Players;
using AsmodeeDigital.PlayReal.Plugin.Logic;
using AsmodeeDigital.PlayReal.Plugin.Logic.Engine;
using AsmodeeDigital.PlayReal.Plugin.Logic.Gameplay;
using AsmodeeDigital.PlayReal.Plugin.Manager.Persistence;
using AsmodeeDigital.PlayReal.Plugin.Manager.Save;
using AsmodeeDigital.PlayReal.Plugin.Network;
using AsmodeeDigital.PlayReal.Samples.Domain.Game;
using AsmodeeDigital.PlayReal.Samples.Domain.GameState.Events;
using AsmodeeDigital.PlayReal.Samples.Logic.ArtificialIntelligence;
using AsmodeeDigital.PlayReal.Samples.Logic.Engine;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace AsmodeeDigital.PlayReal.Samples.Logic.Gameplay
{
    /// <summary>
    /// Gameplay logic. Shared by the local and distant gameplay logics
    /// </summary>
    [Serializable]
    public abstract class GameplayLogicBase : LogicBase, IGameplayLogic<GameStateBase>, IDisposable
    {
        #region Events
        /// <summary>
        /// Event fired when it's local player turn
        /// </summary>
        public event Action<LocalPlayerSeat> LocalPlayerTurnEvent;

        /// <summary>
        /// Event fired when it's other player turn (distant or bot)
        /// </summary>
        public event Action<IPlayerSeat> OtherPlayerTurnEvent;

        /// <summary>
        /// Event fired when a round is ended
        /// </summary>
        public event Action<List<IPlayerSeat>> RoundEndedEvent;

        /// <summary>
        /// Event fired when a game is finished
        /// </summary>
        public event Action<List<IPlayerSeat>> GameOutcomeEvent;

        /// <summary>
        /// Event fired to roll dice in UI
        /// </summary>
        public event Action RollDiceAnimationEvent;

        /// <summary>
        /// Event fired to select dice in UI
        /// </summary>
        public event Action<int> DiePickedIndexEvent;
        #endregion

        // GameState can be encapsulated in an encrypted MemoryStream decrypted via CryptoStream
        /// <summary>
        /// Game state of the game. Contains all events committed by the players
        /// </summary>
        public GameStateBase GameState { get; set; }

        /// <summary>
        /// Game engine capable of interpreting the events of the game state
        /// </summary>
        public IGameEngine GameEngine { get; set; }

        /// <summary>
        /// Current active player
        /// </summary>
        public IPlayerSeat CurrentPlayer { get; set; }

        /// <summary>
        /// List of all players (local, distant, bot)
        /// </summary>
        public List<IPlayerSeat> PlayersSeats { get; set; }

        /// <summary>
        /// State of the game session
        /// </summary>
        public States State = States.NotInGame;

        /// <summary>
        /// Dice rolled by the players. Useful for selection status
        /// </summary>
        public List<Die> Dice;

        /// <summary>
        /// Artificial intelligence
        /// </summary>
        protected AI ai;

        public GameplayLogicBase(ServerConnection serverConnection, Persistence persistence, List<Die> dice) : base(serverConnection, persistence)
        {
            PlayersSeats = new List<IPlayerSeat>();
            this.Dice = dice;
        }

        #region Initialization
        /// <summary>
        /// Initialize the gameplay logic. Create GameEngine, GameState, AI. Set game state to InGame
        /// </summary>
        public virtual void Init()
        {
            ai = new AI(this);
            CurrentPlayer = null;

            GameEngine = new GameEngine();
            GameState = new GameStateBase();
            GameState.Events = new List<IEvent>();

            State = States.InGame;
        }
        #endregion

        #region Fire events to View
        /// <summary>
        /// Event raised when a round ended
        /// </summary>
        /// <param name="winners"></param>
        public void RoundEnded(List<IPlayerSeat> winners)
        {
            if (RoundEndedEvent != null)
                EventManager.Instance.QueueEvent(RoundEndedEvent, winners);
        }

        /// <summary>
        /// Event raised to roll dice in UI
        /// </summary>
        public void RollDiceAnimation()
        {
            if (RollDiceAnimationEvent != null)
                EventManager.Instance.QueueEvent(RollDiceAnimationEvent);
        }

        /// <summary>
        /// Event raised to pick a die in UI
        /// </summary>
        /// <param name="index"></param>
        public void DiePickedIndex(int index)
        {
            if (DiePickedIndexEvent != null)
                EventManager.Instance.QueueEvent(DiePickedIndexEvent, index);
        }

        /// <summary>
        /// Event raised to signal a distant or robot player turn
        /// </summary>
        protected void OtherPlayerTurn()
        {
            if (OtherPlayerTurnEvent != null)
                EventManager.Instance.QueueEvent(OtherPlayerTurnEvent, CurrentPlayer);
        }
        #endregion

        #region
        /// <summary>
        /// Start the turn of a distant / local / robot player
        /// </summary>
        /// <param name="nextPlayers">List of all next players</param>
        /// <param name="canPlayAI">True if the local player can play AI for a left player (Only ater receiving a PlayerTimeoutRequest) or in solo mode</param>
        public void UpdateNextPlayer(List<int> nextPlayers, bool canPlayAI = false)
        {
            GameState.NextPlayers = nextPlayers;

            int idNextPlayer = GameState.NextPlayers.First();
            IPlayerSeat nextPlayerSeat = PlayersSeats.Find(ps => ps.LocalId == idNextPlayer);

            if (nextPlayerSeat is LocalPlayerSeat)
                LocalPlayerTurn((LocalPlayerSeat)nextPlayerSeat);
            else if (nextPlayerSeat is DistantPlayerSeat)
                DistantPlayerTurn((DistantPlayerSeat)nextPlayerSeat);
            else if (nextPlayerSeat is RobotPlayerSeat)
                RobotTurn((RobotPlayerSeat)nextPlayerSeat, canPlayAI);
        }

        /// <summary>
        /// Beginning of the turn of the local player. For first player, create the seed in game state
        /// </summary>
        /// <param name="localPlayerSeat"></param>
        public void LocalPlayerTurn(LocalPlayerSeat localPlayerSeat)
        {
            if ((State == States.InGame || State == States.WaitingOtherPlayers) && !(CurrentPlayer == localPlayerSeat))
            {
                if (IsFirtDiceRoll())
                    State = States.StartPlaying;
                else
                    State = States.DiceRolled;

                StartPlayerTurn(localPlayerSeat);

                //---> Initialize the game state if it's empty
                if (GameState.Events.Count == 0)
                    CreateEventGameInitialized();

                if (LocalPlayerTurnEvent != null)
                    EventManager.Instance.QueueEvent(LocalPlayerTurnEvent, localPlayerSeat);
            }
        }

        /// <summary>
        /// Beginning of the turn of a distant player
        /// </summary>
        ///<param name="distantPlayerSeat">Distant player seat to become current player </param>
        public void DistantPlayerTurn(DistantPlayerSeat distantPlayerSeat)
        {
            CurrentPlayer = distantPlayerSeat;

            if (IsFirtDiceRoll())
            {
                StartPlayerTurn(distantPlayerSeat);

                OtherPlayerTurn();
            }
        }

        /// <summary>
        /// Beginning of the turn of a bot
        /// </summary>
        public void RobotTurn(RobotPlayerSeat robotPlayerSeat, bool canPlayAI)
        {
            //---> If local otherPlayer is a Bot, and the bot is waiting, then current device play the bot
            if (ai.AIState == AIStates.NotPlaying)
            {
                StartPlayerTurn(robotPlayerSeat);

                OtherPlayerTurn();

                if (canPlayAI)
                {
                    //---> Initialize the game state if it's empty
                    if (GameState.Events.Count == 0)
                        CreateEventGameInitialized();

                    ai.SetState(AIStates.StartPlaying);
                }
            }
        }

        /// <summary>
        /// Start of a player turn. Set the current PlayerSeat and select all the dice
        /// </summary>
        /// <param name="playerSeat"></param>
        public void StartPlayerTurn(IPlayerSeat playerSeat)
        {
            CurrentPlayer = playerSeat;

            //---> All dice are selected for first roll
            Dice.ForEach(d => ChangeDiceSelection(d.Index, true));
        }

        /// <summary>
        /// Set state to game outcome and spread event to the UI
        /// </summary>
        /// <param name="gameState"></param>
        public virtual void SetGameOutcome(bool saveGameLocally)
        {
            State = States.GameOutcome;

            if (saveGameLocally)
                SaveGameLocally();

            if (GameOutcomeEvent != null)
            {
                List<IPlayerSeat> orderedPlayersSeats = GetOrderedWinnerGame(GameState);
                List<IPlayerSeat> winners = orderedPlayersSeats.FindAll(p => GetScore(p.LocalId, GameState) == ((GameEngine)GameEngine).MaxScore);

                EventManager.Instance.QueueEvent(GameOutcomeEvent, winners);
            }
        }

        /// <summary>
        /// True if game over or game outcome
        /// </summary>
        /// <returns></returns>
        public bool IsGameOverOrGameOutcome()
        {
            return State == States.GameOver || State == States.GameOutcome;
        }

        /// <summary>
        /// Next player turn after end of turn of the current player. Can be specialized in local and distant gameplay logic
        /// </summary>
        public virtual void NextPlayer() { }

        /// <summary>
        /// End of round. Send list of winner to the view and end the turn 2 seconds later
        /// </summary>
        /// <returns></returns>
        public IEnumerator EndOfRound()
        {
            List<IPlayerSeat> winners = GetWinnerLastRound();

            RoundEnded(winners);

            yield return new WaitForSeconds(2f);
        }

        /// <summary>
        /// Virtual method for game over. Can be specialized in local and distant gameplay logic
        /// </summary>
        public virtual void GameOver() { }

        /// <summary>
        /// True if player can roll dice
        /// </summary>
        /// <returns></returns>
        public bool CanRollDice()
        {
            //--- Player can roll dice a 2nd time
            TurnEnded lastTurnEnded = (TurnEnded)GameState.Events.FindLast(e => e is TurnEnded);
            int countDiceRolled = GameState.Events.FindAll(e => e is DiceRolled && GameState.Events.IndexOf(e) > GameState.Events.IndexOf(lastTurnEnded)).Count;
            //---

            return countDiceRolled < 2;
        }

        /// <summary>
        /// True if it's the first dice roll
        /// </summary>
        /// <returns></returns>
        public bool IsFirtDiceRoll()
        {
            return GameState == null ||
                GameState.Events == null ||
                GameState.Events.Count == 0 ||
                !(GameState.Events.Last() is DiceRolled);
        }

        /// <summary>
        /// Set dice selection and refresh UI
        /// </summary>
        /// <param name="index">Dice index to select</param>
        /// <param name="selected">Select or deselect dice</param>
        public void ChangeDiceSelection(int index, bool selected)
        {
            Die die = Dice.Find(d => d.Index == index);
            die.Selected = selected;

            DiePickedIndex(index);
        }

        /// <summary>
        /// Create DiceRolled event from selected dice
        /// </summary>
        public void RollSelectedDice()
        {
            State = States.DiceRolled;

            //--- Get dice indice to roll
            List<int> indiceRolledDice = Dice.FindAll(d => d.Selected).ConvertAll<int>(d => d.Index);
            //---

            CreateEventDiceRolled(indiceRolledDice);
        }

        /// <summary>
        /// End of turn.
        /// Create TurnEnded event, RoundEnded event if the round is over, GameOver if there is a winner otherwise NextPlayer
        /// </summary>
        public void EndOfTurn()
        {
            State = States.WaitingOtherPlayers;

            CreateEventTurnEnded();

            //---> When next player is first player : Round ended
            // Index 0 : Current player, Index 1 : Next player
            GameInitialized gameInitialized = (GameInitialized)GameState.Events[0];

            if (GameState.NextPlayers[1] == gameInitialized.PlayerLocalID)
            {
                CreateEventRoundEnded();

                CoroutineManager.StartCoroutine(EndOfRound());

                //---> Send a game over request if a player won 3 rounds
                List<IPlayerSeat> orderedPlayerSeatWinner = GetOrderedWinnerGame();
                if (orderedPlayerSeatWinner != null)
                {
                    GameOver();
                }
            }

            if (!IsGameOverOrGameOutcome())
            {
                NextPlayer();
            }
        }

        /// <summary>
        /// Save the game state locally for replay
        /// </summary>
        /// <param name="gameState"></param>
        protected void SaveGameLocally()
        {
            SaveManager.SaveGameState(GameState, String.Format(@"AsmodeeDigital\PlayReal\{0}\{1:yyyyMMdd_HHmmss}_v1.replay", Persistence.ConnectedPlayer.name, DateTime.Now));
        }
        #endregion

        #region Public methods accessed by view
        /// <summary>
        /// Change dice selection after the first roll. Must be called on UI or by AI
        /// </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 virtual bool TryChangeDiceSelection(int index, bool selected)
        {
            //---> The dice can be selected only after the first roll
            bool canSelectDice = State == States.DiceRolled && CanRollDice();

            if (canSelectDice)
            {
                ChangeDiceSelection(index, selected);
            }

            return canSelectDice;
        }

        /// <summary>
        /// Show lobby view
        /// </summary>
        public virtual void ShowLobbyView()
        {
            Persistence.CurrentGameDetails = null;
            Persistence.GameType = GameType.None;

            SceneManager.LoadScene("2_Lobby");
        }

        /// <summary>
        /// Show lobby view
        /// </summary>
        public virtual void ShowMainMenuView()
        {
            Persistence.CurrentGameDetails = null;
            Persistence.GameType = GameType.None;

            SceneManager.LoadScene("1_MainMenu");
        }

        /// <summary>
        /// The current player leave the game. Can be specialized in local and distant gameplay logic 
        /// </summary>
        public virtual void LeaveGame()
        {
            ShowLobbyView();
        }
        #endregion

        #region Get values from GameState
        /// <summary>
        /// Get Score of defined player, by default the last player
        /// </summary>
        /// <param name="playerLocalId">Player local ID. By default the last player</param>
        /// <param name="gameState">Game state whose the score is computed. By default the game state of the current game is used</param>
        /// <returns></returns>
        public int GetScore(int playerLocalId = -1, GameStateBase gameState = null)
        {
            if (gameState == null)
                gameState = GameState;

            if (playerLocalId == -1)
                playerLocalId = CurrentPlayer.LocalId;

            return ((GameEngine)GameEngine).ComputeEvents<int>(gameState, playerLocalId, Evaluation.GetScore);
        }

        /// <summary>
        /// Get dice values of a player, by default the last player which has played (not necessarily the current player)
        /// </summary>
        /// <param name="playerLocalId">Player local ID. By default the last player</param>
        /// <returns></returns>
        public List<int> GetLastDiceValues(int playerLocalId = -1)
        {
            if (playerLocalId == -1)
            {
                DiceRolled lastDiceRolled = (DiceRolled)GameState.Events.FindLast(e => e is DiceRolled);

                if (lastDiceRolled != null)
                    playerLocalId = lastDiceRolled.PlayerLocalID;
                else if (CurrentPlayer != null)
                    playerLocalId = CurrentPlayer.LocalId;
                else
                    playerLocalId = 1;
            }

            return ((GameEngine)GameEngine).ComputeEvents<List<int>>(GameState, playerLocalId, Evaluation.GestLastDiceValues);
        }

        /// <summary>
        /// Get sum of a player, by default the last player
        /// </summary>
        /// <param name="playerLocalId">Player local ID. By default the last player</param>
        /// <returns></returns>
        public int GetLastDiceSum(int playerLocalId = -1)
        {
            if (playerLocalId == -1)
                playerLocalId = CurrentPlayer.LocalId;

            return ((GameEngine)GameEngine).ComputeEvents<int>(GameState, playerLocalId, Evaluation.GetLastDiceSum);
        }

        /// <summary>
        /// Get winner of last round. Equals are possible
        /// </summary>
        /// <param name="gameState">Game state whose the winners list is computed. By default the game state of the current game is used</param>
        /// <returns></returns>
        public List<IPlayerSeat> GetWinnerLastRound(GameStateBase gameState = null)
        {
            if (gameState == null)
                gameState = GameState;

            List<int> indicePlayerSeat = ((GameEngine)GameEngine).ComputeEvents<List<int>>(gameState, CurrentPlayer.LocalId, Evaluation.GetWinnerLastRound);

            return PlayersSeats.FindAll(ps => indicePlayerSeat.Contains(ps.LocalId));
        }

        /// <summary>
        /// Get order of winners
        /// </summary>
        /// <param name="gameState">Game state whose the winners list is computed. By default the game state of the current game is used</param>
        /// <returns></returns>
        public List<IPlayerSeat> GetOrderedWinnerGame(GameStateBase gameState = null)
        {
            if (gameState == null)
                gameState = GameState;

            List<int> indicePlayerSeats = ((GameEngine)GameEngine).ComputeEvents<List<int>>(gameState, CurrentPlayer.LocalId, Evaluation.GetOrderedWinnerGame);

            List<IPlayerSeat> orderedWinners = null;

            if (indicePlayerSeats != null)
            {
                orderedWinners = new List<IPlayerSeat>();
                for (int i = 0; i < indicePlayerSeats.Count; i++)
                {
                    orderedWinners.Add(PlayersSeats.Find(ps => ps.LocalId == indicePlayerSeats[i]));
                }
            }

            return orderedWinners;
        }
        #endregion

        #region Create events
        /// <summary>
        /// Create *game initialized* event in game state. The random seed is planted here
        /// </summary>
        public virtual void CreateEventGameInitialized()
        {
            int seed = RandomManager.GetSeed();

            GameState.PlayerSeats = new List<IPlayerSeat>(PlayersSeats);
            GameState.Events.Add(
                new GameInitialized(
                    CurrentPlayer.LocalId,
                    seed,
                    PlayersSeats.Count));
            Log("Game initialized", new Hashtable() {
                { "local_id", CurrentPlayer.LocalId },
                { "seed", seed },
                { "player_seats", PlayersSeats.Count }
            });
        }

        /// <summary>
        /// Create *dice rolled* event in game state. Store the list of selected dice to roll
        /// </summary>
        /// <param name="diceIndice">List of selected dice to roll, by default all dice</param>
        public virtual void CreateEventDiceRolled(List<int> diceIndice = null)
        {
            if (diceIndice == null)
            {
                diceIndice = new List<int> { 0, 1, 2 };
            }

            GameState.Events.Add(new DiceRolled(CurrentPlayer.LocalId, diceIndice));
            Log("Dice rolled", new Hashtable() { { "local_id", CurrentPlayer.LocalId }, { "dice_indices", diceIndice } });
        }

        /// <summary>
        /// Create *round ended* event
        /// </summary>
        public virtual void CreateEventRoundEnded()
        {
            GameState.Events.Add(new RoundEnded(CurrentPlayer.LocalId));
            Log("round ended", new Hashtable() { { "local_id", CurrentPlayer.LocalId } });
        }

        /// <summary>
        /// Create *turn ended* event
        /// </summary>
        public virtual void CreateEventTurnEnded()
        {
            GameState.Events.Add(new TurnEnded(CurrentPlayer.LocalId));
            Log("turn ended", new Hashtable() { { "local_id", CurrentPlayer.LocalId } });
        }
        #endregion

        /// <summary>
        /// Virtual Dispose. Called by the view
        /// </summary>
        public virtual void Dispose()
        {
        }

        private void Log(string output, Hashtable logParams = null)
        {
            AsmoLogger.Debug("GameplayLogicBase", "Event : " + output, logParams);
        }
    }
}