using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace AnotherReplayReader
{
    internal sealed class Player
    {
        public static readonly IReadOnlyDictionary<string, string> ComputerNames = new Dictionary<string, string>
        {
            { "E", "简单" },
            { "M", "中等" },
            { "H", "困难" },
            { "B", "凶残" },
        };

        public string PlayerName { get; private set; }
        public uint PlayerIP { get; private set; }
        public int FactionID { get; private set; }
        public int Team { get; private set; }

        public Player(string[] playerEntry)
        {
            var isComputer = playerEntry[0][0] == 'C';

            PlayerName = playerEntry[0].Substring(1);

            if (isComputer)
            {
                PlayerName = ComputerNames[PlayerName];
                PlayerIP = 0;
                FactionID = int.Parse(playerEntry[2]);
                Team = int.Parse(playerEntry[4]);
            }
            else
            {
                PlayerIP = uint.Parse(playerEntry[1], System.Globalization.NumberStyles.HexNumber);
                FactionID = int.Parse(playerEntry[5]);
                Team = int.Parse(playerEntry[7]);
            }
            
            
        }
    }

    internal enum ReplayType
    {
        Skirmish,
        Lan,
        Online
    }

    internal sealed class ReplayChunk
    {
        public uint TimeCode { get; private set; }
        public byte Type { get; private set; }
        public byte[] Data { get; private set; }

        public ReplayChunk(uint timeCode, BinaryReader reader)
        {
            TimeCode = timeCode; // reader.ReadUInt32();
            Type = reader.ReadByte();
            var chunkSize = reader.ReadInt32();
            Data = reader.ReadBytes(chunkSize);
            if(reader.ReadInt32() != 0)
            {
                throw new InvalidDataException("Replay Chunk not ended with zero");
            }
        }
    }

    internal enum ReplayFooterOption
    {
        SeekToFooter,
        CurrentlyAtFooter,
    }

    internal sealed class ReplayFooter
    {
        public const uint Terminator = 0x7FFFFFFF;
        public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER");
        public uint FinalTimeCode { get; private set; }
        public byte[] Data { get; private set; }

        public ReplayFooter(BinaryReader reader, ReplayFooterOption option)
        {
            var currentPosition = reader.BaseStream.Position;
            reader.BaseStream.Seek(-4, SeekOrigin.End);
            var footerLength = reader.ReadInt32();

            if (option == ReplayFooterOption.SeekToFooter)
            {
                currentPosition = reader.BaseStream.Length - footerLength;
            }

            reader.BaseStream.Seek(currentPosition, SeekOrigin.Begin);
            var footer = reader.ReadBytes(footerLength);

            if(footer.Length != footerLength || reader.BaseStream.Position != reader.BaseStream.Length)
            {
                throw new InvalidDataException("Invalid footer");
            }

            using (var footerStream = new MemoryStream(footer))
            using (var footerReader = new BinaryReader(footerStream))
            {
                var footerString = footerReader.ReadBytes(17);
                if(!footerString.SequenceEqual(FooterString))
                {
                    throw new InvalidDataException("Invalid footer, no footer string");
                }
                FinalTimeCode = footerReader.ReadUInt32();
                Data = footerReader.ReadBytes(footer.Length - 25);
                if(footerReader.ReadInt32() != footerLength)
                {
                    throw new InvalidDataException();
                }
            }
        }

        public ReplayFooter(uint finalTimeCode)
        {
            FinalTimeCode = finalTimeCode;
            Data = new byte[] { 0x02, 0x1A, 0x00, 0x00, 0x00 };
        }

        List<float> TryGetKillDeathRatio()
        {
            if(Data.Length < 24)
            {
                return null;
            }

            var ratios = new List<float>();
            using (var stream = new MemoryStream(Data, Data.Length - 24, 24))
            using (var reader = new BinaryReader(stream))
            {
                ratios.Add(reader.ReadSingle());
            }

            return ratios;
        }
    }

    internal static class ReplayExtensions
    {
        public static string ReadUTF16String(this BinaryReader reader)
        {
            var currentBytes = new List<byte>();
            byte[] lastTwoBytes = null;
            while(true)
            {
                lastTwoBytes = reader.ReadBytes(2);
                if (lastTwoBytes.Length != 2)
                {
                    throw new InvalidDataException();
                }
                if(lastTwoBytes.All(x => x == 0))
                {
                    break;
                }
                currentBytes.AddRange(lastTwoBytes);
            }

            return Encoding.Unicode.GetString(currentBytes.ToArray());
        }

        public static void WriteChunk(this BinaryWriter writer, ReplayChunk chunk)
        {
            writer.Write(chunk.TimeCode);
            writer.Write(chunk.Type);
            writer.Write(chunk.Data.Length);
            writer.Write(chunk.Data);
            writer.Write(0);
        }

        public static void WriteFooter(this BinaryWriter writer, ReplayFooter footer)
        {
            writer.Write(ReplayFooter.Terminator);
            writer.Write(ReplayFooter.FooterString);
            writer.Write(footer.FinalTimeCode);
            writer.Write(footer.Data);
            writer.Write(ReplayFooter.FooterString.Length + footer.Data.Length + 8);
        }

        public static void WriteReplay(this BinaryWriter writer, Replay replay)
        {
            writer.Write(replay.RawHeader);

            var lastTimeCode = (uint)0;
            foreach(var chunk in replay.Body)
            {
                lastTimeCode = chunk.TimeCode;
                writer.WriteChunk(chunk);
            }

            if (replay.HasFooter)
            {
                writer.WriteFooter(replay.Footer);
            }
            else
            {
                writer.WriteFooter(new ReplayFooter(lastTimeCode));
            }
        }
    }

    internal sealed class Replay
    {
        public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER");
        public static readonly Dictionary<ReplayType, string> TypeStrings = new Dictionary<ReplayType, string>()
        {
            { ReplayType.Skirmish, "遭遇战录像" },
            { ReplayType.Lan, "局域网录像" },
            { ReplayType.Online, "官网录像" },
        };

        public string Path { get; private set; }
        public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path);
        public DateTime Date { get; private set; }
        public bool HasFooter => Footer != null;
        public TimeSpan? Length { get; private set; }
        public string MapName { get; private set; }
        public string MapPath { get; private set; }
        public IReadOnlyList<Player> Players => _players;
        public int NumberOfPlayingPlayers { get; private set; }
        public long Size { get; private set; }
        public Mod Mod { get; private set; }
        public ReplayType Type { get; private set; }
        public string TypeString => TypeStrings[Type];
        public bool HasCommentator { get; private set; }
        public Player ReplaySaver => Players[_replaySaverIndex];

        public byte[] RawHeader { get; private set; }
        public IReadOnlyList<ReplayChunk> Body => _body;
        public ReplayFooter Footer { get; private set; }

        private List<Player> _players;
        private byte _replaySaverIndex;
        private long _headerSize;
        private List<ReplayChunk> _body;

        public Replay(string path)
        {
            using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
            {
                Parse(path, stream);
            }
        }

        public Replay(string path, Stream stream)
        {
            Parse(path, stream);
        }

        private void Parse(string path, Stream stream)
        {
            Path = path;
            
            using (var reader = new BinaryReader(stream))
            {
                Size = reader.BaseStream.Length;
                var headerMagic = reader.ReadBytes(HeaderMagic.Length);
                if(!headerMagic.SequenceEqual(HeaderMagic))
                {
                    throw new InvalidDataException($"{Path} is not a replay, header is {BitConverter.ToString(headerMagic)}");
                }

                var isSkirmish = reader.ReadByte() == 0x04;
                reader.ReadBytes(4 * 4); // version and builds
                reader.ReadBytes(2); // commentary flag, and padding zero byte

                reader.ReadUTF16String(); // title
                reader.ReadUTF16String(); // description
                MapName = reader.ReadUTF16String(); // map name
                reader.ReadUTF16String(); // map id

                NumberOfPlayingPlayers = reader.ReadByte();
                
                for(var i = 0; i <= NumberOfPlayingPlayers; ++i)
                {
                    reader.ReadUInt32();
                    reader.ReadUTF16String(); // utf16 player name
                    if(!isSkirmish)
                    {
                        reader.ReadByte(); // team
                    }
                }

                var offset = reader.ReadInt32();
                var cnc3MagicLength = reader.ReadInt32();
                _headerSize = reader.BaseStream.Position + offset;
                reader.ReadBytes(cnc3MagicLength);
                
                var modInfo = reader.ReadBytes(22);
                Mod = new Mod(Encoding.UTF8.GetString(modInfo));

                var timeStamp = reader.ReadUInt32();
                Date = DateTimeOffset.FromUnixTimeSeconds(timeStamp).DateTime;

                reader.ReadBytes(31);
                var descriptionsLength = reader.ReadInt32();
                var description = Encoding.UTF8.GetString(reader.ReadBytes(descriptionsLength));

                var entries = null as Dictionary<string, string>;
                try
                {
                    var query = from splitted in description.Split(';')
                                where !string.IsNullOrWhiteSpace(splitted)
                                select splitted.Split(new[] { '=' }, 2);
                    entries = query.ToDictionary(x => x[0], x => x[1]);
                }
                catch(Exception exception)
                {
                    throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{exception}");
                }

                try
                {
                    _players = entries["S"].Split(':')
                        .TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
                        .Select(x => new Player(x.Split(',')))
                        .ToList();
                }
                catch (Exception exception)
                {
                    throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}");
                }

                MapPath = entries["M"].Substring(3);

                HasCommentator = !entries["PC"].Equals("-1");

                var lanFlag = int.Parse(entries["GT"]) == 0;
                if (lanFlag)
                {
                    if(_players.First().PlayerIP == 0)
                    {
                        Type = ReplayType.Skirmish;
                    }
                    else
                    {
                        Type = ReplayType.Lan;
                    }
                }
                else
                {
                    Type = ReplayType.Online;
                }

                _replaySaverIndex = reader.ReadByte();

                reader.ReadBytes(8); // 8 bit paddings
                var fileNameLength = reader.ReadInt32();
                reader.ReadBytes(fileNameLength * 2);
                reader.ReadBytes(16);
                var verMagicLength = reader.ReadInt32();
                reader.ReadBytes(verMagicLength);
                reader.ReadBytes(85);

                if(reader.BaseStream.Position != _headerSize)
                {
                    throw new InvalidDataException();
                }

                reader.BaseStream.Seek(-4, SeekOrigin.End);
                try
                {
                    Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
                    Length = TimeSpan.FromSeconds(Math.Round(Footer.FinalTimeCode / 15.0));
                }
                catch(Exception)
                {
                    Length = null;
                    Footer = null;
                }
            }
        }

        public void ParseBody()
        {
            _body = new List<ReplayChunk>();

            using (var stream = new FileStream(Path, FileMode.Open))
            using (var reader = new BinaryReader(stream))
            {
                RawHeader = reader.ReadBytes((int)_headerSize);
                while (true)
                {
                    var timeCode = reader.ReadUInt32();
                    if (timeCode == ReplayFooter.Terminator)
                    {
                        Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter);
                        break;
                    }

                    _body.Add(new ReplayChunk(timeCode, reader));
                }
            }
        }

        public Dictionary<byte, int[]> GetCommandCounts()
        {
            var playerCommands = new Dictionary<byte, int[]>();

            foreach(var chunk in _body)
            {
                if(chunk.Type != 1)
                {
                    continue;
                }

                foreach(var command in CommandChunk.Parse(chunk))
                {
                    var commandCount = playerCommands.TryGetValue(command.CommandID, out var current) ? current : new int[Players.Count];
                    if(command.PlayerIndex >= commandCount.Length) // unknown or unparsable command?
                    {
                        commandCount = commandCount
                            .Concat(new int[command.PlayerIndex - commandCount.Length + 1])
                            .ToArray();
                    }
                    commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
                    playerCommands[command.CommandID] = commandCount;
                }
            }

            return playerCommands;
        }
    }
}