using System;
using System.Collections.Generic;
using System.Text;
using static PKHeX.Core.Species;
using static PKHeX.Core.BattleTemplateParseErrorType;

namespace PKHeX.Core;

/// <summary>
/// Logic for exporting and importing <see cref="PKM"/> data in Pokémon Showdown's text format.
/// </summary>
public sealed class ShowdownSet : IBattleTemplate
{
    private const char ItemSplit = '@';
    private const int MAX_SPECIES = (int)MAX_COUNT - 1;
    private const int MaxMoveCount = 4;
    private const string DefaultLanguage = BattleTemplateLocalization.DefaultLanguage; // English
    private static BattleTemplateLocalization DefaultStrings => BattleTemplateLocalization.Default;

    private static ReadOnlySpan<ushort> DashedSpecies =>
    [
        (int)NidoranF, (int)NidoranM,
        (int)HoOh,
        (int)Jangmoo, (int)Hakamoo, (int)Kommoo,
        (int)TingLu, (int)ChienPao, (int)WoChien, (int)ChiYu,
    ];

    public ushort Species { get; private set; }
    public EntityContext Context { get; private set; } = RecentTrainerCache.Context;
    public string Nickname { get; private set; } = string.Empty;
    public byte? Gender { get; private set; }
    public int HeldItem { get; private set; }
    public int Ability { get; private set; } = -1;
    public byte Level { get; private set; } = Experience.MaxLevel;
    public bool Shiny { get; private set; }
    public byte Friendship { get; private set; } = 255;
    public Nature Nature { get; private set; } = Nature.Random;
    public string FormName { get; private set; } = string.Empty;
    public byte Form { get; private set; }
    public int[] EVs { get; } = [00, 00, 00, 00, 00, 00];
    public int[] IVs { get; } = [31, 31, 31, 31, 31, 31];
    public sbyte HiddenPowerType { get; private set; } = -1;
    public MoveType TeraType { get; private set; } = MoveType.Any;
    public ushort[] Moves { get; } = [0, 0, 0, 0];
    public bool CanGigantamax { get; private set; }
    public byte DynamaxLevel { get; private set; } = 10;

    /// <summary>
    /// Any lines that failed to be parsed.
    /// </summary>
    public readonly List<BattleTemplateParseError> InvalidLines = new(0);

    private void LogError(BattleTemplateParseErrorType type, ReadOnlySpan<char> text = default)
        => InvalidLines.Add(new(type, text.ToString()));

    /// <summary>
    /// Loads a new <see cref="ShowdownSet"/> from the input string.
    /// </summary>
    /// <param name="input">Single-line string which will be split before loading.</param>
    /// <param name="localization">Localization to parse the lines with.</param>
    public ShowdownSet(ReadOnlySpan<char> input, BattleTemplateLocalization? localization = null) => LoadLines(input.EnumerateLines(), localization ?? DefaultStrings);

    /// <summary>
    /// Loads a new <see cref="ShowdownSet"/> from the input string.
    /// </summary>
    /// <param name="lines">Enumerable list of lines.</param>
    /// <param name="localization">Localization to parse the lines with.</param>
    public ShowdownSet(IEnumerable<string> lines, BattleTemplateLocalization? localization = null) => LoadLines(lines, localization ?? DefaultStrings);

    private void LoadLines(SpanLineEnumerator lines, BattleTemplateLocalization localization)
    {
        ParseLines(lines, localization);
        SanitizeResult(localization);
    }

    private void LoadLines(IEnumerable<string> lines, BattleTemplateLocalization localization)
    {
        ParseLines(lines, localization);
        SanitizeResult(localization);
    }

    private void SanitizeResult(BattleTemplateLocalization localization)
    {
        ReviseContextIfPastGenForm(localization.Strings);
        if (localization.Strings.Language == LanguageID.English)
            FormName = ShowdownParsing.GetFormNameFromShowdownFormName(Species, FormName, Ability);
        Form = ShowdownParsing.GetFormFromString(FormName, localization.Strings, Species, Context);

        // Handle edge case with fixed-gender forms.
        if (SpeciesCategory.IsFormGenderSpecific(Species))
            ReviseGenderedForms();
    }

    private void ReviseContextIfPastGenForm(GameStrings strings)
    {
        if (FormName.Length == 0)
            return; // no form name

        if (FormInfo.HasTotemForm(Species) && ShowdownParsing.IsTotemForm(FormName))
            Context = EntityContext.Gen7;
        else if (Species is (int)Pikachu && ShowdownParsing.IsCosplayPikachu(FormName, strings.forms))
            Context = EntityContext.Gen6;
    }

    private void ReviseGenderedForms()
    {
        if (Gender == 1) // Recognized with (F)
        {
            FormName = "F";
            Form = 1;
        }
        else
        {
            FormName = Form == 1 ? "F" : "M";
            Gender = Form;
        }
    }

    // Skip lines that are too short or too long.
    // Longest line is ~74 (Gen2 EVs)
    // Length permitted: 3-80
    // The shortest Pokémon name in Japanese is "ニ" (Ni) which is the name for the Pokémon, Nidoran♂ (male Nidoran). It has only one letter.
    // We will handle this 1-2 letter edge case only if the line is the first line of the set, in the rare chance we are importing for a non-English language?
    private const int MinLength = 3;
    private const int MaxLength = 80;
    private static bool IsLengthOutOfRange(ReadOnlySpan<char> trim) => IsLengthOutOfRange(trim.Length);
    private static bool IsLengthOutOfRange(int length) => (uint)(length - MinLength) > MaxLength - MinLength;

    private void ParseLines(SpanLineEnumerator lines, BattleTemplateLocalization localization)
    {
        int countMoves = 0;
        bool first = true;
        foreach (var line in lines)
        {
            var trim = line.Trim();
            if (IsLengthOutOfRange(trim))
            {
                // Try for other languages just in case.
                if (first && trim.Length != 0)
                {
                    ParseFirstLine(trim, localization.Strings);
                    first = false;
                    continue;
                }
                LogError(LineLength, line);
                continue;
            }

            if (first)
            {
                ParseFirstLine(trim, localization.Strings);
                first = false;
                continue;
            }

            ParseLine(trim, ref countMoves, localization);
        }
    }

    private void ParseLines(IEnumerable<string> lines, BattleTemplateLocalization localization)
    {
        int countMoves = 0;
        bool first = true;
        foreach (var line in lines)
        {
            ReadOnlySpan<char> trim = line.Trim();
            if (IsLengthOutOfRange(trim))
            {
                // Try for other languages just in case.
                if (first && trim.Length != 0)
                {
                    ParseFirstLine(trim, localization.Strings);
                    first = false;
                    continue;
                }
                LogError(LineLength, line);
                continue;
            }

            if (first)
            {
                ParseFirstLine(trim, localization.Strings);
                first = false;
                continue;
            }

            ParseLine(trim, ref countMoves, localization);
        }
    }

    private void ParseLine(ReadOnlySpan<char> line, ref int countMoves, BattleTemplateLocalization localization)
    {
        // Check the first char, to see if it is a move input.
        var firstChar = line[0];
        if (firstChar is '-' or '–')
        {
            ParseMoveLine(line, ref countMoves, localization);
            return;
        }

        // Check if it is a directional move input.
        var dirMove = BattleTemplateConfig.GetMoveDisplay(MoveDisplayStyle.Directional);
        var dirMoveIndex = dirMove.IndexOf(firstChar);
        if (dirMoveIndex != -1)
        {
            ParseMoveLineIndex(line, ref countMoves, localization, dirMoveIndex);
            return;
        }

        // Check if it is an Ability selection/line
        if (firstChar is '[' or '@')
        {
            ParseLineAbilityBracket(line, localization.Strings);
            return;
        }

        // Otherwise, tokenized input line.
        var token = localization.Config.TryParse(line, out var value);
        if (token == BattleTemplateToken.None)
        {
            LogError(TokenUnknown, line);
            return;
        }
        var valid = ParseEntry(token, value, localization);
        if (!valid)
            LogError(TokenFailParse, line);
    }

    private void ParseMoveLine(ReadOnlySpan<char> line, ref int countMoves, BattleTemplateLocalization localization)
    {
        if (countMoves >= MaxMoveCount)
        {
            LogError(MoveCountTooMany, line);
            return;
        }
        TryAddMoveAtIndex(line, ref countMoves, localization, countMoves);
    }

    private void ParseMoveLineIndex(ReadOnlySpan<char> line, ref int countMoves, BattleTemplateLocalization localization, int index)
    {
        if (Moves[index] != 0)
        {
            LogError(MoveSlotAlreadyUsed, line);
            return;
        }
        TryAddMoveAtIndex(line, ref countMoves, localization, index);
    }

    private void TryAddMoveAtIndex(ReadOnlySpan<char> line, ref int countMoves, BattleTemplateLocalization localization, int index)
    {
        var strings = localization.Strings;
        var movelist = strings.movelist;
        var moveString = ParseLineMove(line, strings);
        int move = StringUtil.FindIndexIgnoreCase(movelist, moveString);
        var moves = Moves.AsSpan();
        if (move < 0)
        {
            LogError(MoveUnrecognized, moveString);
        }
        else if (moves.Contains((ushort)move))
        {
            LogError(MoveDuplicate, moveString);
        }
        else
        {
            moves[index] = (ushort)move;
            countMoves++;
        }
    }

    private void ParseLineAbilityBracket(ReadOnlySpan<char> line, GameStrings localizationStrings)
    {
        // Try to peel off Held Item if it is specified.
        var itemStart = line.IndexOf(ItemSplit);
        if (itemStart != -1)
        {
            var itemName = line[(itemStart + 1)..].TrimStart();
            if (!ParseItemName(itemName, localizationStrings))
                LogError(ItemUnrecognized, itemName);
            line = line[..itemStart];
        }

        // Remainder should be [Ability]
        var abilityEnd = line.IndexOf(']');
        if (abilityEnd == -1 || line.Length == 1) // '[' should be present if ']' is; length check.
        {
            LogError(AbilityDeclaration, line);
            return; // invalid line
        }

        var abilityName = line[1..abilityEnd].Trim();
        var abilityIndex = StringUtil.FindIndexIgnoreCase(localizationStrings.abilitylist, abilityName);
        if (abilityIndex < 0)
        {
            LogError(AbilityUnrecognized, abilityName);
            return; // invalid line
        }
        Ability = abilityIndex;
    }

    private bool ParseEntry(BattleTemplateToken token, ReadOnlySpan<char> value, BattleTemplateLocalization localization) => token switch
    {
        BattleTemplateToken.Ability       => ParseLineAbility(value, localization.Strings.abilitylist),
        BattleTemplateToken.Nature        => ParseLineNature(value, localization.Strings.natures),
        BattleTemplateToken.Shiny         => Shiny         = true,
        BattleTemplateToken.Gigantamax    => CanGigantamax = true,
        BattleTemplateToken.HeldItem      => ParseItemName(value, localization.Strings),
        BattleTemplateToken.Nickname      => ParseNickname(value),
        BattleTemplateToken.Gender        => ParseGender(value, localization.Config),
        BattleTemplateToken.Friendship    => ParseFriendship(value),
        BattleTemplateToken.EVs           => ParseLineEVs(value, localization),
        BattleTemplateToken.IVs           => ParseLineIVs(value, localization.Config),
        BattleTemplateToken.Level         => ParseLevel(value),
        BattleTemplateToken.DynamaxLevel  => ParseDynamax(value),
        BattleTemplateToken.TeraType      => ParseTeraType(value, localization.Strings.types),
        _ => false,
    };

    private bool ParseLineAbility(ReadOnlySpan<char> value, ReadOnlySpan<string> abilityNames)
    {
        var index = StringUtil.FindIndexIgnoreCase(abilityNames, value);
        if (index < 0)
        {
            LogError(AbilityUnrecognized, value);
            return false;
        }
        if (Ability != -1 && Ability != index)
        {
            LogError(AbilityAlreadySpecified, value);
            return false;
        }

        Ability = index;
        return true;
    }

    private bool ParseLineNature(ReadOnlySpan<char> value, ReadOnlySpan<string> natureNames)
    {
        var index = StringUtil.FindIndexIgnoreCase(natureNames, value);
        if (index < 0)
            return false;

        var nature = (Nature)index;
        if (!nature.IsFixed())
        {
            LogError(NatureUnrecognized, value);
            return false;
        }
        if (Nature != Nature.Random && Nature != nature)
        {
            LogError(NatureAlreadySpecified, value);
            return false;
        }

        Nature = nature;
        return true;
    }

    private bool ParseNickname(ReadOnlySpan<char> value)
    {
        if (value.Length == 0)
            return false;
        // ignore length, but generally should be <= the Context's max length
        Nickname = value.ToString();
        return true;
    }

    private bool ParseGender(ReadOnlySpan<char> value, BattleTemplateConfig cfg)
    {
        if (value.Equals(cfg.Male, StringComparison.OrdinalIgnoreCase))
        {
            Gender = EntityGender.Male;
            return true;
        }
        if (value.Equals(cfg.Female, StringComparison.OrdinalIgnoreCase))
        {
            Gender = EntityGender.Female;
            return true;
        }
        return false;
    }

    private bool ParseLevel(ReadOnlySpan<char> value)
    {
        if (!byte.TryParse(value.Trim(), out var val))
            return false;
        if ((uint)val is 0 or > Experience.MaxLevel)
            return false;
        Level = val;
        return true;
    }

    private bool ParseFriendship(ReadOnlySpan<char> value)
    {
        if (!byte.TryParse(value.Trim(), out var val))
            return false;
        Friendship = val;
        return true;
    }

    private bool ParseDynamax(ReadOnlySpan<char> value)
    {
        Context = EntityContext.Gen8;
        var val = Util.ToInt32(value);
        if ((uint)val > 10)
            return false;
        DynamaxLevel = (byte)val;
        return true;
    }

    private bool ParseTeraType(ReadOnlySpan<char> value, ReadOnlySpan<string> types)
    {
        Context = EntityContext.Gen9;
        var val = StringUtil.FindIndexIgnoreCase(types, value);
        if (val < 0)
            return false;
        if (val == TeraTypeUtil.StellarTypeDisplayStringIndex)
            val = TeraTypeUtil.Stellar;
        TeraType = (MoveType)val;
        return true;
    }

    /// <summary>
    /// Gets the standard Text representation of the set details.
    /// </summary>
    public string Text => GetText(BattleTemplateExportSettings.Showdown);

    /// <inheritdoc cref="Text"/>
    /// <param name="language">Language code</param>
    public string GetText(string language = DefaultLanguage) => GetText(new BattleTemplateExportSettings(language));

    /// <inheritdoc cref="Text"/>
    /// <param name="language">Language ID</param>
    public string GetText(LanguageID language) => GetText(new BattleTemplateExportSettings(language));

    /// <inheritdoc cref="Text"/>
    /// <param name="settings">Export settings</param>
    public string GetText(in BattleTemplateExportSettings settings)
    {
        if (Species is 0 or > MAX_SPECIES)
            return string.Empty;

        var result = GetSetLines(settings);
        return string.Join(Environment.NewLine, result);
    }

    /// <inheritdoc cref="GetSetLines(in BattleTemplateExportSettings)"/>
    public List<string> GetSetLines(string language = DefaultLanguage) => GetSetLines(new BattleTemplateExportSettings(language));

    /// <summary>
    /// Gets all lines comprising the exported set details.
    /// </summary>
    /// <param name="settings">Export settings</param>
    /// <returns>List of lines comprising the set</returns>
    public List<string> GetSetLines(in BattleTemplateExportSettings settings)
    {
        var result = new List<string>(DefaultListAllocation);
        if (settings.Order.Length == 0)
            return result;
        GetSetLines(result, settings);
        return result;
    }

    /// <inheritdoc cref="GetSetLines(in BattleTemplateExportSettings)"/>
    public void GetSetLines(List<string> result, in BattleTemplateExportSettings settings)
    {
        var tokens = settings.Order;
        foreach (var token in tokens)
            PushToken(token, result, settings);
    }

    private string GetStringFirstLine(string form, in BattleTemplateExportSettings settings)
    {
        var strings = settings.Localization.Strings;
        var speciesList = strings.specieslist;
        if (Species >= speciesList.Length)
            return string.Empty; // invalid species

        string specForm = speciesList[Species];
        var gender = Gender;
        if (form.Length != 0)
        {
            specForm += $"-{form.Replace("Mega ", "Mega-")}";
        }
        else if (Species == (int)NidoranM)
        {
            specForm = specForm.Replace("♂", "-M");
            if (gender != EntityGender.Female)
                gender = null;
        }
        else if (Species == (int)NidoranF)
        {
            specForm = specForm.Replace("♀", "-F");
            if (gender != EntityGender.Male)
                gender = null;
        }

        var nickname = Nickname;
        if (settings.IsTokenInExport(BattleTemplateToken.Nickname))
            nickname = string.Empty; // omit nickname if not in export

        var result = GetSpeciesNickname(specForm, nickname, Species, Context);

        // Append Gender if not default/random.
        if (gender < EntityGender.Genderless && !settings.IsTokenInExport(BattleTemplateToken.Gender))
        {
            if (gender is 0)
                result += $" {FirstLineMale}";
            else if (gender is 1)
                result += $" {FirstLineFemale}";
        }

        // Append item if specified.
        if (HeldItem > 0 && !settings.IsTokenInExport(BattleTemplateToken.HeldItem) && !settings.IsTokenInExport(BattleTemplateToken.AbilityHeldItem))
        {
            var items = strings.GetItemStrings(Context);
            if ((uint)HeldItem < items.Length)
                result += $" {ItemSplit} {items[HeldItem]}";
        }
        return result;
    }

    private static string GetSpeciesNickname(string specForm, string nickname, ushort species, EntityContext context)
    {
        if (nickname.Length == 0 || nickname == specForm)
            return specForm;
        bool isNicknamed = SpeciesName.IsNicknamedAnyLanguage(species, nickname, context);
        if (!isNicknamed)
            return specForm;
        return $"{nickname} ({specForm})";
    }

    private void PushToken(BattleTemplateToken token, List<string> result, in BattleTemplateExportSettings settings)
    {
        var cfg = settings.Localization.Config;
        var strings = settings.Localization.Strings;

        switch (token)
        {
            // Core
            case BattleTemplateToken.FirstLine:
                result.Add(GetStringFirstLine(FormName, settings));
                break;
            case BattleTemplateToken.Ability when (uint)Ability < strings.Ability.Count:
                result.Add(cfg.Push(BattleTemplateToken.Ability, strings.Ability[Ability]));
                break;
            case BattleTemplateToken.Nature when (uint)Nature < strings.Natures.Count:
                result.Add(cfg.Push(token, strings.Natures[(byte)Nature]));
                break;

            case BattleTemplateToken.Moves:
                GetStringMoves(result, settings);
                break;

            // Stats
            case BattleTemplateToken.Level when Level != Experience.MaxLevel:
                result.Add(cfg.Push(token, Level));
                break;
            case BattleTemplateToken.Friendship when Friendship != 255:
                result.Add(cfg.Push(token, Friendship));
                break;
            case BattleTemplateToken.IVs:
                var maxIV = Context.Generation() < 3 ? 15 : 31;
                if (!IVs.AsSpan().ContainsAnyExcept(maxIV))
                    break; // skip if all IVs are maxed
                var nameIVs = cfg.GetStatDisplay(settings.StatsIVs);
                var ivs = GetStringStats(IVs, maxIV, nameIVs);
                if (ivs.Length != 0)
                    result.Add(cfg.Push(BattleTemplateToken.IVs, ivs));
                break;

            // EVs
            case BattleTemplateToken.EVsWithNature:
            case BattleTemplateToken.EVsAppendNature:
            case BattleTemplateToken.EVs when EVs.AsSpan().ContainsAnyExcept(0):
                AddEVs(result, settings, token);
                break;

            // Boolean
            case BattleTemplateToken.Shiny when Shiny:
                result.Add(cfg.Push(token));
                break;

            // Gen8
            case BattleTemplateToken.DynamaxLevel when Context == EntityContext.Gen8 && DynamaxLevel != 10:
                result.Add(cfg.Push(token, DynamaxLevel));
                break;
            case BattleTemplateToken.Gigantamax when Context == EntityContext.Gen8 && CanGigantamax:
                result.Add(cfg.Push(token));
                break;

            // Gen9
            case BattleTemplateToken.TeraType when Context == EntityContext.Gen9 && TeraType != MoveType.Any:
                if ((uint)TeraType <= TeraTypeUtil.MaxType) // Fairy
                    result.Add(cfg.Push(BattleTemplateToken.TeraType, strings.Types[(int)TeraType]));
                else if ((uint)TeraType == TeraTypeUtil.Stellar)
                    result.Add(cfg.Push(BattleTemplateToken.TeraType, strings.Types[TeraTypeUtil.StellarTypeDisplayStringIndex]));
                break;

            // Edge Cases
            case BattleTemplateToken.HeldItem when HeldItem > 0:
                var itemNames = strings.GetItemStrings(Context);
                if ((uint)HeldItem < itemNames.Length)
                    result.Add(cfg.Push(token, itemNames[HeldItem]));
                break;
            case BattleTemplateToken.Nickname when !string.IsNullOrWhiteSpace(Nickname):
                result.Add(cfg.Push(token, Nickname));
                break;
            case BattleTemplateToken.Gender when Gender != EntityGender.Genderless:
                result.Add(cfg.Push(token, Gender == 0 ? cfg.Male : cfg.Female));
                break;

            case BattleTemplateToken.AbilityHeldItem when Ability >= 0 || HeldItem > 0:
                result.Add(GetAbilityHeldItem(strings, Ability, HeldItem, Context));
                break;
        }
    }

    private void AddEVs(List<string> result, in BattleTemplateExportSettings settings, BattleTemplateToken token)
    {
        var cfg = settings.Localization.Config;
        var nameEVs = cfg.GetStatDisplay(settings.StatsEVs);
        var line = token switch
        {
            BattleTemplateToken.EVsWithNature => GetStringStatsNatureAmp(EVs, 0, nameEVs, Nature),
            BattleTemplateToken.EVsAppendNature => GetStringStatsNatureAmp(EVs, 0, nameEVs, Nature),
            _ => GetStringStats(EVs, 0, nameEVs),
        };
        if (token is BattleTemplateToken.EVsAppendNature && Nature.IsFixed())
            line += $" ({settings.Localization.Strings.natures[(int)Nature]})";
        result.Add(cfg.Push(BattleTemplateToken.EVs, line));
    }

    private static string GetAbilityHeldItem(GameStrings strings, int ability, int item, EntityContext context)
    {
        var abilityNames = strings.abilitylist;

        if ((uint)ability >= abilityNames.Length)
            ability = 0; // invalid ability
        var abilityName = abilityNames[ability];

        var itemNames = strings.GetItemStrings(context);
        if ((uint)item >= itemNames.Length)
            item = 0; // invalid item
        var itemName = itemNames[item];

        if (ability <= 0)
            return $"{ItemSplit} {itemName}";
        if (item <= 0)
            return $"[{abilityName}]";
        return $"[{abilityName}] {ItemSplit} {itemName}";
    }

    /// <inheritdoc cref="GetStringStats{T}(ReadOnlySpan{T}, T, StatDisplayConfig)"/>
    /// <remarks>Appends the nature amplification to the stat values, if not a neutral nature.</remarks>
    public static string GetStringStatsNatureAmp<T>(ReadOnlySpan<T> stats, T ignoreValue, StatDisplayConfig statNames, Nature nature) where T : IEquatable<T>
    {
        var (plus, minus) = NatureAmp.GetNatureModification(nature);
        if (plus == minus)
            return GetStringStats(stats, ignoreValue, statNames); // neutral nature won't appear any different

        // Shift as HP is not affected by nature.
        plus++;
        minus++;

        var count = stats.Length;
        if (!statNames.AlwaysShow)
        {
            for (int i = 0; i < stats.Length; i++)
            {
                if (stats[i].Equals(ignoreValue) && i != plus && i != minus)
                    count--; // ignore unused stats
            }
        }
        if (count == 0)
            return string.Empty;

        var result = new StringBuilder();
        int ctr = 0;
        for (int i = 0; i < stats.Length; i++)
        {
            var statIndex = GetStatIndexStored(i);
            var statValue = stats[statIndex];

            var hideValue = statValue.Equals(ignoreValue) && !statNames.AlwaysShow;
            if (hideValue && statIndex != plus && statIndex != minus)
                continue; // ignore unused stats
            var amp = statIndex == plus ? "+" : statIndex == minus ? "-" : string.Empty;
            if (ctr++ != 0)
                result.Append(statNames.Separator);
            statNames.Format(result, i, statValue, amp, hideValue);
        }
        return result.ToString();
    }

    /// <summary>
    /// Gets the string representation of the stats.
    /// </summary>
    /// <param name="stats">Stats to display</param>
    /// <param name="ignoreValue">Value to ignore</param>
    /// <param name="statNames">Stat names to use</param>
    public static string GetStringStats<T>(ReadOnlySpan<T> stats, T ignoreValue, StatDisplayConfig statNames) where T : IEquatable<T>
    {
        var count = stats.Length;
        if (!statNames.AlwaysShow)
        {
            foreach (var stat in stats)
            {
                if (stat.Equals(ignoreValue))
                    count--; // ignore unused stats
            }
        }
        if (count == 0)
            return string.Empty;

        var result = new StringBuilder();
        int ctr = 0;
        for (int i = 0; i < stats.Length; i++)
        {
            var statIndex = GetStatIndexStored(i);
            var statValue = stats[statIndex];
            if (statValue.Equals(ignoreValue) && !statNames.AlwaysShow)
                continue;
            if (ctr++ != 0)
                result.Append(statNames.Separator);
            statNames.Format(result, i, statValue);
        }
        return result.ToString();
    }

    private void GetStringMoves(List<string> result, in BattleTemplateExportSettings settings)
    {
        var strings = settings.Localization.Strings;
        var moveNames = strings.movelist;
        var style = settings.Moves;
        var prefixes = BattleTemplateConfig.GetMoveDisplay(style);

        var added = 0;
        for (var i = 0; i < Moves.Length; i++)
        {
            var move = Moves[i];
            if (move == 0 && !(style is MoveDisplayStyle.Directional && added != 0))
                continue;
            if (move >= moveNames.Length)
                continue;
            var moveName = moveNames[move];

            string line;
            if (move != (int)Move.HiddenPower || HiddenPowerType == -1)
            {
                line = $"{prefixes[i]} {moveName}";
            }
            else
            {
                var type = 1 + HiddenPowerType; // skip Normal
                var typeName = strings.Types[type];
                line = $"{prefixes[i]} {moveName} [{typeName}]";
            }

            result.Add(line);
            added++;
        }
    }

    private static int GetStatIndexStored(int displayIndex) => displayIndex switch
    {
        3 => 4,
        4 => 5,
        5 => 3,
        _ => displayIndex,
    };

    /// <summary>
    /// Forces some properties to indicate the set for future display values.
    /// </summary>
    /// <param name="pk">PKM to convert to string</param>
    public void InterpretAsPreview(PKM pk)
    {
        if (pk.Format <= 2) // Nature preview from IVs
            Nature = Experience.GetNatureVC(pk.EXP);
    }

    /// <summary>
    /// Converts the <see cref="PKM"/> data into an importable set format for Pokémon Showdown.
    /// </summary>
    /// <param name="pk">PKM to convert to string</param>
    /// <param name="localization">Localization to parse the lines with.</param>
    /// <returns>New ShowdownSet object representing the input <see cref="pk"/></returns>
    public ShowdownSet(PKM pk, BattleTemplateLocalization? localization = null)
    {
        localization ??= DefaultStrings;
        if (pk.Species == 0)
            return;

        Context = pk.Context;

        Nickname = pk.Nickname;
        Species = pk.Species;
        HeldItem = pk.HeldItem;
        Ability = pk.Ability;
        pk.GetEVs(EVs);
        pk.GetIVs(IVs);

        var moves = Moves.AsSpan();
        pk.GetMoves(moves);
        if (moves.Contains((ushort)Move.HiddenPower))
            HiddenPowerType = (sbyte)HiddenPower.GetType(IVs, Context);

        Nature = pk.StatNature;
        Gender = pk.Gender < 2 ? pk.Gender : (byte)2;
        Friendship = pk.CurrentFriendship;
        Level = pk.CurrentLevel;
        Shiny = pk.IsShiny;

        if (pk is PK8 g) // Only set Gigantamax if it is a PK8
        {
            CanGigantamax = g.CanGigantamax;
            DynamaxLevel = g.DynamaxLevel;
        }

        if (pk is ITeraType t)
            TeraType = t.TeraType;
        if (pk is IHyperTrain h)
        {
            for (int i = 0; i < 6; i++)
            {
                if (h.IsHyperTrained(i))
                    IVs[i] = pk.MaxIV;
            }
        }

        FormName = ShowdownParsing.GetStringFromForm(Form = pk.Form, localization.Strings, Species, Context);
    }

    private void ParseFirstLine(ReadOnlySpan<char> first, GameStrings strings)
    {
        int itemSplit = first.IndexOf(ItemSplit);
        if (itemSplit != -1)
        {
            var itemName = first[(itemSplit + 1)..].TrimStart();
            var speciesName = first[..itemSplit].TrimEnd();

            if (!ParseItemName(itemName, strings))
                LogError(ItemUnrecognized, itemName);
            ParseFirstLineNoItem(speciesName, strings);
        }
        else
        {
            ParseFirstLineNoItem(first, strings);
        }
    }

    private bool ParseItemName(ReadOnlySpan<char> itemName, GameStrings strings)
    {
        var initial = Context;
        if (TryGetItem(itemName, strings, initial))
            return true;
        if (TryGetItem(itemName, strings, EntityContext.Gen3))
            return true;
        if (TryGetItem(itemName, strings, EntityContext.Gen2))
            return true;
        if (initial is EntityContext.Gen3 or EntityContext.Gen2 && TryGetItem(itemName, strings, Latest.Context))
            return true;
        return false;
    }

    private bool TryGetItem(ReadOnlySpan<char> itemName, GameStrings strings, EntityContext context)
    {
        var items = strings.GetItemStrings(context);
        var item = StringUtil.FindIndexIgnoreCase(items, itemName);
        if (item < 0)
            return false;
        Context = context;
        HeldItem = item;
        return true;
    }

    private const string FirstLineMale = "(M)";
    private const string FirstLineFemale = "(F)";

    private void ParseFirstLineNoItem(ReadOnlySpan<char> line, GameStrings strings)
    {
        // Gender Detection
        if (line.EndsWith(FirstLineMale, StringComparison.Ordinal))
        {
            line = line[..^3].TrimEnd();
            Gender = 0;
        }
        else if (line.EndsWith(FirstLineFemale, StringComparison.Ordinal))
        {
            line = line[..^3].TrimEnd();
            Gender = 1;
        }

        // Nickname Detection
        if (line.IndexOf('(') != -1 && line.IndexOf(')') != -1)
            ParseSpeciesNickname(line, strings);
        else
            ParseSpeciesForm(line, strings);
    }

    private const string Gmax = "-Gmax";

    /// <summary>
    /// Average count of lines in a Showdown set.
    /// </summary>
    /// <remarks>Optimization to skip 1 size update allocation (from 4). Usually first-line, ability, (ivs, evs, shiny, level) 4*moves </remarks>
    public const int DefaultListAllocation = 8;

    private bool ParseSpeciesForm(ReadOnlySpan<char> speciesLine, GameStrings strings)
    {
        speciesLine = speciesLine.Trim();
        if (speciesLine.Length == 0)
            return false;

        if (speciesLine.EndsWith(Gmax, StringComparison.Ordinal))
        {
            CanGigantamax = true;
            speciesLine = speciesLine[..^Gmax.Length];
        }

        var speciesIndex = StringUtil.FindIndexIgnoreCase(strings.specieslist, speciesLine);
        if (speciesIndex > 0)
        {
            // success, nothing else !
            Species = (ushort)speciesIndex;
            return true;
        }

        // Form string present.
        int end = speciesLine.IndexOf('-');
        if (end < 0)
            return false;

        speciesIndex = StringUtil.FindIndexIgnoreCase(strings.specieslist, speciesLine[..end]);
        if (speciesIndex > 0)
        {
            Species = (ushort)speciesIndex;
            FormName = speciesLine[(end + 1)..].ToString();
            return true;
        }

        // failure to parse, check edge cases
        foreach (var e in DashedSpecies)
        {
            var sn = strings.Species[e];
            if (!speciesLine.StartsWith(sn.Replace("♂", "-M").Replace("♀", "-F"), StringComparison.Ordinal))
                continue;
            Species = e;
            FormName = speciesLine[sn.Length..].ToString();
            return true;
        }

        // Version Megas
        end = speciesLine[Math.Max(0, end - 1)..].LastIndexOf('-');
        if (end < 0)
            return false;

        speciesIndex = StringUtil.FindIndexIgnoreCase(strings.specieslist, speciesLine[..end]);
        if (speciesIndex > 0)
        {
            Species = (ushort)speciesIndex;
            FormName = speciesLine[(end + 1)..].ToString();
            return true;
        }
        return false;
    }

    private void ParseSpeciesNickname(ReadOnlySpan<char> line, GameStrings strings)
    {
        // Entering into this method requires both ( and ) to be present within the input line.
        int index = line.LastIndexOf('(');
        ReadOnlySpan<char> species;
        ReadOnlySpan<char> nickname;
        if (index > 1) // parenthesis value after: Nickname (Species), correct.
        {
            nickname = line[..index].TrimEnd();
            species = line[(index + 1)..];
            if (species.Length != 0 && species[^1] == ')')
                species = species[..^1];
        }
        else // parenthesis value before: (Species) Nickname, incorrect
        {
            int start = index + 1;
            int end = line.LastIndexOf(')');
            var tmp = line[start..end];
            if (end < line.Length - 2)
            {
                nickname = line[(end + 2)..];
                species = tmp;
            }
            else // (Species), or garbage
            {
                species = tmp;
                nickname = [];
            }
        }

        if (ParseSpeciesForm(species, strings))
            Nickname = nickname.ToString();
        else if (ParseSpeciesForm(nickname, strings))
            Nickname = species.ToString();
    }

    private ReadOnlySpan<char> ParseLineMove(ReadOnlySpan<char> line, GameStrings strings)
    {
        line = line[1..].TrimStart();

        // Discard any multi-move options; keep only first.
        var option = line.IndexOf('/');
        if (option != -1)
            line = line[..option].TrimEnd();

        var moveString = line;

        var hiddenPowerName = strings.Move[(int)Move.HiddenPower];
        if (!moveString.StartsWith(hiddenPowerName, StringComparison.OrdinalIgnoreCase))
            return moveString; // regular move

        if (moveString.Length == hiddenPowerName.Length)
            return hiddenPowerName;

        // Defined Hidden Power
        var type = GetHiddenPowerType(moveString[(hiddenPowerName.Length + 1)..]);
        var types = strings.HiddenPowerTypes;
        int hpVal = StringUtil.FindIndexIgnoreCase(types, type); // Get HP Type
        if (hpVal == -1)
            return hiddenPowerName;

        HiddenPowerType = (sbyte)hpVal;
        var maxIV = Context.Generation() < 3 ? 15 : 31;
        if (IVs.AsSpan().ContainsAnyExcept(maxIV))
        {
            if (!HiddenPower.SetIVsForType(hpVal, IVs, Context))
                LogError(HiddenPowerIncompatibleIVs, type);
        }
        else if (hpVal >= 0)
        {
            HiddenPower.SetIVs(hpVal, IVs, Context); // Alter IVs
        }
        else
        {
            LogError(HiddenPowerUnknownType, type);
        }
        return hiddenPowerName;
    }

    private static ReadOnlySpan<char> GetHiddenPowerType(ReadOnlySpan<char> line)
    {
        var type = line.Trim();
        if (type.Length == 0)
            return type;

        // Allow for both (Type) and [Type]
        if (type[0] == '(' && type[^1] == ')')
            return type[1..^1].Trim();
        if (type[0] == '[' && type[^1] == ']')
            return type[1..^1].Trim();

        return type;
    }

    private bool ParseLineEVs(ReadOnlySpan<char> line, BattleTemplateLocalization localization)
    {
        // If nature is present, parse it first.
        var nature = line.IndexOf('(');
        if (nature != -1)
        {
            var natureName = line[(nature + 1)..];
            var end = natureName.IndexOf(')');
            if (end == -1)
            {
                LogError(NatureEffortAmpDeclaration, natureName);
                return false; // invalid line
            }
            natureName = natureName[..end].Trim();
            var natureIndex = StringUtil.FindIndexIgnoreCase(localization.Strings.natures, natureName);
            if (natureIndex == -1)
            {
                LogError(NatureEffortAmpUnknown, natureName);
                return false; // invalid line
            }

            if (Nature != Nature.Random) // specified in a separate Nature line
                LogError(NatureEffortAmpAlreadySpecified, natureName);
            else
                Nature = (Nature)natureIndex;

            line = line[..nature].TrimEnd();
        }

        var result = localization.Config.TryParseStats(line, EVs);
        var success = result.IsParsedAllStats;
        if (result is { HasAmps: false })
            return success;

        // Use the amp nature ONLY if nature was not specified.
        // Only indicate invalid if it differs from the current nature.
        var currentNature = Nature;
        result.TreatAmpsAsSpeedNotLast();
        var ampNature = AdjustNature(result.Plus, result.Minus);
        success &= ampNature;
        if (ampNature && currentNature != Nature.Random && currentNature != Nature)
        {
            LogError(NatureEffortAmpConflictNature);
            Nature = currentNature; // revert to original
        }
        return success;
    }

    private bool ParseLineIVs(ReadOnlySpan<char> line, BattleTemplateConfig config)
    {
        // Parse stats, with unspecified name representation (try all).
        var result = config.TryParseStats(line, IVs);
        return result.IsParsedAllStats;
    }

    private bool AdjustNature(sbyte plus, sbyte minus)
    {
        if (plus == StatParseResult.NoStatAmp)
            LogError(NatureAmpNoPlus);
        if (minus == StatParseResult.NoStatAmp)
            LogError(NatureAmpNoMinus);
        else
            Nature = NatureAmp.CreateNatureFromAmps(plus, minus);
        return true;
    }
}
