namespace PKHeX.Core
{
    /// <summary>
    /// Contains logic for the Generation 8 (SW/SH) overworld spawns that walk around the overworld.
    /// </summary>
    /// <remarks>
    /// Entities spawned into the overworld that can be encountered are assigned a 32bit seed, which can be immediately derived from the <see cref="PKM.EncryptionConstant"/>.
    /// </remarks>
    public static class Overworld8RNG
    {
        public static void ApplyDetails(PKM pk, EncounterCriteria criteria, Shiny shiny = Shiny.FixedValue, int flawless = -1)
        {
            if (shiny == Shiny.FixedValue)
                shiny = criteria.Shiny == Shiny.Random ? Shiny.Never : Shiny.Always;
            if (flawless == -1)
                flawless = 0;

            int ctr = 0;
            const int maxAttempts = 50_000;
            var rnd = Util.Rand;
            do
            {
                var seed = Util.Rand32(rnd);
                if (TryApplyFromSeed(pk, criteria, shiny, flawless, seed))
                    return;
            } while (++ctr != maxAttempts);
            TryApplyFromSeed(pk, EncounterCriteria.Unrestricted, shiny, flawless, Util.Rand32(rnd));
        }

        private static bool TryApplyFromSeed(PKM pk, EncounterCriteria criteria, Shiny shiny, int flawless, uint seed)
        {
            var xoro = new Xoroshiro128Plus(seed);

            // Encryption Constant
            pk.EncryptionConstant = (uint) xoro.NextInt(uint.MaxValue);

            // PID
            var pid = (uint) xoro.NextInt(uint.MaxValue);
            if (shiny == Shiny.Never)
            {
                if (GetIsShiny(pk.TID, pk.SID, pid))
                    pid ^= 0x1000_0000;
            }
            else if (shiny != Shiny.Random)
            {
                if (!GetIsShiny(pk.TID, pk.SID, pid))
                    pid = GetShinyPID(pk.TID, pk.SID, pid, 0);

                if (shiny == Shiny.AlwaysSquare && pk.ShinyXor != 0)
                    return false;
                if (shiny == Shiny.AlwaysStar && pk.ShinyXor == 0)
                    return false;
            }

            pk.PID = pid;

            // IVs
            var ivs = new[] {UNSET, UNSET, UNSET, UNSET, UNSET, UNSET};
            const int MAX = 31;
            for (int i = 0; i < flawless; i++)
            {
                int index;
                do { index = (int) xoro.NextInt(6); }
                while (ivs[index] != UNSET);

                ivs[index] = MAX;
            }

            for (int i = 0; i < ivs.Length; i++)
            {
                if (ivs[i] == UNSET)
                    ivs[i] = (int) xoro.NextInt(32);
            }

            if (!criteria.IsIVsCompatible(ivs, 8))
                return false;

            pk.IV_HP = ivs[0];
            pk.IV_ATK = ivs[1];
            pk.IV_DEF = ivs[2];
            pk.IV_SPA = ivs[3];
            pk.IV_SPD = ivs[4];
            pk.IV_SPE = ivs[5];

            // Remainder
            var scale = (IScaledSize) pk;
            scale.HeightScalar = (int) xoro.NextInt(0x81) + (int) xoro.NextInt(0x80);
            scale.WeightScalar = (int) xoro.NextInt(0x81) + (int) xoro.NextInt(0x80);

            return true;
        }

        public static bool ValidateOverworldEncounter(PKM pk, Shiny shiny = Shiny.FixedValue, int flawless = -1)
        {
            var seed = GetOriginalSeed(pk);
            return ValidateOverworldEncounter(pk, seed, shiny, flawless);
        }

        public static bool ValidateOverworldEncounter(PKM pk, uint seed, Shiny shiny = Shiny.FixedValue, int flawless = -1)
        {
            // is the seed Xoroshiro determined, or just truncated state?
            if (seed == uint.MaxValue)
                return false;

            var xoro = new Xoroshiro128Plus(seed);
            var ec = (uint) xoro.NextInt(uint.MaxValue);
            if (ec != pk.EncryptionConstant)
                return false;

            var pid = (uint) xoro.NextInt(uint.MaxValue);
            if (!IsPIDValid(pk, pid, shiny))
                return false;

            var actualCount = flawless == -1 ? GetIsMatchEnd(pk, xoro) : GetIsMatchEnd(pk, xoro, flawless, flawless);
            return actualCount != NoMatchIVs;
        }

        private static bool IsPIDValid(PKM pk, uint pid, Shiny shiny)
        {
            if (shiny == Shiny.Random)
                return pid == pk.PID;

            if (pid == pk.PID)
                return true;

            // Check forced shiny
            if (pk.IsShiny)
            {
                if (GetIsShiny(pk.TID, pk.SID, pid))
                    return false;

                pid = GetShinyPID(pk.TID, pk.SID, pid, 0);
                return pid == pk.PID;
            }

            // Check forced non-shiny
            if (!GetIsShiny(pk.TID, pk.SID, pid))
                return false;

            pid ^= 0x1000_0000;
            return pid == pk.PID;
        }

        private const int NoMatchIVs = -1;
        private const int UNSET = -1;

        private static int GetIsMatchEnd(PKM pk, Xoroshiro128Plus xoro, int start = 0, int end = 3)
        {
            bool skip1 = start == 0 && end == 3;
            for (int iv_count = start; iv_count <= end; iv_count++)
            {
                if (skip1 && iv_count == 1)
                    continue;

                var copy = xoro;
                int[] ivs = { UNSET, UNSET, UNSET, UNSET, UNSET, UNSET };
                const int MAX = 31;
                for (int i = 0; i < iv_count; i++)
                {
                    int index;
                    do { index = (int)copy.NextInt(6); } while (ivs[index] != UNSET);
                    ivs[index] = MAX;
                }

                if (!IsValidSequence(pk, ivs, ref copy))
                    continue;

                if (pk is not IScaledSize s)
                    continue;
                var height = (int) copy.NextInt(0x81) + (int) copy.NextInt(0x80);
                if (s.HeightScalar != height)
                    continue;
                var weight = (int) copy.NextInt(0x81) + (int) copy.NextInt(0x80);
                if (s.WeightScalar != weight)
                    continue;

                return iv_count;
            }
            return NoMatchIVs;
        }

        private static bool IsValidSequence(PKM pk, int[] template, ref Xoroshiro128Plus rng)
        {
            for (int i = 0; i < 6; i++)
            {
                var temp = template[i];
                var expect = temp == UNSET ? (int)rng.NextInt(32) : temp;
                var actual = i switch
                {
                    0 => pk.IV_HP,
                    1 => pk.IV_ATK,
                    2 => pk.IV_DEF,
                    3 => pk.IV_SPA,
                    4 => pk.IV_SPD,
                    _ => pk.IV_SPE,
                };
                if (expect != actual)
                    return false;
            }

            return true;
        }

        private static uint GetShinyPID(int tid, int sid, uint pid, int type)
        {
            return (uint)(((tid ^ sid ^ (pid & 0xFFFF) ^ type) << 16) | (pid & 0xFFFF));
        }

        private static bool GetIsShiny(int tid, int sid, uint pid)
        {
            return GetShinyXor(pid, (uint)((sid << 16) | tid)) < 16;
        }

        private static uint GetShinyXor(uint pid, uint oid)
        {
            var xor = pid ^ oid;
            return (xor ^ (xor >> 16)) & 0xFFFF;
        }

        /// <summary>
        /// Obtains the original seed for the Generation 8 overworld wild encounter.
        /// </summary>
        /// <param name="pk">Entity to check for</param>
        /// <returns>Seed</returns>
        public static uint GetOriginalSeed(PKM pk)
        {
            var seed = pk.EncryptionConstant - unchecked((uint)Xoroshiro128Plus.XOROSHIRO_CONST);
            if (seed == 0xD5B9C463) // Collision seed with the 0xFFFFFFFF re-roll.
            {
                var xoro = new Xoroshiro128Plus(seed);
                /*  ec */ xoro.NextInt(uint.MaxValue);
                var pid = xoro.NextInt(uint.MaxValue);
                if (pid != pk.PID)
                    return 0xDD6295A4;
            }

            return seed;
        }
    }
}