using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Galaxy.Api;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using StardewModdingAPI.Events;
using StardewModdingAPI.Framework.Events;
using StardewModdingAPI.Framework.Networking;
using StardewModdingAPI.Framework.Reflection;
using StardewModdingAPI.Toolkit.Serialization;
using StardewValley;
using StardewValley.Network;
using StardewValley.SDKs;

namespace StardewModdingAPI.Framework
{
    /// <summary>SMAPI's implementation of the game's core multiplayer logic.</summary>
    /// <remarks>
    /// SMAPI syncs mod context to all players through the host as such:
    ///   1. Farmhand sends ModContext + PlayerIntro.
    ///   2. If host receives ModContext: it stores the context, replies with known contexts, and forwards it to other farmhands.
    ///   3. If host receives PlayerIntro before ModContext: it stores a 'vanilla player' context, and forwards it to other farmhands.
    ///   4. If farmhand receives ModContext: it stores it.
    ///   5. If farmhand receives ServerIntro without a preceding ModContext: it stores a 'vanilla host' context.
    ///   6. If farmhand receives PlayerIntro without a preceding ModContext AND it's not the host peer: it stores a 'vanilla player' context.
    ///
    /// Once a farmhand/server stored a context, messages can be sent to that player through the SMAPI APIs.
    /// </remarks>
    internal class SMultiplayer : Multiplayer
    {
        /*********
        ** Fields
        *********/
        /// <summary>Encapsulates monitoring and logging.</summary>
        private readonly IMonitor Monitor;

        /// <summary>Tracks the installed mods.</summary>
        private readonly ModRegistry ModRegistry;

        /// <summary>Encapsulates SMAPI's JSON file parsing.</summary>
        private readonly JsonHelper JsonHelper;

        /// <summary>Simplifies access to private code.</summary>
        private readonly Reflector Reflection;

        /// <summary>Manages SMAPI events.</summary>
        private readonly EventManager EventManager;

        /// <summary>A callback to invoke when a mod message is received.</summary>
        private readonly Action<ModMessageModel> OnModMessageReceived;

        /// <summary>Whether to log network traffic.</summary>
        private readonly bool LogNetworkTraffic;


        /*********
        ** Accessors
        *********/
        /// <summary>The metadata for each connected peer.</summary>
        public IDictionary<long, MultiplayerPeer> Peers { get; } = new Dictionary<long, MultiplayerPeer>();

        /// <summary>The metadata for the host player, if the current player is a farmhand.</summary>
        public MultiplayerPeer HostPeer;


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="monitor">Encapsulates monitoring and logging.</param>
        /// <param name="eventManager">Manages SMAPI events.</param>
        /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param>
        /// <param name="modRegistry">Tracks the installed mods.</param>
        /// <param name="reflection">Simplifies access to private code.</param>
        /// <param name="onModMessageReceived">A callback to invoke when a mod message is received.</param>
        /// <param name="logNetworkTraffic">Whether to log network traffic.</param>
        public SMultiplayer(IMonitor monitor, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, Reflector reflection, Action<ModMessageModel> onModMessageReceived, bool logNetworkTraffic)
        {
            this.Monitor = monitor;
            this.EventManager = eventManager;
            this.JsonHelper = jsonHelper;
            this.ModRegistry = modRegistry;
            this.Reflection = reflection;
            this.OnModMessageReceived = onModMessageReceived;
            this.LogNetworkTraffic = logNetworkTraffic;
        }

        /// <summary>Perform cleanup needed when a multiplayer session ends.</summary>
        public void CleanupOnMultiplayerExit()
        {
            this.Peers.Clear();
            this.HostPeer = null;
        }

        /// <summary>Initialize a client before the game connects to a remote server.</summary>
        /// <param name="client">The client to initialize.</param>
        public override Client InitClient(Client client)
        {
            switch (client)
            {
                case LidgrenClient _:
                    {
                        string address = this.Reflection.GetField<string>(client, "address").GetValue();
                        return new SLidgrenClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage);
                    }

                case GalaxyNetClient _:
                    {
                        GalaxyID address = this.Reflection.GetField<GalaxyID>(client, "lobbyId").GetValue();
                        return new SGalaxyNetClient(address, this.OnClientProcessingMessage, this.OnClientSendingMessage);
                    }

                default:
                    this.Monitor.Log($"Unknown multiplayer client type: {client.GetType().AssemblyQualifiedName}", LogLevel.Trace);
                    return client;
            }
        }

        /// <summary>Initialize a server before the game connects to an incoming player.</summary>
        /// <param name="server">The server to initialize.</param>
        public override Server InitServer(Server server)
        {
            switch (server)
            {
                case LidgrenServer _:
                    {
                        IGameServer gameServer = this.Reflection.GetField<IGameServer>(server, "gameServer").GetValue();
                        return new SLidgrenServer(gameServer, this, this.OnServerProcessingMessage);
                    }

                case GalaxyNetServer _:
                    {
                        IGameServer gameServer = this.Reflection.GetField<IGameServer>(server, "gameServer").GetValue();
                        return new SGalaxyNetServer(gameServer, this, this.OnServerProcessingMessage);
                    }

                default:
                    this.Monitor.Log($"Unknown multiplayer server type: {server.GetType().AssemblyQualifiedName}", LogLevel.Trace);
                    return server;
            }
        }

        /// <summary>A callback raised when sending a message as a farmhand.</summary>
        /// <param name="message">The message being sent.</param>
        /// <param name="sendMessage">Send an arbitrary message through the client.</param>
        /// <param name="resume">Resume sending the underlying message.</param>
        protected void OnClientSendingMessage(OutgoingMessage message, Action<OutgoingMessage> sendMessage, Action resume)
        {
            if (this.LogNetworkTraffic)
                this.Monitor.Log($"CLIENT SEND {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace);

            switch (message.MessageType)
            {
                // sync mod context (step 1)
                case (byte)MessageType.PlayerIntroduction:
                    sendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields()));
                    resume();
                    break;

                // run default logic
                default:
                    resume();
                    break;
            }
        }

        /// <summary>Process an incoming network message as the host player.</summary>
        /// <param name="message">The message to process.</param>
        /// <param name="sendMessage">A method which sends the given message to the client.</param>
        /// <param name="resume">Process the message using the game's default logic.</param>
        public void OnServerProcessingMessage(IncomingMessage message, Action<OutgoingMessage> sendMessage, Action resume)
        {
            if (this.LogNetworkTraffic)
                this.Monitor.Log($"SERVER RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace);

            switch (message.MessageType)
            {
                // sync mod context (step 2)
                case (byte)MessageType.ModContext:
                    {
                        // parse message
                        RemoteContextModel model = this.ReadContext(message.Reader);
                        this.Monitor.Log($"Received context for farmhand {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace);

                        // store peer
                        MultiplayerPeer newPeer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: false);
                        if (this.Peers.ContainsKey(message.FarmerID))
                        {
                            this.Monitor.Log($"Received mod context from farmhand {message.FarmerID}, but the game didn't see them disconnect. This may indicate issues with the network connection.", LogLevel.Info);
                            this.Peers.Remove(message.FarmerID);
                            return;
                        }
                        this.AddPeer(newPeer, canBeHost: false, raiseEvent: false);

                        // reply with own context
                        this.Monitor.VerboseLog("   Replying with host context...");
                        newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, Game1.player.UniqueMultiplayerID, this.GetContextSyncMessageFields()));

                        // reply with other players' context
                        foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID))
                        {
                            this.Monitor.VerboseLog($"   Replying with context for player {otherPeer.PlayerID}...");
                            newPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, otherPeer.PlayerID, this.GetContextSyncMessageFields(otherPeer)));
                        }

                        // forward to other peers
                        if (this.Peers.Count > 1)
                        {
                            object[] fields = this.GetContextSyncMessageFields(newPeer);
                            foreach (MultiplayerPeer otherPeer in this.Peers.Values.Where(p => p.PlayerID != newPeer.PlayerID))
                            {
                                this.Monitor.VerboseLog($"   Forwarding context to player {otherPeer.PlayerID}...");
                                otherPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModContext, newPeer.PlayerID, fields));
                            }
                        }

                        // raise event
                        this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(newPeer));
                    }
                    break;

                // handle player intro
                case (byte)MessageType.PlayerIntroduction:
                    // store peer if new
                    if (!this.Peers.ContainsKey(message.FarmerID))
                    {
                        this.Monitor.Log($"Received connection for vanilla player {message.FarmerID}.", LogLevel.Trace);
                        MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: false);
                        this.AddPeer(peer, canBeHost: false);
                    }

                    resume();
                    break;

                // handle mod message
                case (byte)MessageType.ModMessage:
                    this.ReceiveModMessage(message);
                    break;

                default:
                    resume();
                    break;
            }
        }

        /// <summary>Process an incoming network message as a farmhand.</summary>
        /// <param name="message">The message to process.</param>
        /// <param name="sendMessage">Send an arbitrary message through the client.</param>
        /// <param name="resume">Resume processing the message using the game's default logic.</param>
        /// <returns>Returns whether the message was handled.</returns>
        public void OnClientProcessingMessage(IncomingMessage message, Action<OutgoingMessage> sendMessage, Action resume)
        {
            if (this.LogNetworkTraffic)
                this.Monitor.Log($"CLIENT RECV {(MessageType)message.MessageType} {message.FarmerID}", LogLevel.Trace);

            switch (message.MessageType)
            {
                // mod context sync (step 4)
                case (byte)MessageType.ModContext:
                    {
                        // parse message
                        RemoteContextModel model = this.ReadContext(message.Reader);
                        this.Monitor.Log($"Received context for {(model?.IsHost == true ? "host" : "farmhand")} {message.FarmerID} running {(model != null ? $"SMAPI {model.ApiVersion} with {model.Mods.Length} mods" : "vanilla")}.", LogLevel.Trace);

                        // store peer
                        MultiplayerPeer peer = new MultiplayerPeer(message.FarmerID, model, sendMessage, isHost: model?.IsHost ?? this.HostPeer == null);
                        if (peer.IsHost && this.HostPeer != null)
                        {
                            this.Monitor.Log($"Rejected mod context from host player {peer.PlayerID}: already received host data from {(peer.PlayerID == this.HostPeer.PlayerID ? "that player" : $"player {peer.PlayerID}")}.", LogLevel.Error);
                            return;
                        }
                        this.AddPeer(peer, canBeHost: true);
                    }
                    break;

                // handle server intro
                case (byte)MessageType.ServerIntroduction:
                    {
                        // store peer
                        if (!this.Peers.ContainsKey(message.FarmerID) && this.HostPeer == null)
                        {
                            this.Monitor.Log($"Received connection for vanilla host {message.FarmerID}.", LogLevel.Trace);
                            this.AddPeer(new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: true), canBeHost: false);
                        }
                        resume();
                        break;
                    }

                // handle player intro
                case (byte)MessageType.PlayerIntroduction:
                    {
                        // store peer
                        if (!this.Peers.TryGetValue(message.FarmerID, out MultiplayerPeer peer))
                        {
                            peer = new MultiplayerPeer(message.FarmerID, null, sendMessage, isHost: this.HostPeer == null);
                            this.Monitor.Log($"Received connection for vanilla {(peer.IsHost ? "host" : "farmhand")} {message.FarmerID}.", LogLevel.Trace);
                            this.AddPeer(peer, canBeHost: true);
                        }

                        resume();
                        break;
                    }

                // handle mod message
                case (byte)MessageType.ModMessage:
                    this.ReceiveModMessage(message);
                    break;

                default:
                    resume();
                    break;
            }
        }

        /// <summary>Remove players who are disconnecting.</summary>
        protected override void removeDisconnectedFarmers()
        {
            foreach (long playerID in this.disconnectingFarmers)
            {
                if (this.Peers.TryGetValue(playerID, out MultiplayerPeer peer))
                {
                    this.Monitor.Log($"Player quit: {playerID}", LogLevel.Trace);
                    this.Peers.Remove(playerID);
                    this.EventManager.PeerDisconnected.Raise(new PeerDisconnectedEventArgs(peer));
                }
            }

            base.removeDisconnectedFarmers();
        }

        /// <summary>Broadcast a mod message to matching players.</summary>
        /// <param name="message">The data to send over the network.</param>
        /// <param name="messageType">A message type which receiving mods can use to decide whether it's the one they want to handle, like <c>SetPlayerLocation</c>. This doesn't need to be globally unique, since mods should check the originating mod ID.</param>
        /// <param name="fromModID">The unique ID of the mod sending the message.</param>
        /// <param name="toModIDs">The mod IDs which should receive the message on the destination computers, or <c>null</c> for all mods. Specifying mod IDs is recommended to improve performance, unless it's a general-purpose broadcast.</param>
        /// <param name="toPlayerIDs">The <see cref="Farmer.UniqueMultiplayerID" /> values for the players who should receive the message, or <c>null</c> for all players. If you don't need to broadcast to all players, specifying player IDs is recommended to reduce latency.</param>
        public void BroadcastModMessage<TMessage>(TMessage message, string messageType, string fromModID, string[] toModIDs, long[] toPlayerIDs)
        {
            // validate
            if (message == null)
                throw new ArgumentNullException(nameof(message));
            if (string.IsNullOrWhiteSpace(messageType))
                throw new ArgumentNullException(nameof(messageType));
            if (string.IsNullOrWhiteSpace(fromModID))
                throw new ArgumentNullException(nameof(fromModID));
            if (!this.Peers.Any())
            {
                this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: not connected to any players.");
                return;
            }

            // filter player IDs
            HashSet<long> playerIDs = null;
            if (toPlayerIDs != null && toPlayerIDs.Any())
            {
                playerIDs = new HashSet<long>(toPlayerIDs);
                playerIDs.RemoveWhere(id => !this.Peers.ContainsKey(id));
                if (!playerIDs.Any())
                {
                    this.Monitor.VerboseLog($"Ignored '{messageType}' broadcast from mod {fromModID}: none of the specified player IDs are connected.");
                    return;
                }
            }

            // get data to send
            ModMessageModel model = new ModMessageModel(
                fromPlayerID: Game1.player.UniqueMultiplayerID,
                fromModID: fromModID,
                toModIDs: toModIDs,
                toPlayerIDs: playerIDs?.ToArray(),
                type: messageType,
                data: JToken.FromObject(message)
            );
            string data = JsonConvert.SerializeObject(model, Formatting.None);

            // log message
            if (this.LogNetworkTraffic)
                this.Monitor.Log($"Broadcasting '{messageType}' message: {data}.", LogLevel.Trace);

            // send message
            if (Context.IsMainPlayer)
            {
                foreach (MultiplayerPeer peer in this.Peers.Values)
                {
                    if (playerIDs == null || playerIDs.Contains(peer.PlayerID))
                    {
                        model.ToPlayerIDs = new[] { peer.PlayerID };
                        peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, data));
                    }
                }
            }
            else if (this.HostPeer != null && this.HostPeer.HasSmapi)
                this.HostPeer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, this.HostPeer.PlayerID, data));
            else
                this.Monitor.VerboseLog("  Can't send message because no valid connections were found.");

        }


        /*********
        ** Private methods
        *********/
        /// <summary>Save a received peer.</summary>
        /// <param name="peer">The peer to add.</param>
        /// <param name="canBeHost">Whether to track the peer as the host if applicable.</param>
        /// <param name="raiseEvent">Whether to raise the <see cref="Events.EventManager.PeerContextReceived"/> event.</param>
        private void AddPeer(MultiplayerPeer peer, bool canBeHost, bool raiseEvent = true)
        {
            // store
            this.Peers[peer.PlayerID] = peer;
            if (canBeHost && peer.IsHost)
                this.HostPeer = peer;

            // raise event
            if (raiseEvent)
                this.EventManager.PeerContextReceived.Raise(new PeerContextReceivedEventArgs(peer));
        }

        /// <summary>Read the metadata context for a player.</summary>
        /// <param name="reader">The stream reader.</param>
        private RemoteContextModel ReadContext(BinaryReader reader)
        {
            string data = reader.ReadString();
            RemoteContextModel model = this.JsonHelper.Deserialize<RemoteContextModel>(data);
            return model.ApiVersion != null
                ? model
                : null; // no data available for unmodded players
        }

        /// <summary>Receive a mod message sent from another player's mods.</summary>
        /// <param name="message">The raw message to parse.</param>
        private void ReceiveModMessage(IncomingMessage message)
        {
            // parse message
            string json = message.Reader.ReadString();
            ModMessageModel model = this.JsonHelper.Deserialize<ModMessageModel>(json);
            HashSet<long> playerIDs = new HashSet<long>(model.ToPlayerIDs ?? this.GetKnownPlayerIDs());
            if (this.LogNetworkTraffic)
                this.Monitor.Log($"Received message: {json}.", LogLevel.Trace);

            // notify local mods
            if (playerIDs.Contains(Game1.player.UniqueMultiplayerID))
                this.OnModMessageReceived(model);

            // forward to other players
            if (Context.IsMainPlayer && playerIDs.Any(p => p != Game1.player.UniqueMultiplayerID))
            {
                ModMessageModel newModel = new ModMessageModel(model);
                foreach (long playerID in playerIDs)
                {
                    if (playerID != Game1.player.UniqueMultiplayerID && playerID != model.FromPlayerID && this.Peers.TryGetValue(playerID, out MultiplayerPeer peer))
                    {
                        newModel.ToPlayerIDs = new[] { peer.PlayerID };
                        this.Monitor.VerboseLog($"  Forwarding message to player {peer.PlayerID}.");
                        peer.SendMessage(new OutgoingMessage((byte)MessageType.ModMessage, peer.PlayerID, this.JsonHelper.Serialize(newModel, Formatting.None)));
                    }
                }
            }
        }

        /// <summary>Get all connected player IDs, including the current player.</summary>
        private IEnumerable<long> GetKnownPlayerIDs()
        {
            yield return Game1.player.UniqueMultiplayerID;
            foreach (long peerID in this.Peers.Keys)
                yield return peerID;
        }

        /// <summary>Get the fields to include in a context sync message sent to other players.</summary>
        private object[] GetContextSyncMessageFields()
        {
            RemoteContextModel model = new RemoteContextModel
            {
                IsHost = Context.IsWorldReady && Context.IsMainPlayer,
                Platform = Constants.TargetPlatform,
                ApiVersion = Constants.ApiVersion,
                GameVersion = Constants.GameVersion,
                Mods = this.ModRegistry
                    .GetAll()
                    .Select(mod => new RemoteContextModModel
                    {
                        ID = mod.Manifest.UniqueID,
                        Name = mod.Manifest.Name,
                        Version = mod.Manifest.Version
                    })
                    .ToArray()
            };

            return new object[] { this.JsonHelper.Serialize(model, Formatting.None) };
        }

        /// <summary>Get the fields to include in a context sync message sent to other players.</summary>
        /// <param name="peer">The peer whose data to represent.</param>
        private object[] GetContextSyncMessageFields(IMultiplayerPeer peer)
        {
            if (!peer.HasSmapi)
                return new object[] { "{}" };

            RemoteContextModel model = new RemoteContextModel
            {
                IsHost = peer.IsHost,
                Platform = peer.Platform.Value,
                ApiVersion = peer.ApiVersion,
                GameVersion = peer.GameVersion,
                Mods = peer.Mods
                    .Select(mod => new RemoteContextModModel
                    {
                        ID = mod.ID,
                        Name = mod.Name,
                        Version = mod.Version
                    })
                    .ToArray()
            };

            return new object[] { this.JsonHelper.Serialize(model, Formatting.None) };
        }
    }
}