using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Ionic.Zip;
using MinecraftClient.Mapping;
using MinecraftClient.Protocol.Handlers;
using MinecraftClient.Protocol.Handlers.PacketPalettes;

namespace MinecraftClient.Protocol
{
    /// <summary>
    /// Record and save replay file that can be used by Replay mod
    /// </summary>
    public class ReplayHandler
    {
        public string ReplayFileName = @"whhhh.mcpr";
        public string ReplayFileDirectory = @"replay_recordings";
        public MetaDataHandler MetaData;
        public bool RecordRunning { get { return !cleanedUp; } }

        private readonly string recordingTmpFileName = @"recording.tmcpr";
        private readonly string temporaryCache = @"recording_cache";
        private readonly DataTypes dataTypes;
        private readonly PacketTypePalette packetType;
        private readonly int protocolVersion;
        private readonly BinaryWriter? recordStream;
        private readonly DateTime recordStartTime;
        private DateTime lastPacketTime;
        private bool prepareCleanUp = false;
        private bool cleanedUp = false;

        private static readonly bool logOutput = true;

        private int playerEntityID;
        private Guid playerUUID;
        private Location playerLastPosition;
        private float playerLastYaw;
        private float playerLastPitch;

        public ReplayHandler(int protocolVersion)
        {
            dataTypes = new DataTypes(protocolVersion);
            packetType = new PacketTypeHandler().GetTypeHandler(protocolVersion);
            this.protocolVersion = protocolVersion;

            if (!Directory.Exists(ReplayFileDirectory))
                Directory.CreateDirectory(ReplayFileDirectory);
            if (!Directory.Exists(temporaryCache))
                Directory.CreateDirectory(temporaryCache);

            recordStream = new BinaryWriter(new FileStream(Path.Combine(temporaryCache, recordingTmpFileName), FileMode.Create, FileAccess.ReadWrite));
            recordStartTime = DateTime.Now;

            MetaData = new MetaDataHandler
            {
                date = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalMilliseconds,
                protocol = protocolVersion,
                mcversion = ProtocolHandler.ProtocolVersion2MCVer(protocolVersion)
            };
            MetaData.SaveToFile();

            playerLastPosition = new Location(0, 0, 0);
            WriteLog("Start recording.");
        }

        public ReplayHandler(int protocolVersion, string serverName, string recordingDirectory = @"replay_recordings")
            : this(protocolVersion)
        {
            dataTypes = new DataTypes(protocolVersion);
            packetType = new PacketTypeHandler().GetTypeHandler(protocolVersion);

            MetaData.serverName = serverName;
            ReplayFileDirectory = recordingDirectory;
        }

        ~ReplayHandler()
        {
            OnShutDown();
        }

        public void SetClientEntityID(int entityID)
        {
            playerEntityID = entityID;
        }

        public void SetClientPlayerUUID(Guid uuid)
        {
            playerUUID = uuid;
        }

        #region File and stream handling

        public void CloseRecordStream()
        {
            try
            {
                recordStream!.Flush();
                recordStream.Close();
            }
            catch { }
        }

        /// <summary>
        /// Stop recording and save replay file. Should called once before program exit
        /// </summary>
        public void OnShutDown()
        {
            if (!cleanedUp)
            {
                prepareCleanUp = true;
                CloseRecordStream();
                CreateReplayFile();
                cleanedUp = true;
            }
        }

        /// <summary>
        /// Create the replay file for Replay mod to read
        /// </summary>
        public void CreateReplayFile()
        {
            string replayFileName = GetReplayDefaultName();
            CreateReplayFile(replayFileName);
        }

        /// <summary>
        /// Create the replay file for Replay mod to read
        /// </summary>
        /// <param name="replayFileName">Replay file name</param>
        public void CreateReplayFile(string replayFileName)
        {
            WriteLog("Creating replay file.");

            MetaData.duration = Convert.ToInt32((lastPacketTime - recordStartTime).TotalMilliseconds);
            MetaData.SaveToFile();

            using (Stream recordingFile = new FileStream(Path.Combine(temporaryCache, recordingTmpFileName), FileMode.Open))
            {
                using Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open);
                using ZipOutputStream zs = new(Path.Combine(ReplayFileDirectory, replayFileName));
                zs.PutNextEntry(recordingTmpFileName);
                recordingFile.CopyTo(zs);
                zs.PutNextEntry(MetaData.MetaDataFileName);
                metaDataFile.CopyTo(zs);
                zs.Close();
            }

            File.Delete(Path.Combine(temporaryCache, recordingTmpFileName));
            File.Delete(Path.Combine(temporaryCache, MetaData.MetaDataFileName));

            WriteLog("Replay file created.");
        }

        /// <summary>
        /// Create a backup replay file while recording
        /// </summary>
        /// <param name="replayFileName"></param>
        public void CreateBackupReplay(string replayFileName)
        {
            if (cleanedUp || prepareCleanUp)
                return;
            WriteDebugLog("Creating backup replay file.");

            MetaData.duration = Convert.ToInt32((lastPacketTime - recordStartTime).TotalMilliseconds);
            MetaData.SaveToFile();

            using (Stream metaDataFile = new FileStream(Path.Combine(temporaryCache, MetaData.MetaDataFileName), FileMode.Open))
            {
                using ZipOutputStream zs = new(replayFileName);
                zs.PutNextEntry(recordingTmpFileName);
                // .CopyTo() method start from stream current position
                // We need to reset position in order to get full content
                var lastPosition = recordStream!.BaseStream.Position;
                recordStream.BaseStream.Position = 0;
                recordStream.BaseStream.CopyTo(zs);
                recordStream.BaseStream.Position = lastPosition;

                zs.PutNextEntry(MetaData.MetaDataFileName);
                metaDataFile.CopyTo(zs);
                zs.Close();
            }

            WriteDebugLog("Backup replay file created.");
        }

        /// <summary>
        /// Get the default mcpr file name by current time
        /// </summary>
        /// <returns></returns>
        public string GetReplayDefaultName()
        {
            var now = DateTime.Now;
            return string.Format("{0}_{1}_{2}_{3}_{4}_{5}.mcpr", now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second); // yyyy_mm_dd_hh_mm_ss
        }

        #endregion

        #region Packet related method

        /// <summary>
        /// Add a packet from network
        /// </summary>
        /// <param name="packetID"></param>
        /// <param name="packetData"></param>
        /// <param name="isLogin"></param>
        /// <param name="isInbound"></param>
        public void AddPacket(int packetID, IEnumerable<byte> packetData, bool isLogin, bool isInbound)
        {
            try
            {
                if (isInbound)
                    HandleInBoundPacket(packetID, packetData, isLogin);
                else return;

                if (PacketShouldSave(packetID, isLogin, isInbound))
                    AddPacket(packetID, packetData);
            }
            catch (Exception e)
            {
                WriteDebugLog("Exception while adding packet: " + e.Message + "\n" + e.StackTrace);
            }
        }

        /// <summary>
        /// Add packet directly without checking (internal use only)
        /// </summary>
        /// <param name="packetID"></param>
        /// <param name="packetData"></param>
        private void AddPacket(int packetID, IEnumerable<byte> packetData)
        {
            lastPacketTime = DateTime.Now;
            // build raw packet
            // format: packetID + packetData
            List<byte> rawPacket = new();
            rawPacket.AddRange(DataTypes.GetVarInt(packetID).ToArray());
            rawPacket.AddRange(packetData.ToArray());
            // build format
            // format: timestamp + packetLength + RawPacket
            List<byte> line = new();
            int nowTime = Convert.ToInt32((lastPacketTime - recordStartTime).TotalMilliseconds);
            line.AddRange(BitConverter.GetBytes((Int32)nowTime).Reverse().ToArray());
            line.AddRange(BitConverter.GetBytes((Int32)rawPacket.Count).Reverse().ToArray());
            line.AddRange(rawPacket.ToArray());
            // Write out to the file
            recordStream!.Write(line.ToArray());
        }

        /// <summary>
        /// Add a player's UUID to the MetaData
        /// </summary>
        /// <param name="uuid"></param>
        /// <param name="name"></param>
        public void OnPlayerSpawn(Guid uuid)
        {
            // Metadata has a field for storing uuid for all players entered client render range
            MetaData.AddPlayerUUID(uuid);
        }

        /// <summary>
        /// Determine a packet should be saved
        /// </summary>
        /// <param name="packetID"></param>
        /// <param name="isLogin"></param>
        /// <param name="isInbound"></param>
        /// <returns></returns>
        private bool PacketShouldSave(int packetID, bool isLogin, bool isInbound)
        {
            if (!isInbound) // save inbound only
                return false;
            if (!isLogin) // save all play state packet
            {
                return true;
            }
            else
            { // is login
                if (packetID == 0x02) // login success
                {
                    return true;
                }
                else return false;
            }
        }

        /// <summary>
        /// Used to gather information needed
        /// </summary>
        /// <remarks>
        /// Also for converting client side packet to server side packet
        /// </remarks>
        /// <param name="packetID"></param>
        /// <param name="packetData"></param>
        /// <param name="isLogin"></param>
        private void HandleInBoundPacket(int packetID, IEnumerable<byte> packetData, bool isLogin)
        {
            Queue<byte> p = new(packetData);
            PacketTypesIn pType = packetType.GetIncommingTypeById(packetID);
            // Login success. Get player UUID
            if (isLogin && packetID == 0x02)
            {
                if (protocolVersion < Protocol18Handler.MC_1_16_Version)
                {
                    if (Guid.TryParse(dataTypes.ReadNextString(p), out Guid uuid))
                    {
                        SetClientPlayerUUID(uuid);
                        WriteDebugLog("User UUID: " + uuid.ToString());
                    }
                }
                else
                {
                    var uuid2 = dataTypes.ReadNextUUID(p);
                    SetClientPlayerUUID(uuid2);
                    WriteDebugLog("User UUID: " + uuid2.ToString());
                }
                return;
            }

            if (!isLogin && pType == PacketTypesIn.JoinGame)
            {
                // Get client player entity ID
                SetClientEntityID(dataTypes.ReadNextInt(p));
                return;
            }

            if (!isLogin && pType == PacketTypesIn.SpawnPlayer)
            {
                dataTypes.ReadNextVarInt(p);
                OnPlayerSpawn(dataTypes.ReadNextUUID(p));
                return;
            }

            // Get client player location for calculating movement delta later
            if (pType == PacketTypesIn.PlayerPositionAndLook)
            {
                double x = dataTypes.ReadNextDouble(p);
                double y = dataTypes.ReadNextDouble(p);
                double z = dataTypes.ReadNextDouble(p);
                float yaw = dataTypes.ReadNextFloat(p);
                float pitch = dataTypes.ReadNextFloat(p);
                byte locMask = dataTypes.ReadNextByte(p);

                playerLastPitch = pitch;
                playerLastYaw = yaw;
                if (protocolVersion >= Protocol18Handler.MC_1_8_Version)
                {
                    playerLastPosition.X = (locMask & 1 << 0) != 0 ? playerLastPosition.X + x : x;
                    playerLastPosition.Y = (locMask & 1 << 1) != 0 ? playerLastPosition.Y + y : y;
                    playerLastPosition.Z = (locMask & 1 << 2) != 0 ? playerLastPosition.Z + z : z;
                }
                else
                {
                    playerLastPosition.X = x;
                    playerLastPosition.Y = y;
                    playerLastPosition.Z = z;
                }
                return;
            }
        }

        /// <summary>
        /// Handle outbound packet (i.e. client player movement)
        /// </summary>
        /// <param name="packetID"></param>
        /// <param name="packetData"></param>
        /// <param name="isLogin"></param>
        private void HandleOutBoundPacket(int packetID, IEnumerable<byte> packetData, bool isLogin)
        {
            var packetType = this.packetType.GetOutgoingTypeById(packetID);
            if (packetType == PacketTypesOut.PlayerPosition
                || packetType == PacketTypesOut.PlayerPositionAndRotation)
            {
                // translate them to incoming entitymovement packet then save them
            }
        }

        private byte[] GetSpawnPlayerPacket(int entityID, Guid playerUUID, Location location, double pitch, double yaw)
        {
            List<byte> packet = new();
            packet.AddRange(DataTypes.GetVarInt(entityID));
            packet.AddRange(playerUUID.ToBigEndianBytes());
            packet.AddRange(dataTypes.GetDouble(location.X));
            packet.AddRange(dataTypes.GetDouble(location.Y));
            packet.AddRange(dataTypes.GetDouble(location.Z));
            packet.Add((byte)0);
            packet.Add((byte)0);
            return packet.ToArray();
        }

        #endregion

        #region Helper method

        private static void WriteLog(string t)
        {
            if (logOutput)
                ConsoleIO.WriteLogLine("[Replay] " + t);
        }

        private static void WriteDebugLog(string t)
        {
            if (Settings.Config.Logging.DebugMessages && logOutput)
                WriteLog(t);
        }

        #endregion
    }

    /// <summary>
    /// Handle MetaData used by Replay mod
    /// </summary>
    public class MetaDataHandler
    {
        public readonly string MetaDataFileName = @"metaData.json";
        public readonly string temporaryCache = @"recording_cache";

        public bool singlePlayer = false;
        public string? serverName;
        public int duration = 0; // duration of the whole replay
        public long date; // start time of the recording in unix timestamp milliseconds
        public string mcversion = "0.0"; // e.g. 1.15.2
        public string fileFormat = "MCPR";
        public int fileFormatVersion = 14; // 14 is what I found in metadata generated in 1.15.2 replay mod
        public int protocol;
        public string generator = "MCC"; // The program which generated the file (MCC have more popularity now :P)
        public int selfId = -1; // I saw -1 in medaData file generated by Replay mod. Not sure what is this for
        public List<string> players; // Array of UUIDs of all players which can be seen in the replay

        public MetaDataHandler()
        {
            players = new List<string>();
        }

        /// <summary>
        /// Add a player's UUID who appeared in the replay
        /// </summary>
        /// <param name="uuid"></param>
        public void AddPlayerUUID(Guid uuid)
        {
            players.Add(uuid.ToString());
        }

        /// <summary>
        /// Export metadata to JSON string
        /// </summary>
        /// <returns>JSON string</returns>
        public string ToJson()
        {
            return String.Concat(new[] { "{"
                , "\"singleplayer\":" , singlePlayer.ToString().ToLower() , ","
                , "\"serverName\":\"" , serverName , "\","
                , "\"duration\":" , duration.ToString() , ","
                , "\"date\":" , date.ToString() , ","
                , "\"mcversion\":\"" , mcversion , "\","
                , "\"fileFormat\":\"" , fileFormat , "\","
                , "\"fileFormatVersion\":" , fileFormatVersion.ToString() , ","
                , "\"protocol\":" , protocol.ToString() , ","
                , "\"generator\":\"" , generator , "\","
                , "\"selfId\":" , selfId.ToString() + ","
                , "\"player\":" , GetPlayersJsonArray()
                , "}"
            });
        }

        /// <summary>
        /// Save metadata to disk file
        /// </summary>
        public void SaveToFile()
        {
            File.WriteAllText(Path.Combine(temporaryCache, MetaDataFileName), ToJson());
        }

        /// <summary>
        /// Get players UUID JSON array string
        /// </summary>
        /// <returns></returns>
        private string GetPlayersJsonArray()
        {
            if (players.Count == 0)
                return "[]";

            // Place between brackets the comma-separated list of player names placed between quotes
            return String.Format("[{0}]",
                String.Join(",",
                    players.Select(player => String.Format("\"{0}\"", player))
                )
            );
        }
    }
}
