using System.Buffers;
using System.Buffers.Binary;
using System.Runtime.InteropServices;
using System.Text;
using SoulstormReplayReader.Core.Domain;
using SoulstormReplayReader.Core.Domain.Action;
using SoulstormReplayReader.Core.Domain.BugCheckers;
using SoulstormReplayReader.Core.Domain.ChatMessage;
using SoulstormReplayReader.Core.Domain.Player;
using SoulstormReplayReader.Core.Domain.Replay;
using SoulstormReplayReader.Core.Domain.Tick;
using SoulstormReplayReader.Core.Enums;
using SoulstormReplayReader.Core.Extensions;
using SoulstormReplayReader.Core.Models;
using SoulstormReplayReader.Core.Utils;

namespace SoulstormReplayReader.Core;
// Существует благодаря
// http://forums.warforge.ru/index.php?showtopic=101573&hl=replay
// а также реплей менеджеру эламаунта :0

/*
 * Relic chunk struct:
 * - int32 version
 * - int32 chunkSize
 * - int32 variableStringLength or else
 * - ...other data...
 */
public sealed class SsReplayReader(Stream stream) : IDisposable
{
    private readonly ExBinaryReader _binaryReader = new(stream);

    /// <summary>
    /// Если параметр активен, картинки баннера и бейджа игроков не парсятся
    /// <para/>
    /// Player.Banner.Bitmap = null
    /// <para/>
    /// Player.Badge.Bitmap = null
    /// </summary>
    public bool SkipImages { get; set; }

    /// <summary>
    /// Если параметр активен, на игроков будут повешены BugChecker'ы в зависимости от их расы
    /// </summary>
    public bool CheckForBugs { get; set; }

    public PixelType ImagePixelType { get; set; } = PixelType.Bgra8888;

    public ReplayDescriptor Descriptor { get; } = new();
    public ReplayModel Replay { get; set; }
    public int CurrentTick { get; set; }

    public ReplayModel ReadFull(bool readOrdinaryTicks = true)
    {
        ReadInfo();

        ReadTicks(readOrdinaryTicks);

        return Replay;
    }

    public ReplayModel ReadInfo()
    {
        ReadHeader();

        ReadPlayers();

        return Replay;
    }

    public ReplayModel ReadHeader()
    {
        Reset();

        ReadStaticHeader(); // 226 - Последняя статичная позиция

        ReadDynamicHeader();

        return Replay;
    }

    public void Reset()
    {
        _binaryReader.Seek(0);
        Replay = new ReplayModel();
        CurrentTick = 0;
    }

    private void ReadStaticHeader()
    {
        Replay.Version = (ReplayVersion)_binaryReader.ReadInt32();

        var modNameSpan = _binaryReader.ReadBytes(stackalloc byte[32]);
        Replay.ModName = Encoding.ASCII.GetString(modNameSpan.TrimEnd());

        _binaryReader // relic chunky
            .Skip(44)
            .NextAsciiStringMustEqual("POSTGAMEINFO\0DATADATA")
            .SkipInt32(3);

        Replay.TotalTicks = _binaryReader.ReadInt32(); // Полное игровое время (1 сек = 8 тиков)

        _binaryReader // relic chunky
            .Skip(24)
            .NextAsciiStringMustEqual("FOLDINFO")
            .SkipInt32();

        Descriptor.FoldInfoStart = _binaryReader.Position; // 153
        Descriptor.FoldInfoSize = _binaryReader.ReadInt32();

        _binaryReader
            .SkipInt32()
            .NextAsciiStringMustEqual("GAMEINFO\0FOLDWMAN")
            .SkipInt32();

        Descriptor.FoldWmanStart = _binaryReader.Position; // 182
        Descriptor.FoldWmanSize = _binaryReader.ReadInt32();

        _binaryReader
            .SkipInt32()
            .NextAsciiStringMustEqual("DATASDSC")
            .Skip(20);

        Replay.Map.MaxPlayersCount = _binaryReader.ReadInt32();
        Replay.Map.Size = _binaryReader.ReadInt32();
    }

    private void ReadDynamicHeader()
    {
        var modNameLength = _binaryReader.ReadInt32();
        modNameLength.RequireInRange(1, 1000, "Не удалось считать название мода");
        Replay.EngineAddon = _binaryReader.ReadAsciiString(modNameLength);

        // Название движка (?)
        var engineNameLength = _binaryReader.ReadInt32();
        engineNameLength.RequireInRange(1, 1000, "Не удалось считать название движка");
        Replay.EngineName = _binaryReader.ReadUnicodeString(engineNameLength);

        var mapNameLength = _binaryReader.ReadInt32();
        mapNameLength.RequireInRange(1, 1000, "Не удалось считать название карты");
        // DATA:Scenarios\MP\2P_FATA_MORGANA
        // Пропускаем DATA:Scenarios\MP\
        Replay.Map.Name = _binaryReader.Skip(18).ReadAsciiString(mapNameLength - 18);

        _binaryReader
            .SkipInt32(4)
            .NextAsciiStringMustEqual("DATABASE")
            .SkipInt32();

        Descriptor.BeginDataBaseChunkSize = _binaryReader.Position;
        Descriptor.DataBaseChunkSize = _binaryReader.ReadInt32();

        _binaryReader.SkipInt32(3);
        Replay.WorldSeed = _binaryReader.ReadUInt32();
        _binaryReader.SkipInt32(); // Размер слотов в игре. Всегда 8

        Descriptor.DataBaseStart = _binaryReader.Position;
        Descriptor.DataBaseSize = Descriptor.DataBaseChunkSize - 20;

        ReadGameSettings();

        Descriptor.FileNameStart = (int)_binaryReader.Position;
        var ingameNameLength = _binaryReader.ReadInt32();
        ingameNameLength.RequireInRange(0, 300, "Не удалось считать имя реплея");
        Replay.IngameName = _binaryReader.ReadUnicodeString(ingameNameLength);

        _binaryReader.SkipInt32(); // 0

        var winConditionCount = _binaryReader.ReadInt32();
        winConditionCount.RequireInRange(0, 1000, "Не удалось считать условия победы");

        var winConditions = MemoryMarshal.Cast<byte, int>(_binaryReader.ReadBytes(stackalloc byte[winConditionCount * 4]));

        Replay.WinConditions.Annihilate = winConditions.Contains((int)WinConditions.ConditionValues.Annihilate);
        Replay.WinConditions.SuddenDeath = winConditions.Contains((int)WinConditions.ConditionValues.SuddenDeath);
        Replay.WinConditions.Assassinate = winConditions.Contains((int)WinConditions.ConditionValues.Assassinate);
        Replay.WinConditions.EconomicVictory = winConditions.Contains((int)WinConditions.ConditionValues.EconomicVictory);
        Replay.WinConditions.ControlArea = winConditions.Contains((int)WinConditions.ConditionValues.ControlArea);
        Replay.WinConditions.DestroyHQ = winConditions.Contains((int)WinConditions.ConditionValues.DestroyHQ);
        Replay.WinConditions.TakeAndHold = winConditions.Contains((int)WinConditions.ConditionValues.TakeAndHold);
        Replay.WinConditions.GameTimer = winConditions.Contains((int)WinConditions.ConditionValues.GameTimer);

        _binaryReader.Skip(5);

        Descriptor.PlayersChunkStart = _binaryReader.Position;
    }

    // Game setting:
    // * 0000 - value
    // * AAAA - compact name
    // res: 0000COLS = (0000)random (COLS=SLOC)Starting locations
    private void ReadGameSettings()
    {
        Replay.GameSettings.AiDifficulty = _binaryReader.ReadByte(); // FDIA = AIDF = AI Difficulty
        _binaryReader.Skip(7);

        Replay.GameSettings.StartResources = _binaryReader.ReadByte(); // TSSR = RSST = Starting Resources
        _binaryReader.Skip(7);

        Replay.GameSettings.LockTeams = _binaryReader.ReadByte(); // MTKL = LKTM = Lock Teams
        _binaryReader.Skip(7);

        Replay.GameSettings.CheatsOn = _binaryReader.ReadByte(); // AEHC = CHEA = Cheats Enabled
        _binaryReader.Skip(7);

        Replay.GameSettings.StartingLocation = _binaryReader.ReadByte(); // COLS = SLOC = Starting Location
        _binaryReader.Skip(7);

        Replay.GameSettings.GameSpeed = _binaryReader.ReadByte(); // DPSG = GSPD = Game Speed
        _binaryReader.Skip(7);

        Replay.GameSettings.ResourceSharing = _binaryReader.ReadByte(); // HSSR = RSSH = Resource Sharing
        _binaryReader.Skip(7);

        Replay.GameSettings.ResourceRate = _binaryReader.ReadByte(); // TRSR = RSRT = Resource Rate
        _binaryReader.Skip(7);

#if DEBUGLOGGING
        Console.WriteLine($"{Replay.GameSettings.AiDifficulty} " +
                          $"{Replay.GameSettings.StartResources} " +
                          $"{Replay.GameSettings.LockTeams} " +
                          $"{Replay.GameSettings.CheatsOn} " +
                          $"{Replay.GameSettings.StartingLocation} " +
                          $"{Replay.GameSettings.GameSpeed} " +
                          $"{Replay.GameSettings.ResourceSharing} " +
                          $"{Replay.GameSettings.ResourceRate}"
        );
#endif

        _binaryReader.Skip(1); // 0
    }


    public void ReadPlayers()
    {
        Descriptor.PlayerStartPoses = new List<long>();
        Replay.Players = new List<PlayerModel>(Consts.PlayersMaxCount);

        for (var players = 0; players < Consts.PlayersMaxCount; players++)
            Replay.Players.Add(ReadPlayer());

        if (Replay.GameSettings.ArePositionsRandomized)
        {
            var randomizer = new PlayersRandomizer(Replay.WorldSeed);
            Replay.Players = randomizer.Randomize(Replay.Players, Replay.Map.MaxPlayersCount);
        }

        Descriptor.ActionsChunkStart = _binaryReader.Position;
        Descriptor.ActionsChunkSize = _binaryReader.BytesLeft;
    }

    private PlayerModel ReadPlayer()
    {
        // В реплеях c ботами нет пустых мест
        if (!_binaryReader.IsNextAsciiStringEquals("FOLDGPLY"))
        {
            _binaryReader.Skip(-8);
            return PlayerModel.Empty;
        }

        Descriptor.PlayerStartPoses.Add(_binaryReader.Position);
        var player = new PlayerModel();

        _binaryReader
            .Skip(12) // int32 version 2; int32 size; int32 strLen 0;
            .NextAsciiStringMustEqual("DATAINFO")
            .Skip(12); // int32 version 1; int32 size; int32 strLen 0;

        player.Name = _binaryReader.ReadNextUnicodeString();
        player.RawType = _binaryReader.ReadInt32(); // Тип игрока: 0 Host/2 player/4 specc/7 empty/1,3,11 computer
        player.Team = _binaryReader.ReadInt32() + 1; // Команда игрока (тут начинается с 0 (в игре с 1))
        player.RawRace = _binaryReader.ReadNextAsciiString(); // Раса игрока: necron_race

        _binaryReader
            .Skip(Replay.Version == ReplayVersion.Steam ? 21 : 9) // byte; int32 0; int32 0xFFFFFFFF;
            .Skip(_binaryReader.ReadInt32()) // непонятно зачем и почему
            .Skip(36);

        if (!_binaryReader.HasBytes)
            return player;

        if (_binaryReader.IsNextAsciiStringEquals("FOLDTCUC"))
        {
            _binaryReader
                .Skip(12) // int32 version 1; int32 size; int32 strLen 0;
                .NextAsciiStringMustEqual("DATALCIN")
                .Skip(4); // int32 version 2;

            _binaryReader.ReadNextAsciiString(); // описание раскрасок отправителя реплея?
            //    ◄   chaos_marine_race↔   chaos_marine_race/AlphaLegion↔   chaos_marine_race/AlphaLe
            //    ←   inquisition_daemonhunt_race0   inquisition_daemonhunt_race/lodge_magister_badge1   inquisition_daemonhunt_race/lodge_magister_ba

            _binaryReader
                .Skip(4) // int32 strLen 0;
                .NextAsciiStringMustEqual("DATAUNCU") // 42192
                .Skip(12); // int32 version 1; int32 size; int32 strLen 0;

            player.ColorScheme = new ColorScheme
            {
                Name = _binaryReader.ReadNextUnicodeString(),

                Primary = _binaryReader.ReadBgraToArgb(),
                Secondary = _binaryReader.ReadBgraToArgb(),
                Trim = _binaryReader.ReadBgraToArgb(),
                Weapons = _binaryReader.ReadBgraToArgb(),
                Eyes = _binaryReader.ReadBgraToArgb()
            };

            // Бейдж и\или баннер (могут не существовать или быть перепутаны)
            for (var imgNum = 0; imgNum < 2; imgNum++)
            {
                var imgTypeName = _binaryReader.ReadAsciiString(8);

                if (!imgTypeName.StartsWith("FOLDTC")) // fold team color
                {
                    _binaryReader.Skip(-8);
                    break;
                }

                if (SkipImages)
                {
                    _binaryReader
                        .Skip(4)
                        .Skip(_binaryReader.ReadInt32() + 4);
                    continue;
                }

                _binaryReader.Skip(12); // int32 version 1; int32 size; int32 strLen 0;

                var playerImage = new ReplayImage();
                if (imgTypeName.EndsWith("BN"))
                {
                    player.Banner = playerImage;
                }
                else
                {
                    player.Badge = playerImage;
                }

                _binaryReader
                    .NextAsciiStringMustEqual("FOLDIMAG")
                    .Skip(8); // int32 version 1; int32 size;

                playerImage.Name = _binaryReader.ReadNextAsciiString();

                _binaryReader
                    .NextAsciiStringMustEqual("DATAATTR")
                    .Skip(16); // int32 version 2; int32 size; 2 * int32 (?) 0;

                var width = _binaryReader.ReadInt32();
                var height = _binaryReader.ReadInt32();

                _binaryReader
                    .Skip(4) // int32 (?) 1;
                    .NextAsciiStringMustEqual("DATADATA")
                    .Skip(12); // int32 version 2; int32 size; int32 (?) 0;

                playerImage.Init(width, height);
                for (var y = height - 1; y >= 0; y--)
                {
                    var line = playerImage.GetLine(y);
                    _binaryReader.ReadBytes(MemoryMarshal.AsBytes(line));

                    if (ImagePixelType == PixelType.Argb8888)
                    {
                        var uintLine = MemoryMarshal.Cast<ReplayColor, uint>(line);

                        for (var x = 0; x < uintLine.Length; x++)
                            uintLine[x] = BinaryPrimitives.ReverseEndianness(uintLine[x]);
                    }
                }
            }
        }
        else _binaryReader.Skip(-8);

        return player;
    }

    private void ReadTicks(bool readOrdinaryTicks)
    {
        if (Replay.Version < ReplayVersion._1_2) // Действия в старых версиях читаются не правильно
        {
            Console.WriteLine("SS version must be at least 1.2 to read actions");
            return;
        }

        if (!Replay.GameSettings.IsQuickStart && !Replay.GameSettings.AreCheatsOn && CheckForBugs)
            AttachBugCheckers();

        Replay.Actions = new List<IGameAction>(Replay.TotalTicks / 2);
        Replay.ChatMessages = new List<ChatMessageModel>();

        // Иногда в конце реплея появляются остаточные байты
        while (_binaryReader.BytesLeft >= 17)
            ReadTick(readOrdinaryTicks);

#if DEBUGLOGGING
        if (_binaryReader.HasBytes)
            Console.WriteLine($"bytes left: {_binaryReader.BytesLeft} at pos: {_binaryReader.Position}");
#endif
    }

    private void AttachBugCheckers()
    {
        for (var playerId = 0; playerId < Replay.Players.Count; playerId++)
        {
            var player = Replay.Players[playerId];

            if (!player.IsActive) continue;

            player.Id = playerId;
            player.BugChecker = player.Race switch
            {
                RaceEnum.Necrons => new PlayerBugChecker()
                    .Add(new GenBugChecker(player.Id, player.Race)),
                RaceEnum.Eldar => new PlayerBugChecker()
                    .Add(new FastT2Checker(player.Id, player.Race)),
                _ => null
            };
        }
    }

    // структура тика:
    // tick:
    // - header
    // - player chunk 1:
    // - - action 1
    // - - action 2
    // - player chunk 2:
    // - - action 1
    private void ReadTick(bool readOrdinaryTicks)
    {
        var tickType = (TickType)_binaryReader.ReadInt32();
        var tickSize = _binaryReader.ReadInt32();

        if (tickType == TickType.Normal)
        {
            if (tickSize == 17 || !readOrdinaryTicks)
            {
                _binaryReader.Skip(tickSize); // Всегда 17 байт
            }
            else
            {
                byte[] rentBytes = null;
                var bytes = tickSize switch
                {
                    > 0 and < 0xFF => stackalloc byte[tickSize],
                    _ => rentBytes = ArrayPool<byte>.Shared.Rent(tickSize)
                };

                try
                {
                    bytes = _binaryReader.ReadBytes(bytes[..tickSize]);
                    ParseOrdinaryTick(bytes);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex);
                }
                finally
                {
                    if (rentBytes is not null)
                        ArrayPool<byte>.Shared.Return(rentBytes);
                }
            }

            CurrentTick++;
        }
        else if (tickType == TickType.Extra)
            ReadExtraTick();
        else
            ReadWierdTick((int)tickType, tickSize);
    }

    private void ParseOrdinaryTick(Span<byte> bytes)
    {
        var spanReader = new SpanReader(bytes);

        // -- TICK HEADER -- size: 17
        spanReader.Skip(1); // 1 byte = 0x50
        var tickCount = spanReader.ReadInt32();
        spanReader.Skip(8); // 8 bytes = int32 "Номер действия игрока" ??? + int32 Random
        var playerChunksCount = spanReader.ReadInt32();

        // -- TICK BODY --
        for (var i = 0; i < playerChunksCount; i++) // для каждого игрока предусмотрен свой чанк со своим размером
        {
            // -- PLAYER CHUNK HEADER -- size: 13 + пока размер чанка != 0
            spanReader.Skip(8); // 8 bytes = ???
            var playerChunkSize = spanReader.ReadInt32();
            spanReader.Skip(1); // 1 byte = playerChunkSize, но байт

            // -- PLAYER CHUNK BODY --
            while (playerChunkSize != 0)
            {
                var actionSize = spanReader.ReadInt16(); // размер захватывает 2 байта (размера) следующего экшена

                spanReader.SliceToOffset();
                var action = ParsePlayerAction(ref spanReader, actionSize - 2);
                action.Tick = tickCount;

                Replay.Actions.Add(action);
                playerChunkSize -= actionSize;
            }
        }
    }

    private GameActionModel ParsePlayerAction(ref SpanReader spanReader, int actionSize)
    {
        // -- ACTION HEADER --
        spanReader.Skip(4); // 4 bytes = какое то время (свое для каждого игрока)
        // (начинается с рандомного значения и увеличивается по ходу игры)
        // Скорее всего является global timer

        var cmd = spanReader.ReadInt32();
        var arg = spanReader.ReadByte(); // int32 in game
        var subCmd = spanReader.ReadInt16();
        var someNum = spanReader.ReadByte();
        var shiftPressed = spanReader.ReadByte() == 1;
        // 2 bytes = player id
        var playerId = spanReader.Skip(2).ReadInt16() % 10;
        var playerActionCount = spanReader.ReadInt16();

#if DEBUGLOGGING
        var selectedCount = _binaryReader.ReadByte();
        var actionTypeNum = _binaryReader.ReadByte();
        var actionType = actionTypeNum switch
        {
            1 => "1.Builder",
            2 => "2.Building",
            3 => "3.Army",
            _ => throw new ArgumentOutOfRangeException($"Action type num: {actionTypeNum} was not recognized")
        };

        var primaryId = _binaryReader.ReadInt32();
        var actionBodyDataType = _binaryReader.ReadByte();
#else
        // _binaryReader.Skip(7);
#endif

        var curAction = new GameActionModel
        {
            Cmd = cmd,
            Arg = arg,
            SubCmd = subCmd,
            SomeByte = someNum,
            ShiftPressed = shiftPressed,

            PlayerId = playerId,
            PlayerActionCount = playerActionCount
        };


        if (CheckForBugs)
            Replay.Players[playerId].BugChecker?.Check(curAction);

        spanReader.Skip((uint)(actionSize - spanReader.offset));


#if DEBUGLOGGING
        // -- ACTION BODY --
        var leftOverBytes = actionEndPos - _binaryReader.Position;
        var bytes = _binaryReader.ReadBytes((int)leftOverBytes);

        var player = Replay.Players[curAction.PlayerId];
        var playerRace = player.Race == RaceEnum.Unknown ? player.RawRace : player.Race.ToString();
        // curAction.PlayerId == 0 && pCommandNum == 117 && Replay.EngineAddon == "dxp2" dataLeftType == 1 && !nn.Contains(f) && !player.Name.StartsWith("Computer")
        // if (Replay.EngineAddon == "dxp2")
        // {
        var playname = player.Name;
        var playnameFormatted = player.Name.PadRight(50 - player.Name.Length, '#');
        Console.WriteLine($"{playname.Length} {playname} \n{playnameFormatted.Length} {playnameFormatted}");
            // Actions.Add(curAction.Cmd);
            Console.Write($"[{TicksFormatter.Format(curAction.Tick)}] | "); // base p:{startPos,-7} 
            Console.Write($"{playnameFormatted} | {playerRace,-18} | "); // player info
            Console.Write($"{curAction.Name,-30} | {curAction} | {selectedCount} units | ");
            Console.Write($"{actionType,-10} | {primaryId,-5} | ");
            Console.WriteLine($"{actionBodyDataType} | {bytes.ToContentString()}");
        // }
#else
        // -- ACTION BODY --
        // var leftOverBytes = actionEndPos - _binaryReader.Position;
        // _binaryReader.Skip((int)leftOverBytes);
#endif

        return curAction;
    }


    private void ReadExtraTick()
    {
        var tickType = _binaryReader.ReadInt32();
        _binaryReader.Skip(4); // Размер тика
        _binaryReader.Skip(1); // Размер тика, но байт

        if (tickType == 0)
            ReadPlayerQuit();
        else if (tickType == 1)
            Replay.ChatMessages.Add(ReadChatMessage());
    }


    private void ReadPlayerQuit() // Событие выхода игрока. (может отсутствовать)
    {
        // ED 03 00 00 00 00 00 00 => 03 ED = int16 player id + 1000;
        _binaryReader.Skip(8);
    }


    private ChatMessageModel ReadChatMessage() => new(
        ticks: CurrentTick,
        senderName: _binaryReader.ReadNextUnicodeString(),
        senderId: _binaryReader.ReadInt32() % 10,
        from: (SenderType)_binaryReader.ReadInt32(), // 0 player | 1 observer | 2 system 
        to: (ReceiverType)_binaryReader.ReadInt32(), // 0 all | 1 team | 2 system 
        text: _binaryReader.ReadNextUnicodeString()
    );

    private void ReadWierdTick(int type, int size)
    {
        Replay.Exception = new Exception($"Wierd tick at {_binaryReader.Position} of type:{type} and size:{size}");

        // Console.WriteLine(Replay.Exception.Message);

        _binaryReader.Skip(size);
    }

    public void Dispose()
    {
        _binaryReader.Dispose();
    }
}