using AnotherReplayReader.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;

namespace AnotherReplayReader.ReplayFile
{
    internal enum ReplayType
    {
        Skirmish,
        Lan,
        Online
    }

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

        private readonly byte _replaySaverIndex;
        private readonly byte[]? _rawHeader;

        public string Path { get; }
        public DateTime Date { get; }
        public string MapName { get; }
        public string MapPath { get; }
        public ImmutableArray<Player> Players { get; }
        public int NumberOfPlayingPlayers { get; }
        public Mod Mod { get; }
        public ReplayType Type { get; }
        public bool HasCommentator { get; }

        public long Size { get; }
        public ReplayFooter? Footer { get; }
        public ImmutableArray<ReplayChunk>? Body { get; }

        public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path);
        public Player ReplaySaver => Players[_replaySaverIndex];
        public string TypeString => TypeStrings[Type];
        public bool HasFooter => Footer != null;
        public ShortTimeSpan? Length => Footer?.ReplayLength;

        public Replay(string path, bool parseBody = false) :
            this(path, new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), parseBody)
        {
        }

        public Replay(string path, Stream stream, bool parseBody = false)
        {
            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();
            var headerSize = checked((int)(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 e)
            {
                throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{e}");
            }

            try
            {
                Players = entries["S"].Split(':')
                    .TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
                    .Select(x => new Player(x.Split(',')))
                    .ToImmutableArray();
            }
            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)
            {
                Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (acutally {reader.BaseStream.Position})\r\n";
                return;
            }

            if (!parseBody)
            {
                // jump to footer directly
                reader.BaseStream.Seek(-4, SeekOrigin.End);
                try
                {
                    Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
                }
                catch (Exception e)
                {
                    Debug.Instance.DebugMessage += $"Failed to parse replay footer, replay might be corrupt: {e}\r\n";
                    Footer = null;
                }

                return;
            }

            var body = new List<ReplayChunk>();
            try
            {
                while (true)
                {
                    var timeCode = reader.ReadUInt32();
                    if (timeCode == ReplayFooter.Terminator)
                    {
                        Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter);
                        break;
                    }

                    body.Add(new ReplayChunk(timeCode, reader));
                }
            }
            catch (Exception e)
            {
                Debug.Instance.DebugMessage += $"Failed to parse replay body, replay might be corrupt: {e}\r\n";
            }

            byte[]? rawHeader = null;
            try
            {
                // 重新读取原来的整个录像头
                reader.BaseStream.Seek(0, SeekOrigin.Begin);
                rawHeader = reader.ReadBytes(headerSize);
            }
            catch (Exception e)
            {
                Debug.Instance.DebugMessage += $"Warning: failed to read raw header: {e}\r\n";
                return;
            }
            if (rawHeader.Length != headerSize)
            {
                Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (raw header length =  {rawHeader.Length})\r\n";
                return;
            }

            _rawHeader = rawHeader;
            Body = body.ToImmutableArray();
        }

        public Replay CloneHeader()
        {
            using var stream = new MemoryStream();
            using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
            {
                WriteTo(writer);
            }
            stream.Position = 0;
            return new Replay(Path, stream);
        }

        public bool PathEquals(Replay replay) => PathEquals(replay.Path);
        public bool PathEquals(string path) => Path.Equals(path, StringComparison.OrdinalIgnoreCase);

        public string GetDetails(PlayerIdentity playerIdentity)
        {
            static string GetSizeString(double size)
            {
                if (size > 1024 * 1024)
                {
                    return $"{Math.Round(size / (1024 * 1024), 2)}MB";
                }
                return $"{Math.Round(size / 1024)}KB";
            }

            string size = GetSizeString(Size);
            string length = Length?.ToString() ?? "录像已损坏,请先修复录像";

            var replaySaver = _replaySaverIndex < Players.Length
                ? ReplaySaver.PlayerName
                : "[无法获取保存录像的玩家]";

            using var writer = new StringWriter();
            writer.WriteLine("文件名:{0}", FileName);
            writer.WriteLine("大小:{0}", size);
            writer.WriteLine("地图:{0}", MapName);
            writer.WriteLine("日期:{0}", Date);
            writer.WriteLine("长度:{0}", length);
            writer.WriteLine("录像类别:{0}", TypeString);
            writer.WriteLine("这个文件是{0}保存的", replaySaver);
            writer.WriteLine("玩家列表:");
            foreach (var player in Players)
            {
                if (player == Players.Last() && player.PlayerName.Equals(PostCommentator))
                {
                    break;
                }
                var factionName = ModData.GetFaction(Mod, player.FactionId).Name;
                var realName = Type == ReplayType.Lan ? playerIdentity.FormatRealName(player.PlayerIp) : string.Empty;
                writer.WriteLine($"{player.PlayerName + realName},{factionName}");
            }

            return writer.ToString();
        }

        public void WriteTo(BinaryWriter writer) => WriteTo(writer, false);

        private void WriteTo(BinaryWriter writer, bool skipBody)
        {
            if ((_rawHeader is null || Body is null) && !skipBody)
            {
                throw new InvalidOperationException("Replay body must be parsed before writing replay");
            }

            writer.Write(_rawHeader);

            var lastTimeCode = Footer?.FinalTimeCode;
            if (Body is not null)
            {
                foreach (var chunk in Body)
                {
                    lastTimeCode = chunk.TimeCode;
                    writer.Write(chunk);
                }
            }

            if (Footer is not null)
            {
                writer.Write(Footer);
            }
            else if (lastTimeCode is uint lastTimeCodeValue)
            {
                writer.Write(new ReplayFooter(lastTimeCodeValue));
            }
        }
    }


}