using Microsoft.Xna.Framework;
using System;
using System.Collections.Generic;
using Barotrauma.IO;
using System.Linq;
using System.Threading;

namespace Barotrauma.Networking
{
    class FileSender
    {
        public class FileTransferOut
        {
            public FileTransferStatus Status;
            
            public string FileName
            {
                get;
                private set;
            }

            public string FilePath
            {
                get;
                private set;
            }

            public FileTransferType FileType
            {
                get;
                private set;
            }

            public float Progress
            {
                get { return KnownReceivedOffset / (float)Data.Length; }
            }

            private float waitTimer;
            public float WaitTimer
            {
                get => waitTimer;
                set
                {
                    if (value > 0.0f)
                    {
                        //setting a wait timer means that network conditions
                        //aren't ideal, slow down the packet rate
                        PacketsPerUpdate = Math.Max(PacketsPerUpdate / 4.0f, 1.0f);
                    }
                    waitTimer = value;
                }
            }

            public static int MaxPacketsPerUpdate = 10;
            public float PacketsPerUpdate { get; set; } = 1.0f;

            public byte[] Data { get; }

            public bool Acknowledged;

            public int SentOffset
            {
                get;
                set;
            }

            public int KnownReceivedOffset;

            public NetworkConnection Connection { get; }

            public DateTime StartingTime { get; }

            public int ID;

            public FileTransferOut(NetworkConnection recipient, FileTransferType fileType, string filePath)
            {
                Connection = recipient;

                FileType = fileType;
                FilePath = filePath;
                FileName = Path.GetFileName(filePath);

                Acknowledged = false;
                SentOffset = 0;
                KnownReceivedOffset = 0;

                Status = FileTransferStatus.NotStarted;
                
                StartingTime = DateTime.Now;
                
                int maxRetries = 4;
                for (int i = 0; i <= maxRetries; i++)
                {
                    try
                    {
                        Data = File.ReadAllBytes(filePath, catchUnauthorizedAccessExceptions: false);
                    }
                    catch (System.IO.IOException e)
                    {
                        if (i >= maxRetries) { throw; }
                        DebugConsole.NewMessage("Failed to initiate a file transfer {" + e.Message + "}, retrying in 250 ms...", Color.Red);
                        Thread.Sleep(250);
                    }
                }
            }
        }

        const int MaxTransferCount = 16;
        const int MaxTransferCountPerRecipient = 5;
                
        public delegate void FileTransferDelegate(FileTransferOut fileStreamReceiver);
        public FileTransferDelegate OnStarted;
        public FileTransferDelegate OnEnded;

        private readonly List<FileTransferOut> activeTransfers;

        private readonly int chunkLen;

        private readonly ServerPeer peer;

        public static DateTime StartTime;
#if DEBUG
        public float ForceMinimumFileTransferDuration { get; set; }
#endif

        public IReadOnlyList<FileTransferOut> ActiveTransfers => activeTransfers;

        public FileSender(ServerPeer serverPeer, int mtu)
        {
            peer = serverPeer;
            chunkLen = mtu - 200;

            activeTransfers = new List<FileTransferOut>();
        }

        public FileTransferOut StartTransfer(NetworkConnection recipient, FileTransferType fileType, string filePath)
        {
            if (activeTransfers.Count >= MaxTransferCount)
            {
                return null;
            }

            if (activeTransfers.Count(t => t.Connection == recipient) > MaxTransferCountPerRecipient)
            {
                return null;
            }

            if (!File.Exists(filePath))
            {
                DebugConsole.ThrowError("Failed to initiate file transfer (file \"" + filePath + "\" not found).\n" + Environment.StackTrace);
                return null;
            }

            FileTransferOut transfer = null;
            try
            {
                transfer = new FileTransferOut(recipient, fileType, filePath)
                {
                    ID = 1
                };
                while (activeTransfers.Any(t => t.Connection == recipient && t.ID == transfer.ID))
                {
                    transfer.ID++;
                }
                activeTransfers.Add(transfer);
            }
            catch (Exception e)
            {
                DebugConsole.ThrowError("Failed to initiate file transfer", e);
                return null;
            }

            StartTime = DateTime.Now;

            OnStarted(transfer);
            GameMain.Server.LastClientListUpdateID++;

            return transfer;
        }

        public void Update(float deltaTime)
        {
            int numRemoved = activeTransfers.RemoveAll(t => t.Connection.Status != NetworkConnectionStatus.Connected);

            var endedTransfers = activeTransfers.FindAll(t => 
                t.Connection.Status != NetworkConnectionStatus.Connected ||
                t.Status == FileTransferStatus.Finished ||
                t.Status == FileTransferStatus.Canceled || 
                t.Status == FileTransferStatus.Error);

            foreach (FileTransferOut transfer in endedTransfers)
            {
                activeTransfers.Remove(transfer);
                OnEnded(transfer);
            }

            foreach (FileTransferOut transfer in activeTransfers)
            {
                transfer.WaitTimer -= deltaTime;
                if (transfer.WaitTimer > 0.0f) { continue; }
                Send(transfer);
            }

            if (numRemoved > 0 || endedTransfers.Count > 0)
            {
                GameMain.Server.LastClientListUpdateID++;
            }
        }

        private void Send(FileTransferOut transfer)
        {
            // send another part of the file
            IWriteMessage message;

            try
            {
                //first message; send length, file name etc
                //wait for acknowledgement before sending data
                if (!transfer.Acknowledged)
                {
                    message = new WriteOnlyMessage();
                    message.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER);

                    //if the recipient is the owner of the server (= a client running the server from the main exe)
                    //we don't need to send anything, the client can just read the file directly
                    if (transfer.Connection == GameMain.Server.OwnerConnection)
                    {
                        message.WriteByte((byte)FileTransferMessageType.TransferOnSameMachine);
                        message.WriteByte((byte)transfer.ID);
                        message.WriteByte((byte)transfer.FileType);
                        message.WriteString(transfer.FilePath);
                        peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable);
                        transfer.Status = FileTransferStatus.Finished;
                    }
                    else
                    {
                        message.WriteByte((byte)FileTransferMessageType.Initiate);
                        message.WriteByte((byte)transfer.ID);
                        message.WriteByte((byte)transfer.FileType);
                        //message.Write((ushort)chunkLen);
                        message.WriteInt32(transfer.Data.Length);
                        message.WriteString(transfer.FileName);
                        peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable);

                        transfer.Status = FileTransferStatus.Sending;

                        if (GameSettings.CurrentConfig.VerboseLogging)
                        {
                            DebugConsole.Log("Sending file transfer initiation message: ");
                            DebugConsole.Log("  File: " + transfer.FileName);
                            DebugConsole.Log("  Size: " + transfer.Data.Length);
                            DebugConsole.Log("  ID: " + transfer.ID);
                        }
                    }
                    transfer.WaitTimer = 0.1f;
                    return;
                }

                for (int i = 0; i < Math.Floor(transfer.PacketsPerUpdate); i++)
                {
                    long remaining = transfer.Data.Length - transfer.SentOffset;
#if DEBUG
                    bool stalling = false;
                    float elapsedTime = (float)(DateTime.Now - StartTime).TotalSeconds;
                    if (elapsedTime < ForceMinimumFileTransferDuration)
                    {
                        int remainingChunks = (int)Math.Max(remaining / chunkLen, 1);
                        transfer.WaitTimer =
                            Math.Max(transfer.WaitTimer, (ForceMinimumFileTransferDuration - elapsedTime) / remainingChunks);
                        if (remainingChunks <= 1) { break; }
                    }
#endif
                    int sendByteCount = remaining > chunkLen ? chunkLen : (int)remaining;
                    
                    message = new WriteOnlyMessage();
                    message.WriteByte((byte)ServerPacketHeader.FILE_TRANSFER);
                    message.WriteByte((byte)FileTransferMessageType.Data);

                    message.WriteByte((byte)transfer.ID);
                    message.WriteInt32(transfer.SentOffset);

                    message.WriteUInt16((ushort)sendByteCount);
                    int chunkDestPos = message.BytePosition;
                    message.BitPosition += sendByteCount * 8;
                    message.LengthBits = Math.Max(message.LengthBits, message.BitPosition);
                    Array.Copy(transfer.Data, transfer.SentOffset, message.Buffer, chunkDestPos, sendByteCount);

                    transfer.SentOffset += sendByteCount;

                    peer.Send(message, transfer.Connection, DeliveryMethod.Unreliable, compressPastThreshold: false);

                    if (GameSettings.CurrentConfig.VerboseLogging)
                    {
                        DebugConsole.Log($"Sending {sendByteCount} bytes of the file {transfer.FileName} ({transfer.SentOffset / 1000}/{transfer.Data.Length / 1000} kB sent)");
                    }
                    
                    if (transfer.SentOffset >= transfer.Data.Length)
                    {
                        transfer.SentOffset = transfer.KnownReceivedOffset;
                        transfer.WaitTimer = 1.0f;
                    }

                    //try to increase the packet rate so large files get sent faster,
                    //this gets reset when packet loss or disorder sets in
                    transfer.PacketsPerUpdate = Math.Min(FileTransferOut.MaxPacketsPerUpdate,
                        transfer.PacketsPerUpdate + 0.05f);
#if DEBUG
                    if (stalling) { break; }
#endif
                }
            }

            catch (Exception e)
            {
                DebugConsole.ThrowError("FileSender threw an exception when trying to send data", e);
                GameAnalyticsManager.AddErrorEventOnce(
                    "FileSender.Update:Exception",
                    GameAnalyticsManager.ErrorSeverity.Error,
                    "FileSender threw an exception when trying to send data:\n" + e.Message + "\n" + e.StackTrace.CleanupStackTrace());
                transfer.Status = FileTransferStatus.Error;
                return;
            }
        }

        public void CancelTransfer(FileTransferOut transfer)
        {
            transfer.Status = FileTransferStatus.Canceled;
            activeTransfers.Remove(transfer);

            OnEnded(transfer);

            GameMain.Server.SendCancelTransferMsg(transfer);
        }

        public void ReadFileRequest(IReadMessage inc, Client client)
        {
            FileTransferMessageType messageType = (FileTransferMessageType)inc.ReadByte();

            if (messageType == FileTransferMessageType.Cancel)
            {
                byte transferId = inc.ReadByte();
                var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId);
                if (matchingTransfer != null) { CancelTransfer(matchingTransfer); }
                return;
            }
            else if (messageType == FileTransferMessageType.Data)
            {
                byte transferId = inc.ReadByte();
                var matchingTransfer = activeTransfers.Find(t => t.Connection == inc.Sender && t.ID == transferId);
                if (matchingTransfer != null)
                {
                    matchingTransfer.Acknowledged = true;
                    int expecting = inc.ReadInt32(); //the offset the client is waiting for
                    int lastSeen = Math.Min(matchingTransfer.SentOffset, inc.ReadInt32()); //the last offset the client got from us
                    matchingTransfer.KnownReceivedOffset = Math.Max(expecting, matchingTransfer.KnownReceivedOffset);
                    if (matchingTransfer.SentOffset < matchingTransfer.KnownReceivedOffset)
                    {
                        matchingTransfer.WaitTimer = 0.0f;
                        matchingTransfer.SentOffset = matchingTransfer.KnownReceivedOffset; 
                    }
                    
                    if (lastSeen - matchingTransfer.KnownReceivedOffset >= chunkLen * 10 ||
                        matchingTransfer.SentOffset >= matchingTransfer.Data.Length)
                    {
                        matchingTransfer.SentOffset = matchingTransfer.KnownReceivedOffset;
                        matchingTransfer.WaitTimer = 1.0f;
                    }

                    if (matchingTransfer.KnownReceivedOffset >= matchingTransfer.Data.Length)
                    {
                        matchingTransfer.Status = FileTransferStatus.Finished;
                        DebugConsole.Log($"Finished sending file \"{matchingTransfer.FilePath}\" to \"{client.Name}\". Took {DateTime.Now - StartTime}");
                    }
                }
                return;
            }

            FileTransferType fileType = (FileTransferType)inc.ReadByte();
            switch (fileType)
            {
                case FileTransferType.Submarine:
                    string fileName = inc.ReadString();
                    string fileHash = inc.ReadString();
                    var requestedSubmarine = SubmarineInfo.SavedSubmarines.FirstOrDefault(s => s.Name == fileName && s.MD5Hash.StringRepresentation == fileHash);

                    DebugConsole.Log($"Received a submarine file request from \"{client.Name}\" ({fileName}).");

                    if (requestedSubmarine != null)
                    {
                        if (activeTransfers.Any(t => t.Connection == inc.Sender && t.FilePath == requestedSubmarine.FilePath))
                        {
                            DebugConsole.Log($"Ignoring a submarine file request from \"{client.Name}\" ({fileName}) - already transferring.");
                            return;
                        }
                        StartTransfer(inc.Sender, FileTransferType.Submarine, requestedSubmarine.FilePath);
                    }
                    break;
                case FileTransferType.CampaignSave:
                    if (GameMain.GameSession != null &&
                        !ActiveTransfers.Any(t => t.Connection == inc.Sender && t.FileType == FileTransferType.CampaignSave))
                    {
                        StartTransfer(inc.Sender, FileTransferType.CampaignSave, GameMain.GameSession.DataPath.LoadPath);
                        if (GameMain.GameSession?.GameMode is MultiPlayerCampaign campaign)
                        {
                            client.LastCampaignSaveSendTime = (campaign.LastSaveID, (float)Lidgren.Network.NetTime.Now);
                        }
                    }
                    break;
                case FileTransferType.Mod:
                    string modName = inc.ReadString();
                    Md5Hash modHash = Md5Hash.StringAsHash(inc.ReadString());

                    DebugConsole.Log($"Received a mod file request from \"{client.Name}\" ({modName}).");

                    if (!GameMain.Server.ServerSettings.AllowModDownloads) { return; }
                    if (!(GameMain.Server.ModSender is { Ready: true })) { return; }
                    
                    ContentPackage mod = ContentPackageManager.AllPackages.FirstOrDefault(p => p.Hash.Equals(modHash));

                    if (mod is null) { return; }
                    
                    string modCompressedPath = ModSender.GetCompressedModPath(mod);
                    if (!File.Exists(modCompressedPath)) { return; }

                    if (activeTransfers.Any(t => t.Connection == inc.Sender && t.FilePath == modCompressedPath))
                    {
                        DebugConsole.Log($"Ignoring a mod file request from \"{client.Name}\" ({modName}) - already transferring.");
                        return;
                    }

                    StartTransfer(inc.Sender, FileTransferType.Mod, modCompressedPath);
                    
                    break;
            }
        }

    }
}
