﻿using AsmodeeDigital.Common.Plugin.Utils.Extensions;
using AsmodeeDigital.PlayReal.Plugin.Domain.GameState;
using AsmodeeDigital.PlayReal.Plugin.Domain.Message;
using AsmodeeDigital.PlayReal.Plugin.Domain.Players;
using AsmodeeDigital.PlayReal.Plugin.Exceptions;
using AsmodeeDigital.PlayReal.Plugin.Manager.Persistence;
using AsmodeeDigital.PlayReal.Plugin.Network;
using com.daysofwonder;
using com.daysofwonder.async;
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;

namespace AsmodeeDigital.PlayReal.Plugin.Logic.Gameplay
{
    /// <summary>
    /// Distant gameplay component
    /// </summary>
    /// <typeparam name="T">Game state type</typeparam>
    /// <typeparam name="U">Multicast message type</typeparam>
    public class DistantGameplayComponent<T, U> : ILogic, IDisposable
        where T : GameStateBase
        where U : IMessage
    {
        /// <summary>
        /// Persistent data
        /// </summary>
        public Persistence Persistence { get; set; }

        /// <summary>
        /// Unique communication component with DoW server
        /// </summary>
        public ServerConnection ServerConnection { get; set; }

        /// <summary>
        /// Instance of distant gameplay logic
        /// </summary>
        private IDistantGameplayLogic<T, U> _distantGameplayLogic;

        public DistantGameplayComponent(ServerConnection serverConnection, Persistence persistence, IDistantGameplayLogic<T, U> distantGameplayLogic)
        {
            ServerConnection = serverConnection;
            Persistence = persistence;
            _distantGameplayLogic = distantGameplayLogic;
        }

        /// <summary>
        /// Initialize all .Net events
        /// </summary>
        public void Init()
        {
            ServerConnection.GameStatusReportEvent += ServerConnection_GameStatusReportEvent;
            ServerConnection.ActionRequiredEvent += ServerConnection_ActionRequiredEvent;
            ServerConnection.PlayerPresenceUpdatedEvent += ServerConnection_PlayerPresenceUpdatedEvent;
            ServerConnection.GameAbortedEvent += ServerConnection_GameAbortedEvent;
            ServerConnection.PlayerReplacedEvent += ServerConnection_PlayerReplacedEvent;
            ServerConnection.ClientDataEvent += ServerConnection_ClientDataEvent;
            ServerConnection.GameOutcomeEvent += ServerConnection_GameOutcomeEvent;
            ServerConnection.PlayerTimeoutEvent += ServerConnection_PlayerTimeoutEvent;
        }

        /// <summary>
        /// Commit an action to DoW server
        /// </summary>
        /// <param name="nextPlayerTurn">If true, change the order of next player in game state</param>
        public void CommitAction(bool nextPlayerTurn = true)
        {
            if (nextPlayerTurn)
            {
                int first = _distantGameplayLogic.GameState.NextPlayers.First();
                _distantGameplayLogic.GameState.NextPlayers.RemoveAt(0);
                _distantGameplayLogic.GameState.NextPlayers.Add(first);
            }

            ServerConnection.CommitAction(
                Persistence.CurrentGameDetails.game_id,
                _distantGameplayLogic.CurrentPlayer.LocalId,
                _distantGameplayLogic.GameState.NextPlayers,
                _distantGameplayLogic.GameState);
        }

        /// <summary>
        /// Leave a game. Send a specific message depending on the game mode (synchrone or asynchrone)
        /// </summary>
        public void LeaveGame()
        {
            if (Persistence.CurrentGameDetails.configuration.game_mode == GameConfiguration.GameMode.ASYNCHRONOUS)
                ServerConnection.SwitchedToGameRequest();
            else
                ServerConnection.LeaveSyncGameRequest(Persistence.CurrentGameDetails.game_id);
        }

        /// <summary>
        /// Refresh player seats
        /// </summary>
        /// <param name="dowPlayers">List of DoW players</param>
        /// <param name="leftPlayers">List of players who left the game</param>
        private void RefreshPlayerSeat(List<Player> dowPlayers, List<int> leftPlayers)
        {
            foreach (Player dowPlayer in dowPlayers)
            {
                IPlayerSeat playerSeat = _distantGameplayLogic.PlayersSeats.Find(ps => ps.LocalId == dowPlayer.id);

                if (playerSeat == null)
                {
                    if (leftPlayers.Contains(dowPlayer.id))
                        playerSeat = new RobotPlayerSeat(dowPlayer);
                    else if (dowPlayer.w_w_w_id == Persistence.ConnectedPlayer.w_w_w_id)
                    {
                        playerSeat = new LocalPlayerSeat(dowPlayer);
                        Persistence.ConnectedPlayer.id = playerSeat.LocalId;
                    }
                    else
                        playerSeat = new DistantPlayerSeat(dowPlayer, true);

                    _distantGameplayLogic.PlayersSeats.Add(playerSeat);
                }
            }
            //---

            //---> Refresh CurrentPlayer
            if (_distantGameplayLogic.CurrentPlayer != null)
                _distantGameplayLogic.CurrentPlayer = _distantGameplayLogic.PlayersSeats.Find(ps => ps.DoWPlayer.w_w_w_id == _distantGameplayLogic.CurrentPlayer.DoWPlayer.w_w_w_id);
        }

        /// <summary>
        /// Deserialize game state and replace the old one
        /// </summary>
        /// <param name="data">Serialized game state</param>
        /// <returns>True if the game state has been replaced by the one passed in parameter</returns>
        private bool RetrieveGameState(byte[] data)
        {
            bool isGameStateReplaced = true;

            if (data != null && data.Length > 0)
            {
                using (MemoryStream ms = new MemoryStream(data))
                {
                    T gameState = default(T);
                    gameState = (T)ServerConnection.GameDataModel.Deserialize(ms, gameState, typeof(T));
                    gameState.CheckGameState();

                    //---> Replace the game state only if the one received from the server is newer than the one in memory
                    if (gameState.Events.Count >= _distantGameplayLogic.GameState.Events.Count)
                    {
                        _distantGameplayLogic.GameState = gameState;
                    }
                    else
                    {
                        isGameStateReplaced = false;
                    }
                }
            }

            return isGameStateReplaced;
        }

        #region Server Events
        /// <summary>
        /// Raised ServerConnection event - Refresh player seats and game state of current game
        /// </summary>
        /// <param name="statusReport">Status report</param>
        private void ServerConnection_GameStatusReportEvent(StatusReport statusReport)
        {
            if (statusReport.game_id == Persistence.CurrentGameDetails.game_id)
            {
                RefreshPlayerSeat(statusReport.players, statusReport.left_players);
                RetrieveGameState(statusReport.data);

                _distantGameplayLogic.UpdateNextPlayer(statusReport.next_player_ids);

                _distantGameplayLogic.RefreshGameStatus(statusReport);
            }
        }

        /// <summary>
        /// Raised ServerConnection event - Refresh player seats and game state of current game. Set next player
        /// </summary>
        /// <param name="actionRequiredRequest"></param>
        private void ServerConnection_ActionRequiredEvent(ActionRequiredRequest actionRequiredRequest)
        {
            try
            {
                if (actionRequiredRequest.game_id == Persistence.CurrentGameDetails.game_id)
                {
                    RefreshPlayerSeat(actionRequiredRequest.players, new List<int>());
                    RetrieveGameState(actionRequiredRequest.state);
                    _distantGameplayLogic.UpdateNextPlayer(actionRequiredRequest.next_player_ids);
                }
            }
            catch (DataCorruptedException e)
            {
                //TODO : End the game
                Debug.LogException(e);
            }
        }

        /// <summary>
        /// Raised ServerConnection event - Refresh player presence
        /// </summary>
        /// <param name="playerPresenceUpdateRequest"></param>
        private void ServerConnection_PlayerPresenceUpdatedEvent(PlayerPresenceUpdateRequest playerPresenceUpdateRequest)
        {
            if (_distantGameplayLogic.IsGameOverOrGameOutcome() || playerPresenceUpdateRequest.game_id != Persistence.CurrentGameDetails.game_id)
                return;

            foreach (IPlayerSeat playerSeat in _distantGameplayLogic.PlayersSeats)
            {
                DistantPlayerSeat distantPlayerSeat = playerSeat as DistantPlayerSeat;

                if (distantPlayerSeat != null)
                {
                    distantPlayerSeat.Online = playerPresenceUpdateRequest.in_game.Contains(distantPlayerSeat.LocalId);
                }
            }

            _distantGameplayLogic.PlayerPresenceUpdated();
        }

        /// <summary>
        /// Raised ServerConnection event - The current game is aborted
        /// </summary>
        /// <param name="gameAbortedRequest"></param>
        private void ServerConnection_GameAbortedEvent(GameAbortedRequest gameAbortedRequest)
        {
            if (_distantGameplayLogic.IsGameOverOrGameOutcome() || gameAbortedRequest.game_id != Persistence.CurrentGameDetails.game_id)
                return;

            _distantGameplayLogic.GameAborted(gameAbortedRequest);
        }

        /// <summary>
        /// Raised ServerConnection event - A player has been replaced by a robot or a robot has been replaced by a player
        /// </summary>
        /// <param name="playerReplacedRequest"></param>
        private void ServerConnection_PlayerReplacedEvent(PlayerReplacedRequest playerReplacedRequest)
        {
            //---> If local state of the game is game over, then nothing (Player is already disconnected from the game)
            if (_distantGameplayLogic.IsGameOverOrGameOutcome() || playerReplacedRequest.game_id != Persistence.CurrentGameDetails.game_id)
                return;

            IPlayerSeat playerSeat = _distantGameplayLogic.PlayersSeats.Find(p => p.LocalId == playerReplacedRequest.player_id);

            if (playerReplacedRequest.become_robot)
            {
                //--- Replace distant player by a robot
                RobotPlayerSeat robotPlayerSeat = new RobotPlayerSeat(playerSeat.DoWPlayer);
                _distantGameplayLogic.PlayersSeats.Remove(playerSeat);
                _distantGameplayLogic.PlayersSeats.Add(robotPlayerSeat);
                //---

                if (playerSeat is LocalPlayerSeat)
                {
                    _distantGameplayLogic.SetGameOver();

                    _distantGameplayLogic.LocalPlayerQuitGame(robotPlayerSeat, playerReplacedRequest.status);
                }
                else
                {
                    _distantGameplayLogic.OtherPlayerQuitGame(robotPlayerSeat, playerReplacedRequest.status);
                }
            }
            else if (!playerReplacedRequest.become_robot)
            {
                //--- Replace robot by distant player
                DistantPlayerSeat distantPlayerSeat = new DistantPlayerSeat(playerSeat.DoWPlayer, true);
                _distantGameplayLogic.PlayersSeats.Remove(playerSeat);
                _distantGameplayLogic.PlayersSeats.Add(distantPlayerSeat);
                //---

                _distantGameplayLogic.PlayerJoinGame(distantPlayerSeat);
            }
        }

        /// <summary>
        /// Raised ServerConnection event - Multicast data have been received
        /// </summary>
        /// <param name="clientDataRequest">Multicast data</param>
        private void ServerConnection_ClientDataEvent(ClientDataRequest clientDataRequest)
        {
            if (clientDataRequest.data != null && clientDataRequest.data.Length > 0)
            {
                using (MemoryStream ms = new MemoryStream(clientDataRequest.data))
                {
                    U message = default(U);
                    message = (U)ServerConnection.GameDataModel.Deserialize(ms, message, typeof(U));

                    _distantGameplayLogic.MessageReceived(message);
                }
            }
        }

        /// <summary>
        /// Raised ServerConnection event - A game has been finished
        /// </summary>
        /// <param name="gameOutcomeRequest"></param>
        private void ServerConnection_GameOutcomeEvent(GameOutcomeRequest gameOutcomeRequest)
        {
            if (_distantGameplayLogic.IsGameOverOrGameOutcome() || gameOutcomeRequest.game_id != Persistence.CurrentGameDetails.game_id)
                return;

            RetrieveGameState(gameOutcomeRequest.data);

            _distantGameplayLogic.SetGameOutcome(true);
        }

        /// <summary>
        /// Raised ServerConnection event - Ask current device to play AI for current player
        /// </summary>
        /// <param name="playerTimeoutRequest"></param>
        private void ServerConnection_PlayerTimeoutEvent(PlayerTimeoutRequest playerTimeoutRequest)
        {
            if (_distantGameplayLogic.IsGameOverOrGameOutcome() || playerTimeoutRequest.game_id != Persistence.CurrentGameDetails.game_id)
                return;

            bool isGameStateReplaced = RetrieveGameState(playerTimeoutRequest.state);

            if (isGameStateReplaced)
            {
                _distantGameplayLogic.UpdateNextPlayer(playerTimeoutRequest.next_player_ids, true);
            }
        }
        #endregion

        /// <summary>
        /// Dispose all events
        /// </summary>
        public void Dispose()
        {
            ServerConnection.GameStatusReportEvent -= ServerConnection_GameStatusReportEvent;
            ServerConnection.ActionRequiredEvent -= ServerConnection_ActionRequiredEvent;
            ServerConnection.PlayerPresenceUpdatedEvent -= ServerConnection_PlayerPresenceUpdatedEvent;
            ServerConnection.GameAbortedEvent -= ServerConnection_GameAbortedEvent;
            ServerConnection.PlayerReplacedEvent -= ServerConnection_PlayerReplacedEvent;
            ServerConnection.ClientDataEvent -= ServerConnection_ClientDataEvent;
            ServerConnection.GameOutcomeEvent -= ServerConnection_GameOutcomeEvent;
            ServerConnection.PlayerTimeoutEvent -= ServerConnection_PlayerTimeoutEvent;
        }
    }
}
