using System;
using System.Collections.Generic;
using static System.Buffers.Binary.BinaryPrimitives;

namespace PKHeX.Core
{
    /// <summary>
    /// Pokémon Stadium 2 (Pokémon Stadium GS in Japan)
    /// </summary>
    public sealed class SAV2Stadium : SAV_STADIUM
    {
        public override int SaveRevision => Japanese ? 0 : 1;
        public override string SaveRevisionString => Japanese ? "J" : "U";

        public override PersonalTable Personal => PersonalTable.C;
        public override int MaxEV => ushort.MaxValue;
        public override IReadOnlyList<ushort> HeldItems => Legal.HeldItems_GSC;
        public override GameVersion Version { get; protected set; } = GameVersion.Stadium2;

        protected override SaveFile CloneInternal() => new SAV2Stadium((byte[])Data.Clone(), Japanese);

        public override int Generation => 2;
        private const int StringLength = 12;
        public override int OTLength => StringLength;
        public override int NickLength => StringLength;
        public override int BoxCount => Japanese ? 9 : 14;
        public override int BoxSlotCount => Japanese ? 30 : 20;

        public override int MaxMoveID => Legal.MaxMoveID_2;
        public override int MaxSpeciesID => Legal.MaxSpeciesID_2;
        public override int MaxAbilityID => Legal.MaxAbilityID_2;
        public override int MaxItemID => Legal.MaxItemID_2;

        public override Type PKMType => typeof(SK2);
        public override PKM BlankPKM => new SK2(Japanese);
        protected override PKM GetPKM(byte[] data) => new SK2(data, Japanese);

        private const int SIZE_SK2 = PokeCrypto.SIZE_2STADIUM; // 60
        protected override int SIZE_STORED => SIZE_SK2;
        protected override int SIZE_PARTY => SIZE_SK2;

        private const int ListHeaderSizeTeam = 0x10;
        private const int ListHeaderSizeBox = 0x20;
        private const int ListFooterSize = 6; // POKE + 2byte checksum

        protected override int TeamCount => 60;
        private const int TeamCountType = 10;
        private const int TeamSize = ListHeaderSizeTeam + (SIZE_SK2 * 6) + 2 + ListFooterSize; // 0x180

        private int BoxSize => Japanese ? BoxSizeJ : BoxSizeU;
        private const int BoxSizeJ = ListHeaderSizeBox + (SIZE_SK2 * 30) + 2 + ListFooterSize; // 0x730
        private const int BoxSizeU = ListHeaderSizeBox + (SIZE_SK2 * 20) + 2 + ListFooterSize; // 0x4D8

        // Box 1 is stored separately from the remainder of the boxes.
        private const int BoxStart = 0x5E00; // Box 1
        private const int BoxContinue = 0x8000; // Box 2+

        private const uint MAGIC_FOOTER = 0x30763350; // P3v0

        public SAV2Stadium(byte[] data) : this(data, IsStadiumJ(data)) { }

        public SAV2Stadium(byte[] data, bool japanese) : base(data, japanese, StadiumUtil.IsMagicPresentSwap(data, TeamSize, MAGIC_FOOTER))
        {
            Box = BoxStart;
        }

        public SAV2Stadium(bool japanese = false) : base(japanese, SaveUtil.SIZE_G2STAD)
        {
            Box = BoxStart;
            ClearBoxes();
        }

        protected override bool GetIsBoxChecksumValid(int box)
        {
            var boxOfs = GetBoxOffset(box) - ListHeaderSizeBox;
            var size = BoxSize - 2;
            var chk = Checksums.CheckSum16(new ReadOnlySpan<byte>(Data, boxOfs, size));
            var actual = ReadUInt16BigEndian(Data.AsSpan(boxOfs + size));
            return chk == actual;
        }

        protected override void SetBoxMetadata(int box)
        {
            var bdata = GetBoxOffset(box);

            // Set box count
            int count = 0;
            for (int s = 0; s < BoxSlotCount; s++)
            {
                var rel = bdata + (SIZE_STORED * s);
                if (Data[rel] != 0) // Species present
                    count++;
            }

            var boxOfs = bdata - ListHeaderSizeBox;
            if (Data[boxOfs] == 0)
            {
                Data[boxOfs] = 1;
                Data[boxOfs + 1] = (byte)count;
                Data[boxOfs + 4] = StringConverter12.G1TerminatorCode;
                StringConverter12.SetString(Data.AsSpan(boxOfs + 0x10, 4), "1234".AsSpan(), 4, Japanese, StringConverterOption.None);
            }
            else
            {
                Data[boxOfs + 1] = (byte)count;
            }
        }

        protected override void SetBoxChecksum(int box)
        {
            var boxOfs = GetBoxOffset(box) - ListHeaderSizeBox;
            var size = BoxSize - 2;
            var chk = Checksums.CheckSum16(new ReadOnlySpan<byte>(Data, boxOfs, size));
            WriteUInt16BigEndian(Data.AsSpan(boxOfs + size), chk);
        }

        public static int GetTeamOffset(Stadium2TeamType type, int team)
        {
            if ((uint)team >= TeamCountType)
                throw new ArgumentOutOfRangeException(nameof(team));

            var index = (TeamCountType * (int)type) + team;
            return GetTeamOffset(index);
        }

        public static int GetTeamOffset(int team)
        {
            if (team < 40)
                return 0 + (team * TeamSize);
            // Teams 41-60 are in a separate chunk
            return 0x4000 + ((team - 40) * TeamSize);
        }

        public string GetTeamName(int team)
        {
            var name = $"{((Stadium2TeamType) (team / TeamCountType)).ToString().Replace('_', ' ')} {(team % 10) + 1}";

            var ofs = GetTeamOffset(team);
            var str = GetString(ofs + 4, 7);
            if (string.IsNullOrWhiteSpace(str))
                return name;
            var id = ReadUInt16BigEndian(Data.AsSpan(ofs + 2));
            return $"{name} [{id:D5}:{str}]";
        }

        public override string GetBoxName(int box)
        {
            var ofs = GetBoxOffset(box) - 0x10;
            var str = GetString(ofs, 0x10);
            if (string.IsNullOrWhiteSpace(str))
                return $"Box {box + 1}";
            return str;
        }

        public override SlotGroup GetTeam(int team)
        {
            if ((uint)team >= TeamCount)
                throw new ArgumentOutOfRangeException(nameof(team));

            var name = GetTeamName(team);
            var members = new SK2[6];
            var ofs = GetTeamOffset(team);
            for (int i = 0; i < 6; i++)
            {
                var rel = ofs + ListHeaderSizeTeam + (i * SIZE_STORED);
                members[i] = (SK2)GetStoredSlot(Data, rel);
            }
            return new SlotGroup(name, members);
        }

        public override int GetBoxOffset(int box)
        {
            if (box == 0)
                return BoxStart + ListHeaderSizeBox;
            return BoxContinue + ListHeaderSizeBox + ((box - 1) * BoxSize);
        }

        public static bool IsStadium(ReadOnlySpan<byte> data)
        {
            if (data.Length is not (SaveUtil.SIZE_G2STAD or SaveUtil.SIZE_G2STADF))
                return false;
            return StadiumUtil.IsMagicPresentEither(data, TeamSize, MAGIC_FOOTER);
        }

        // Check Box 1's footer magic.
        private static bool IsStadiumJ(ReadOnlySpan<byte> data) => StadiumUtil.IsMagicPresentAbsolute(data, BoxStart + BoxSizeJ - ListFooterSize, MAGIC_FOOTER);
    }

    public enum Stadium2TeamType
    {
        Anything_Goes = 0,
        Little_Cup = 1,
        Poké_Cup = 2,
        Prime_Cup = 3,
        GymLeader_Castle = 4,
        Vs_Rival = 5,
    }
}
