using System;
using System.Collections.Generic;
using System.Linq;
using static PKHeX.Core.GameVersion;
using static PKHeX.Core.Legal;

namespace PKHeX.Core
{
    /// <summary>
    /// Generation specific Evolution Tree data.
    /// </summary>
    /// <remarks>
    /// Used to determine if a <see cref="PKM.Species"/> can evolve from prior steps in its evolution branch.
    /// </remarks>
    public sealed class EvolutionTree
    {
        private static readonly EvolutionTree Evolves1 = new(GetResource("rby"), Gen1, PersonalTable.Y, MaxSpeciesID_1);
        private static readonly EvolutionTree Evolves2 = new(GetResource("gsc"), Gen2, PersonalTable.C, MaxSpeciesID_2);
        private static readonly EvolutionTree Evolves3 = new(GetResource("g3"), Gen3, PersonalTable.RS, MaxSpeciesID_3);
        private static readonly EvolutionTree Evolves4 = new(GetResource("g4"), Gen4, PersonalTable.DP, MaxSpeciesID_4);
        private static readonly EvolutionTree Evolves5 = new(GetResource("g5"), Gen5, PersonalTable.BW, MaxSpeciesID_5);
        private static readonly EvolutionTree Evolves6 = new(GetReader("ao"), Gen6, PersonalTable.AO, MaxSpeciesID_6);
        private static readonly EvolutionTree Evolves7 = new(GetReader("uu"), Gen7, PersonalTable.USUM, MaxSpeciesID_7_USUM);
        private static readonly EvolutionTree Evolves7b = new(GetReader("gg"), Gen7, PersonalTable.GG, MaxSpeciesID_7b);
        private static readonly EvolutionTree Evolves8 = new(GetReader("ss"), Gen8, PersonalTable.SWSH, MaxSpeciesID_8);
        private static readonly EvolutionTree Evolves8a = new(GetReader("la"), Gen8, PersonalTable.LA, MaxSpeciesID_8a);
        private static readonly EvolutionTree Evolves8b = new(GetReader("bs"), Gen8, PersonalTable.BDSP, MaxSpeciesID_8b);

        private static ReadOnlySpan<byte> GetResource(string resource) => Util.GetBinaryResource($"evos_{resource}.pkl");
        private static BinLinkerAccessor GetReader(string resource) => BinLinkerAccessor.Get(GetResource(resource), resource);

        static EvolutionTree()
        {
            // Add in banned evolution data!
            Evolves7.FixEvoTreeSM();
            Evolves8.FixEvoTreeSS();
            Evolves8a.FixEvoTreeLA();
            Evolves8b.FixEvoTreeBS();
        }

        public static EvolutionTree GetEvolutionTree(int generation) => generation switch
        {
            1 => Evolves1,
            2 => Evolves2,
            3 => Evolves3,
            4 => Evolves4,
            5 => Evolves5,
            6 => Evolves6,
            7 => Evolves7,
            _ => Evolves8,
        };

        public static EvolutionTree GetEvolutionTree(PKM pkm, int generation) => generation switch
        {
            1 => Evolves1,
            2 => Evolves2,
            3 => Evolves3,
            4 => Evolves4,
            5 => Evolves5,
            6 => Evolves6,
            7 => pkm.Version is (int)GO or (int)GP or (int)GE ? Evolves7b : Evolves7,
            _ => pkm.Version switch
            {
                (int)PLA => Evolves8a,
                (int)BD or (int)SP => Evolves8b,
                _ => Evolves8,
            },
        };

        private readonly IReadOnlyList<EvolutionMethod[]> Entries;
        private readonly GameVersion Game;
        private readonly PersonalTable Personal;
        private readonly int MaxSpeciesTree;
        private readonly ILookup<int, EvolutionLink> Lineage;
        private static int GetLookupKey(int species, int form) => species | (form << 11);

        #region Constructor

        private EvolutionTree(ReadOnlySpan<byte> data, GameVersion game, PersonalTable personal, int maxSpeciesTree)
        {
            Game = game;
            Personal = personal;
            MaxSpeciesTree = maxSpeciesTree;
            Entries = GetEntries(data, game);

            // Starting in Generation 7, forms have separate evolution data.
            int format = Game - Gen1 + 1;
            var oldStyle = format < 7;
            var connections = oldStyle ? CreateTreeOld() : CreateTree();

            Lineage = connections.ToLookup(obj => obj.Key, obj => obj.Value);
        }

        private EvolutionTree(BinLinkerAccessor data, GameVersion game, PersonalTable personal, int maxSpeciesTree)
        {
            Game = game;
            Personal = personal;
            MaxSpeciesTree = maxSpeciesTree;
            Entries = GetEntries(data, game);

            // Starting in Generation 7, forms have separate evolution data.
            int format = Game - Gen1 + 1;
            var oldStyle = format < 7;
            var connections = oldStyle ? CreateTreeOld() : CreateTree();

            Lineage = connections.ToLookup(obj => obj.Key, obj => obj.Value);
        }

        private IEnumerable<KeyValuePair<int, EvolutionLink>> CreateTreeOld()
        {
            for (int sSpecies = 1; sSpecies <= MaxSpeciesTree; sSpecies++)
            {
                var fc = Personal[sSpecies].FormCount;
                for (int sForm = 0; sForm < fc; sForm++)
                {
                    var index = sSpecies;
                    var evos = Entries[index];
                    foreach (var evo in evos)
                    {
                        var dSpecies = evo.Species;
                        if (dSpecies == 0)
                            continue;

                        var dForm = sSpecies == (int)Species.Espurr && evo.Method == (int)EvolutionType.LevelUpFormFemale1 ? 1 : sForm;
                        var key = GetLookupKey(dSpecies, dForm);

                        var link = new EvolutionLink(sSpecies, sForm, evo);
                        yield return new KeyValuePair<int, EvolutionLink>(key, link);
                    }
                }
            }
        }

        private IEnumerable<KeyValuePair<int, EvolutionLink>> CreateTree()
        {
            for (int sSpecies = 1; sSpecies <= MaxSpeciesTree; sSpecies++)
            {
                var fc = Personal[sSpecies].FormCount;
                for (int sForm = 0; sForm < fc; sForm++)
                {
                    var index = Personal.GetFormIndex(sSpecies, sForm);
                    var evos = Entries[index];
                    foreach (var evo in evos)
                    {
                        var dSpecies = evo.Species;
                        if (dSpecies == 0)
                            break;

                        var dForm = evo.GetDestinationForm(sForm);
                        var key = GetLookupKey(dSpecies, dForm);

                        var link = new EvolutionLink(sSpecies, sForm, evo);
                        yield return new KeyValuePair<int, EvolutionLink>(key, link);
                    }
                }
            }
        }

        private IReadOnlyList<EvolutionMethod[]> GetEntries(ReadOnlySpan<byte> data, GameVersion game) => game switch
        {
            Gen1 => EvolutionSet1.GetArray(data, MaxSpeciesTree),
            Gen2 => EvolutionSet1.GetArray(data, MaxSpeciesTree),
            Gen3 => EvolutionSet3.GetArray(data),
            Gen4 => EvolutionSet4.GetArray(data),
            Gen5 => EvolutionSet5.GetArray(data),
            _ => throw new ArgumentOutOfRangeException(),
        };

        private IReadOnlyList<EvolutionMethod[]> GetEntries(BinLinkerAccessor data, GameVersion game) => game switch
        {
            Gen6 => EvolutionSet6.GetArray(data),
            Gen7 => EvolutionSet7.GetArray(data),
            Gen8 => EvolutionSet7.GetArray(data),
            _ => throw new ArgumentOutOfRangeException(),
        };

        private void FixEvoTreeSM()
        {
            // Sun/Moon lack Ultra's Kantonian evolution methods.
            BanEvo((int)Species.Raichu, 0, pkm => pkm.IsUntraded && pkm.SM);
            BanEvo((int)Species.Marowak, 0, pkm => pkm.IsUntraded && pkm.SM);
            BanEvo((int)Species.Exeggutor, 0, pkm => pkm.IsUntraded && pkm.SM);
        }

        private void FixEvoTreeSS()
        {
            // Gigantamax Pikachu, Meowth-0, and Eevee are prevented from evolving.
            // Raichu cannot be evolved to the Alolan variant at this time.
            BanEvo((int)Species.Raichu, 0, pkm => pkm is IGigantamax {CanGigantamax: true});
            BanEvo((int)Species.Raichu, 1, pkm => (pkm is IGigantamax {CanGigantamax: true}) || pkm.Version is (int)GO or >= (int)GP);
            BanEvo((int)Species.Persian, 0, pkm => pkm is IGigantamax {CanGigantamax: true});
            BanEvo((int)Species.Persian, 1, pkm => pkm is IGigantamax {CanGigantamax: true});
            BanEvo((int)Species.Perrserker, 0, pkm => pkm is IGigantamax {CanGigantamax: true});

            BanEvo((int)Species.Exeggutor, 1, pkm => pkm.Version is (int)GO or >= (int)GP);
            BanEvo((int)Species.Marowak, 1, pkm => pkm.Version is (int)GO or >= (int)GP);
            BanEvo((int)Species.Weezing, 0, pkm => pkm.Version >= (int)SW);
            BanEvo((int)Species.MrMime, 0, pkm => pkm.Version >= (int)SW);

            foreach (var s in GetEvolutions((int)Species.Eevee, 0)) // Eeveelutions
                BanEvo(s, 0, pkm => pkm is IGigantamax {CanGigantamax: true});
        }

        private void FixEvoTreeLA()
        {
        }

        private void FixEvoTreeBS()
        {
            BanEvo((int)Species.Glaceon, 0, pkm => pkm.CurrentLevel == pkm.Met_Level); // Ice Stone is unreleased, requires Route 217 Ice Rock Level Up instead
            BanEvo((int)Species.Milotic, 0, pkm => pkm is IContestStats { CNT_Beauty: < 170 } || pkm.CurrentLevel == pkm.Met_Level); // Prism Scale is unreleased, requires 170 Beauty Level Up instead
        }

        private void BanEvo(int species, int form, Func<PKM, bool> func)
        {
            var key = GetLookupKey(species, form);
            var node = Lineage[key];
            foreach (var link in node)
                link.IsBanned = func;
        }

        #endregion

        /// <summary>
        /// Gets a list of evolutions for the input <see cref="PKM"/> by checking each evolution in the chain.
        /// </summary>
        /// <param name="pkm">Pokémon data to check with.</param>
        /// <param name="maxLevel">Maximum level to permit before the chain breaks.</param>
        /// <param name="maxSpeciesOrigin">Maximum species ID to permit within the chain.</param>
        /// <param name="skipChecks">Ignores an evolution's criteria, causing the returned list to have all possible evolutions.</param>
        /// <param name="minLevel">Minimum level to permit before the chain breaks.</param>
        public EvoCriteria[] GetValidPreEvolutions(PKM pkm, byte maxLevel, int maxSpeciesOrigin = -1, bool skipChecks = false, byte minLevel = 1)
        {
            if (maxSpeciesOrigin <= 0)
                maxSpeciesOrigin = GetMaxSpeciesOrigin(pkm);
            if (pkm.IsEgg && !skipChecks)
            {
                return new[]
                {
                    new EvoCriteria{ Species = (ushort)pkm.Species, Form = (byte)pkm.Form, LevelMax = maxLevel, LevelMin = maxLevel },
                };
            }

            // Shedinja's evolution case can be a little tricky; hard-code handling.
            if (pkm.Species == (int)Species.Shedinja && maxLevel >= 20 && (!pkm.HasOriginalMetLocation || minLevel < maxLevel))
            {
                var min = Math.Max(minLevel, (byte)20);
                return new[]
                {
                    new EvoCriteria { Species = (ushort)Species.Shedinja, LevelMax = maxLevel, LevelMin = min, Method = EvolutionType.LevelUp },
                    new EvoCriteria { Species = (ushort)Species.Nincada, LevelMax = maxLevel, LevelMin = minLevel },
                };
            }

            return GetExplicitLineage(pkm, maxLevel, skipChecks, maxSpeciesOrigin, minLevel);
        }

        public bool IsSpeciesDerivedFrom(int species, int form, int otherSpecies, int otherForm, bool ignoreForm = true)
        {
            var evos = GetEvolutionsAndPreEvolutions(species, form);
            foreach (var evo in evos)
            {
                var s = evo & 0x3FF;
                if (s != otherSpecies)
                    continue;
                if (ignoreForm)
                    return true;
                var f = evo >> 11;
                return f == otherForm;
            }
            return false;
        }

        /// <summary>
        /// Gets all species the <see cref="species"/>-<see cref="form"/> can evolve to &amp; from, yielded in order of increasing evolution stage.
        /// </summary>
        /// <param name="species">Species ID</param>
        /// <param name="form">Form ID</param>
        /// <returns>Enumerable of species IDs (with the Form IDs included, left shifted by 11).</returns>
        public IEnumerable<int> GetEvolutionsAndPreEvolutions(int species, int form)
        {
            foreach (var s in GetPreEvolutions(species, form))
                yield return s;
            yield return species;
            foreach (var s in GetEvolutions(species, form))
                yield return s;
        }

        public int GetBaseSpeciesForm(int species, int form, int skip = 0)
        {
            var chain = GetEvolutionsAndPreEvolutions(species, form);
            foreach (var c in chain)
            {
                if (skip == 0)
                    return c;
                skip--;
            }
            return species | (form << 11);
        }

        /// <summary>
        /// Gets all species the <see cref="species"/>-<see cref="form"/> can evolve from, yielded in order of increasing evolution stage.
        /// </summary>
        /// <param name="species">Species ID</param>
        /// <param name="form">Form ID</param>
        /// <returns>Enumerable of species IDs (with the Form IDs included, left shifted by 11).</returns>
        public IEnumerable<int> GetPreEvolutions(int species, int form)
        {
            int index = GetLookupKey(species, form);
            var node = Lineage[index];
            foreach (var method in node)
            {
                var s = method.Species;
                if (s == 0)
                    continue;
                var f = method.Form;
                var preEvolutions = GetPreEvolutions(s, f);
                foreach (var preEvo in preEvolutions)
                    yield return preEvo;
                yield return s | (f << 11);
            }
        }

        /// <summary>
        /// Gets all species the <see cref="species"/>-<see cref="form"/> can evolve to, yielded in order of increasing evolution stage.
        /// </summary>
        /// <param name="species">Species ID</param>
        /// <param name="form">Form ID</param>
        /// <returns>Enumerable of species IDs (with the Form IDs included, left shifted by 11).</returns>
        public IEnumerable<int> GetEvolutions(int species, int form)
        {
            int format = Game - Gen1 + 1;
            int index = format < 7 ? species : Personal.GetFormIndex(species, form);
            var evos = Entries[index];
            foreach (var method in evos)
            {
                var s = method.Species;
                if (s == 0)
                    continue;
                var f = method.GetDestinationForm(form);
                yield return s | (f << 11);
                var nextEvolutions = GetEvolutions(s, f);
                foreach (var nextEvo in nextEvolutions)
                    yield return nextEvo;
            }
        }

        /// <summary>
        /// Generates the reverse evolution path for the input <see cref="pkm"/>.
        /// </summary>
        /// <param name="pkm">Entity data</param>
        /// <param name="maxLevel">Maximum level</param>
        /// <param name="skipChecks">Skip the secondary checks that validate the evolution</param>
        /// <param name="maxSpeciesOrigin">Clamp for maximum species ID</param>
        /// <param name="minLevel">Minimum level</param>
        private EvoCriteria[] GetExplicitLineage(PKM pkm, byte maxLevel, bool skipChecks, int maxSpeciesOrigin, byte minLevel)
        {
            var species = pkm.Species;
            var form = pkm.Form;
            var lvl = maxLevel;
            var first = new EvoCriteria { Species = (ushort)species, Form = (byte)form, LevelMax = lvl };

            const int maxEvolutions = 3;
            Span<EvoCriteria> evos = stackalloc EvoCriteria[maxEvolutions];
            evos[0] = first;

            switch (species)
            {
                case (int)Species.Silvally: form = 0;
                    break;
            }

            // There aren't any circular evolution paths, and all lineages have at most 3 evolutions total.
            // There aren't any convergent evolution paths, so only yield the first connection.
            int ctr = 1;
            while (true)
            {
                var key = GetLookupKey(species, form);
                var node = Lineage[key];

                bool oneValid = false;
                foreach (var link in node)
                {
                    if (link.IsEvolutionBanned(pkm) && !skipChecks)
                        continue;

                    var evo = link.Method;
                    if (!evo.Valid(pkm, lvl, skipChecks))
                        continue;

                    if (evo.RequiresLevelUp && minLevel >= lvl)
                        break; // impossible evolution

                    oneValid = true;
                    UpdateMinValues(evos[..ctr], evo, minLevel);

                    species = link.Species;
                    form = link.Form;
                    evos[ctr++] = evo.GetEvoCriteria((ushort)species, (byte)form, lvl);
                    if (evo.RequiresLevelUp)
                        lvl--;
                    break;
                }
                if (!oneValid)
                    break;
            }

            // Remove future gen pre-evolutions; no Munchlax from a Gen3 Snorlax, no Pichu from a Gen1-only Raichu, etc
            ref var last = ref evos[ctr - 1];
            if (last.Species > maxSpeciesOrigin)
            {
                for (int i = 0; i < ctr; i++)
                {
                    if (evos[i].Species > maxSpeciesOrigin)
                        continue;
                    ctr--;
                    break;
                }
            }

            // Last species is the wild/hatched species, the minimum level is because it has not evolved from previous species
            var result = evos[..ctr];
            last = ref result[^1];
            last = last with { LevelMin = minLevel, LevelUpRequired = 0 };

            // Rectify minimum levels
            for (int i = result.Length - 2; i >= 0; i--)
            {
                ref var evo = ref result[i];
                var prev = result[i + 1];
                var min = (byte)Math.Max(prev.LevelMin + evo.LevelUpRequired, evo.LevelMin);
                evo = evo with { LevelMin = min };
            }

            return result.ToArray();
        }

        private static void UpdateMinValues(Span<EvoCriteria> evos, EvolutionMethod evo, byte minLevel)
        {
            ref var last = ref evos[^1];
            if (!evo.RequiresLevelUp)
            {
                // Evolutions like elemental stones, trade, etc
                last = last with { LevelMin = minLevel };
                return;
            }
            if (evo.Level == 0)
            {
                // Friendship based Level Up Evolutions, Pichu -> Pikachu, Eevee -> Umbreon, etc
                last = last with { LevelMin = (byte)(minLevel + 1) };

                // Raichu from Pikachu would have a minimum level of 1; accounting for Pichu (level up required) results in a minimum level of 2
                if (evos.Length > 1)
                {
                    ref var first = ref evos[0];
                    if (!first.RequiresLvlUp)
                        first = first with { LevelMin = (byte)(minLevel + 1) };
                }
            }
            else // level up evolutions
            {
                last = last with { LevelMin = evo.Level };

                if (evos.Length > 1)
                {
                    ref var first = ref evos[0];
                    if (first.RequiresLvlUp)
                    {
                        // Pokemon like Crobat, its minimum level is Golbat minimum level + 1
                        if (first.LevelMin <= evo.Level)
                            first = first with {LevelMin = (byte)(evo.Level + 1) };
                    }
                    else
                    {
                        // Pokemon like Nidoqueen who evolve with an evolution stone, minimum level is prior evolution minimum level
                        if (first.LevelMin < evo.Level)
                            first = first with { LevelMin = evo.Level };
                    }
                }
            }
            last = last with { LevelUpRequired = evo.RequiresLevelUp ? (byte)1 : (byte)0 };
        }

        /// <summary>
        /// Links a <see cref="EvolutionMethod"/> to the source <see cref="Species"/> and <see cref="Form"/> that the method can be triggered from.
        /// </summary>
        private sealed class EvolutionLink
        {
            public readonly int Species;
            public readonly int Form;
            public readonly EvolutionMethod Method;
            public Func<PKM, bool>? IsBanned { private get; set; }

            public EvolutionLink(int species, int form, EvolutionMethod method)
            {
                Species = species;
                Form = form;
                Method = method;
            }

            /// <summary>
            /// Checks if the <see cref="Method"/> is allowed.
            /// </summary>
            /// <param name="pkm">Entity to check</param>
            /// <returns>True if banned, false if allowed.</returns>
            public bool IsEvolutionBanned(PKM pkm) => IsBanned != null && IsBanned(pkm);
        }
    }
}
