using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using static PKHeX.Core.LanguageID;

namespace PKHeX.Core;

/// <summary>
/// Repository of localized game strings for a given <see cref="LanguageID"/>.
/// </summary>
public sealed class GameStrings : IBasicStrings
{
    // PKM Info
    public readonly string[] specieslist, movelist, itemlist, abilitylist, types, natures, forms,
        memories, genloc, feeling6, feeling8, intensity,
        trainingbags, trainingstage, characteristics,
        groundtiletypes, balllist, gamelist, pokeblocks, ribbons;

    private readonly string[] g4items, g3coloitems, g3xditems, g3items, g2items, g1items;

    // Met Locations
    public readonly LocationSet0 Gen2, Gen3, CXD;
    public readonly LocationSet4 Gen4;
    public readonly LocationSet6 Gen5, Gen6, Gen7, Gen7b, Gen8, Gen8a, Gen8b, Gen9, Gen9a;

    // Misc
    public readonly string[] wallpapernames, puffs, walkercourses;
    public readonly string[] uggoods, ugspheres, ugtraps, ugtreasures;
    public readonly string[] seals, accessories, backdrops, poketchapps;
    public readonly string[] console3ds, languageNames;
    private readonly string LanguageFilePrefix;

    public ReadOnlySpan<string> HiddenPowerTypes => types.AsSpan(1, HiddenPower.TypeCount);
    public readonly RibbonStrings Ribbons;

    public LanguageID Language { get; }
    public string EggName { get; }
    public IReadOnlyList<string> Species => specieslist;
    public IReadOnlyList<string> Item => itemlist;
    public IReadOnlyList<string> Move => movelist;
    public IReadOnlyList<string> Ability => abilitylist;
    public IReadOnlyList<string> Types => types;
    public IReadOnlyList<string> Natures => natures;

    private string[] Get(string ident) => GameLanguage.GetStrings(ident, LanguageFilePrefix);
    private const string NPC = "NPC";
    private const string EmptyIndex = "---";

    /// <summary>
    /// Item IDs that correspond to the <see cref="Ball"/> value.
    /// </summary>
    private static ReadOnlySpan<ushort> Items_Ball =>
    [
        0000, 0001, 0002, 0003, 0004, 0005, 0006, 0007, 0008, 0009,
        0010, 0011, 0012, 0013, 0014, 0015, 0016, 0492, 0493, 0494,
        0495, 0496, 0497, 0498, 0499, 0576, 0851,
        1785, 1710, 1711,
        1712, 1713, 1746, 1747, 1748, 1749, 1750, 1771,
    ];

    internal GameStrings(string langFilePrefix)
    {
        Language = GameLanguage.GetLanguage(LanguageFilePrefix = langFilePrefix);

        ribbons = Get("ribbons");
        Ribbons = new(ribbons);

        // Past Generation strings
        g3items = Get("ItemsG3");
        g3coloitems = GetG3CXD(g3items, "ItemsG3Colosseum");
        g3xditems = GetG3CXD(g3items, "ItemsG3XD");

        g2items = Get("ItemsG2");
        g1items = Get("ItemsG1");
        Gen2 = new(Get("gsc_00000"));
        Gen3 = new(Get("rsefrlg_00000"));
        CXD = new(Get("cxd_00000"));

        // Less than 10% of location values are unique.
        // Just mark them with the ID if they aren't empty.
        AppendLocationIndex(CXD.Met0.AsSpan(0, 227));

        // Current Generation strings
        natures = Util.GetNaturesList(langFilePrefix);
        types = Get("types");
        abilitylist = Get("abilities");

        movelist = Get("moves");
        // Differentiate Physical/Special Z-Moves
        for (int i = 622; i < 658; i++)
        {
            const string p = " (P)";
            const string s = " (S)";
            bool isPhysicalZMove = (i & 1) == 0;
            movelist[i] += isPhysicalZMove ? p : s;
        }

        itemlist = Get("items");
        characteristics = Get("character");
        specieslist = Get("species");
        wallpapernames = Get("wallpaper");
        groundtiletypes = Get("groundtile");
        gamelist = Get("games");

        var balls = Items_Ball;
        balllist = new string[balls.Length];
        for (int i = 0; i < balllist.Length; i++)
            balllist[i] = itemlist[balls[i]];

        pokeblocks = Get("pokeblock");
        forms = Get("forms");
        memories = Get("memories");
        feeling6 = Get("feeling6");
        feeling8 = Get("feeling");
        intensity = Get("intensity");
        genloc = Get("genloc");
        trainingbags = Get("trainingbag");
        trainingstage = Get("supertraining");
        puffs = Get("puff");

        walkercourses = Get("walkercourses");

        uggoods = Get("uggoods");
        ugspheres = Get("ugspheres");
        ugtraps = Get("ugtraps");
        ugtreasures = Get("ugtreasures");

        seals = Get("seals");
        accessories = Get("accessories");
        backdrops = Get("backdrops");
        poketchapps = Get("poketchapps");
        console3ds = Get("console3ds");
        languageNames = Get("language");

        EggName = specieslist[0];
        Gen4 = Get4("hgss");
        Gen5 = Get6("bw2");
        Gen6 = Get6("xy");
        Gen7 = Get6("sm");

        Gen7b = Get6("gg", Gen7.Met3, Gen7.Met6);
        Gen8 = Get6("swsh");
        Gen8a = Get6("la");
        Gen8b = Get6("bdsp");
        Gen9 = Get6("sv");
        Gen9a = Get6("za");

        Sanitize();

        g4items = [..itemlist];
        Get("mail4").CopyTo(g4items, 137);
    }

    private LocationSet4 Get4([ConstantExpected] string ident)
    {
        var met0 = Get($"{ident}_00000");
        var met2 = Get($"{ident}_02000");
        var met3 = Get($"{ident}_03000");
        return new LocationSet4(met0, met2, met3);
    }

    private LocationSet6 Get6([ConstantExpected] string ident)
    {
        var met0 = Get($"{ident}_00000");
        var met3 = Get($"{ident}_30000");
        var met4 = Get($"{ident}_40000");
        var met6 = Get($"{ident}_60000");
        return new LocationSet6(met0, met3, met4, met6);
    }

    private LocationSet6 Get6([ConstantExpected] string ident, string[] met3, string[] met6)
    {
        var met0 = Get($"{ident}_00000");
        var met4 = Get($"{ident}_40000");
        return new LocationSet6(met0, met3, met4, met6);
    }

    private string[] GetG3CXD(ReadOnlySpan<string> arr, [ConstantExpected] string fileName)
    {
        // Concatenate the Gen3 Item list with the CXD item array; CXD items starting at index 500.
        var item500 = Get(fileName);
        var result = new string[500 + item500.Length];
        for (int i = arr.Length; i < result.Length; i++)
            result[i] = string.Empty;
        arr.CopyTo(result);
        item500.CopyTo(result, 500);
        return result;
    }

    private static void AppendLocationIndex(Span<string> names)
    {
        for (int i = 0; i < names.Length; i++)
        {
            ref var str = ref names[i];
            if (str.Length != 0)
                str += $" [{i:000}]";
        }
    }

    private void Sanitize()
    {
        SanitizeItemNames();
        SanitizeMetLocations();

        // De-duplicate the Calyrex ability names
        abilitylist[(int)Core.Ability.AsOneI] += $" ({specieslist[(int)Core.Species.Glastrier]})";
        abilitylist[(int)Core.Ability.AsOneG] += $" ({specieslist[(int)Core.Species.Spectrier]})";
        // De-duplicate the Ogerpon ability names
        abilitylist[(int)Core.Ability.EmbodyAspect0] += $" ({forms[FormConverter.MaskTeal]})";
        abilitylist[(int)Core.Ability.EmbodyAspect1] += $" ({forms[FormConverter.MaskHearthflame]})";
        abilitylist[(int)Core.Ability.EmbodyAspect2] += $" ({forms[FormConverter.MaskWellspring]})";
        abilitylist[(int)Core.Ability.EmbodyAspect3] += $" ({forms[FormConverter.MaskCornerstone]})";

        // Replace the Egg Name with ---; egg name already stored to eggname
        specieslist[0] = EmptyIndex;
        // Fix (None) tags
        var none = $"({itemlist[0]})";
        abilitylist[0] = itemlist[0] = movelist[0] = Gen6.Met0[0] = Gen5.Met0[0] = Gen4.Met0[0] = CXD.Met0[0] = puffs[0] = none;
    }

    private void SanitizeItemNames()
    {
        // Fix Item Names (Duplicate entries)
        var HM06 = itemlist[425];
        var HM0 = HM06[..^1]; // language ambiguous!
        itemlist[426] = $"{HM0}7 (G4)";
        itemlist[427] = $"{HM0}8 (G4)";
        itemlist[456] += " (HG/SS)"; // S.S. Ticket
        itemlist[736] += " (OR/AS)"; // S.S. Ticket
        itemlist[463] += " (DPPt)"; // Storage Key
        itemlist[734] += " (OR/AS)"; // Storage Key
        itemlist[476] += " (HG/SS)"; // Basement Key
        itemlist[723] += " (OR/AS)"; // Basement Key
        itemlist[621] += " (M)"; // Xtransceiver
        itemlist[626] += " (F)"; // Xtransceiver
        itemlist[629] += " (2)"; // DNA Splicers
        itemlist[636] += " (M)"; // Dropped Item
        itemlist[637] += " (F)"; // Dropped Item
        itemlist[707] += " (2)"; // Travel Trunk
        itemlist[713] += " (2)"; // Alt Bike
        itemlist[714] += " (2)"; // Holo Caster
        itemlist[729] += " (1)"; // Meteorite
        itemlist[740] += " (2)"; // Contest Costume
        itemlist[751] += " (2)"; // Meteorite
        itemlist[771] += " (3)"; // Meteorite
        itemlist[772] += " (4)"; // Meteorite
        itemlist[842] += " (SM)"; // Fishing Rod
        itemlist[945] += " (2)"; // Used Solarizer
        itemlist[946] += " (2)"; // Used Lunarizer

        itemlist[873] += " (GP/GE)"; // S.S. Ticket
        itemlist[459] += " (HG/SS)"; // Parcel
        itemlist[467] += " (Pt)"; // Secret Key
        itemlist[475] += " (HG/SS)"; // Card Key
        itemlist[894] += " (GP)"; // Leaf Letter
        itemlist[895] += " (GE)"; // Leaf Letter

        // some languages have same names for other items!
        itemlist[878] += " (GP/GE)"; // Lift Key (Elevator Key=700)
        itemlist[479] += " (HG/SS)"; // Lost Item (Dropped Item=636)

        // Append Z-Crystal Key Item differentiator
        foreach (var i in ItemStorage7USUM.ZCrystalKey)
            itemlist[i] += " [Z]";

        itemlist[0121] += " (1)"; // Pokémon Box Link
        itemlist[1075] += " (2)"; // Pokémon Box Link

        itemlist[1080] += " (SW/SH)"; // Fishing Rod

        itemlist[1081] += " (1)"; // Rotom Bike
        itemlist[1266] += " (2)"; // Rotom Bike
        itemlist[1585] += " (3)"; // Rotom Bike
        itemlist[1586] += " (4)"; // Rotom Bike

        itemlist[1590] += " (1)"; // Reins of Unity
        itemlist[1591] += " (2)"; // Reins of Unity
        itemlist[1607] += " (3)"; // Reins of Unity

        for (int i = 12; i <= 29; i++) // Differentiate DNA Samples
            g3coloitems[500 + i] += $" ({i - 11:00})";
        // differentiate G3 Card Key from Colo
        g3coloitems[500 + 10] += " (COLO)";

        SanitizeItemsLA(itemlist);
        SanitizeItemsSV(itemlist);
        SanitizeItemsZA(itemlist);

        if (Language is French)
        {
            itemlist[1681] += " (LA)"; // Galet Noir       dup with 617 (Dark Stone | Black Tumblestone)
            itemlist[1262] += " (G8)"; // Nouilles         dup with 1933 (Noodles | Instant Noodles)
            itemlist[1263] += " (G8)"; // Steak Haché      dup with 1925 (Hamburger | Precooked Burger)
        }
        else if (Language is Japanese)
        {
            itemlist[1693] += " (LA)"; // むしよけスプレー   dup with 79 (Repel)
            itemlist[1716] += " (LA)"; // ビビリだま        dup with 847 (Adrenaline Orb | Scatter Bang)
            itemlist[1717] += " (LA)"; // けむりだま        dup with 228 (Smoke Ball | Smoke Bomb)
        }

        itemlist[464] += " (G4)"; // Secret Medicine
        itemlist[1763] += " (LA)"; // Secret Medicine
    }

    private void SanitizeItemsZA(Span<string> items)
    {
        // Seed of Mastery
        items[1622] += " (LA)";
        items[2558] += " (ZA)";
        // Canari Plushes
        Canari(items[2620..]); // Red
        Canari(items[2623..]); // Gold
        Canari(items[2626..]); // Pink
        Canari(items[2629..]); // Green
        Canari(items[2632..]); // Blue
        return;
        static void Canari(Span<string> arr)
        {
            arr[0] += " (1)";
            arr[1] += " (2)";
            arr[2] += " (3)";
        }
    }

    private static void SanitizeItemsSV(Span<string> items)
    {
        items[2313] += " (1)"; // Academy Bottle
        items[2314] += " (2)"; // Academy Bottle
        items[2318] += " (1)"; // Academy Cup
        items[2319] += " (2)"; // Academy Cup
        items[2323] += " (1)"; // Academy Tablecloth
        items[2324] += " (2)"; // Academy Tablecloth
        items[2329] += " (1)"; // Academy Ball
        items[2330] += " (2)"; // Academy Ball
        items[0694] += " (G6-8)"; // TM100, not held.

        items[2418] += " (SL)"; // Academy Chairs
        items[2419] += " (VL)"; // Academy Chairs

        items[1834] += " (1)"; // Scarlet Book
        items[1835] += " (1)"; // Violet Book
        items[2555] += " (2)"; // Scarlet Book
        items[2556] += " (2)"; // Violet Book
    }

    private static void SanitizeItemsLA(Span<string> items)
    {
        // Recipes
        items[1784] += " (~)"; // Gigaton Ball
        items[1783] += " (~)"; // Leaden Ball
        items[1753] += " (~)"; // Heavy Ball
        items[1752] += " (~)"; // Jet Ball
        items[1751] += " (~)"; // Wing Ball
        items[1731] += " (~)"; // Twice-Spiced Radish
        items[1730] += " (~)"; // Choice Dumpling
        items[1729] += " (~)"; // Swap Snack
        items[1677] += " (~)"; // Aux Powerguard
        items[1676] += " (~)"; // Aux Evasion
        items[1675] += " (~)"; // Dire Hit
        items[1674] += " (~)"; // Aux Guard
        items[1673] += " (~)"; // Aux Power
        items[1671] += " (~)"; // Stealth Spray
        items[1670] += " (~)"; // Max Elixir
        items[1669] += " (~)"; // Max Ether
        items[1668] += " (~)"; // Max Revive
        items[1667] += " (~)"; // Revive
        items[1666] += " (~)"; // Full Heal
        items[1665] += " (~)"; // Jubilife Muffin
        items[1664] += " (~)"; // Old Gateau
        items[1663] += " (~)"; // Superb Remedy
        items[1662] += " (~)"; // Fine Remedy
        items[1661] += " (~)"; // Remedy
        items[1660] += " (~)"; // Full Restore
        items[1659] += " (~)"; // Max Potion
        items[1658] += " (~)"; // Hyper Potion
        items[1657] += " (~)"; // Super Potion
        items[1656] += " (~)"; // Potion
        items[1655] += " (~)"; // Salt Cake
        items[1654] += " (~)"; // Bean Cake
        items[1653] += " (~)"; // Grain Cake
        items[1652] += " (~)"; // Honey Cake
        items[1650] += " (~)"; // Mushroom Cake
        items[1649] += " (~)"; // Star Piece
        items[1648] += " (~)"; // Sticky Glob
        items[1647] += " (~)"; // Scatter Bang
        items[1646] += " (~)"; // Smoke Bomb
        items[1644] += " (~)"; // Pokéshi Doll
        items[1643] += " (~)"; // Feather Ball
        items[1642] += " (~)"; // Ultra Ball
        items[1641] += " (~)"; // Great Ball
        items[1640] += " (~)"; // Poké Ball

        // Items
        items[1616] += " (LA)"; // Dire Hit
        items[1689] += " (LA)"; // Snowball
        items[1710] += " (LA)"; // Poké Ball
        items[1711] += " (LA)"; // Great Ball
        items[1712] += " (LA)"; // Ultra Ball
        items[1748] += " (LA)"; // Heavy Ball

        // Key Items
        items[1622] += " (-)"; // Poké Ball
        items[1765] += " (1)"; // Lost Satchel
        items[1766] += " (2)"; // Lost Satchel
        items[1767] += " (3)"; // Lost Satchel
        items[1768] += " (4)"; // Lost Satchel
        items[1769] += " (5)"; // Lost Satchel
    }

    private void SanitizeMetLocations()
    {
        // Fix up some of the Location strings to make them more descriptive
        SanitizeMetGen4(Gen4);
        SanitizeMetGen5(Gen5);
        SanitizeMetGen6(Gen6);
        SanitizeMetGen7(Gen7);
        SanitizeMetGen7b(Gen7b);
        SanitizeMetGen8(Gen8);
        SanitizeMetGen8a(Gen8a);
        SanitizeMetGen8b(Gen8b);
        SanitizeMetGen9(Gen9);
        SanitizeMetGen9a(Gen9a);

        if (Language is Italian or Spanish or SpanishL)
        {
            // Campeonato Mundial duplicates
            for (int i = 28; i < 35; i++)
                Gen6.Met4[i] += " (-)";

            // Evento de Videojuegos -- first as duplicate
            Gen6.Met4[35] += " (-)";
            Gen7.Met4[38] += " (-)";
            Gen7b.Met4[27] += " (-)";
        }

        if (Language is Korean)
        {
            // Pokémon Ranger duplicate (should be Ranger Union)
            Gen5.Met4[71] += " (-)";
        }
    }

    private void SanitizeMetGen4(LocationSet4 set)
    {
        set.Met0[054] += " (D/P/Pt)"; // Victory Road
        set.Met0[221] += " (HG/SS)"; // Victory Road

        // German language duplicate; handle for all since it can be confused.
        set.Met0[104] += " (D/P/Pt)"; // Vista Lighthouse
        set.Met0[212] += " (HG/SS)"; // Lighthouse

        set.Met2[1] += $" ({NPC})";     // Anything from an NPC
        set.Met2[2] += $" ({EggName})"; // Egg From Link Trade
    }

    private void SanitizeMetGen5(LocationSet6 set)
    {
        const string bw = " (B/W)";
        set.Met0[36] = $"{set.Met0[84]}/{set.Met0[36]}"; // Cold Storage in B/W = PWT in B2/W2
        set.Met0[40] += bw; // Victory Road in B/W
        set.Met0[134] += " (B2/W2)"; // Victory Road in B2/W2
        // B2/W2 Entries from 76 to 105 are for Entralink in B/W
        for (int i = 76; i < 106; i++)
            set.Met0[i] += bw;

        // Collision between 40002 (legal) and 00002 (illegal) "Faraway place"
        set.Met0[2] += " (-)";

        // Collision between 00069 (legal) and 30014 (illegal) "Entralink"
        if (set.Met3[14] == set.Met0[69])
            set.Met3[14] += " (-)";

        for (int i = 97; i < 109; i++)
            set.Met4[i] += $" ({i - 97})";

        // Localize the Poketransfer to the language (30001)
        set.Met3[1] = GetTransporterName(Language);
        set.Met3[2] += $" ({NPC})";             // Anything from an NPC
        set.Met3[3] += $" ({EggName})";         // Link Trade (Egg)

        // Zorua/Zoroark events
        set.Met3[10] = $"{specieslist[251]} ({specieslist[570]} 1)"; // Celebi's Zorua Event
        set.Met3[11] = $"{specieslist[251]} ({specieslist[570]} 2)"; // Celebi's Zorua Event
        set.Met3[12] = $"{specieslist[571]} (1)"; // Zoroark
        set.Met3[13] = $"{specieslist[571]} (2)"; // Zoroark

        set.Met6[3] += $" ({EggName})";  // Egg Treasure Hunter/Breeder, whatever...
    }

    private void SanitizeMetGen6(LocationSet6 set)
    {
        // Add in the sub-location if available.
        for (int i = 8; i <= 136; i += 2)
        {
            var nextLoc = set.Met0[i + 1];
            if (nextLoc.Length == 0)
                continue;
            set.Met0[i + 1] = string.Empty;
            set.Met0[i] += $" ({nextLoc})";
        }

        set.Met0[104] += " (X/Y)";      // Victory Road
        set.Met0[106] += " (X/Y)";      // Pokémon League
        set.Met0[202] += " (OR/AS)";    // Pokémon League
        set.Met0[298] += " (OR/AS)";    // Victory Road
        set.Met3[1] += $" ({NPC})";     // Anything from an NPC
        set.Met3[2] += $" ({EggName})"; // Egg From Link Trade

        // Collision between 40002 (legal) and 00004 (illegal) "Faraway place"
        set.Met0[4] += " (-)";

        for (int i = 63; i <= 69; i++)
            set.Met4[i] += $" ({i - 62})";
    }

    private void SanitizeMetGen7(LocationSet6 set)
    {
        // Sun/Moon duplicates -- elaborate!
        for (int i = 6; i < set.Met0.Length; i += 2)
        {
            if (i is >= 194 and < 198)
                continue; // Skip Island Names (unused)
            var nextLoc = set.Met0[i + 1];
            if (nextLoc.Length == 0)
                continue;
            set.Met0[i + 1] = string.Empty;
            set.Met0[i] += $" ({nextLoc})";
        }
        set.Met0[32] += " (2)";
        set.Met0[102] += " (2)";

        // Collision between 40002 (legal) and 00004 (illegal) "Faraway place"
        set.Met0[4] += " (-)";

        // Collision between 30012 (legal, in Gen8+) and 40084 (illegal) "Pokémon GO"
        if (set.Met3[12] == set.Met4[84])
            set.Met4[84] += " (-)";

        set.Met3[1] += $" ({NPC})";      // Anything from an NPC
        set.Met3[2] += $" ({EggName})";  // Egg From Link Trade
        for (int i = 3; i <= 6; i++) // distinguish first set of regions (unused) from second (used)
            set.Met3[i] += " (-)";

        for (int i = 59; i < 66; i++) // distinguish Event year duplicates
            set.Met4[i] += " (-)";
    }

    private static void SanitizeMetGen7b(LocationSet6 set)
    {
        for (int i = 48; i < 55; i++) // distinguish Event year duplicates
            set.Met4[i] += " (-)";

        // Collision between 40002 (legal) and 00002 (illegal) "Faraway place"
        set.Met0[2] += " (-)";
        set.Met4[73] += " (-)"; // Pokémon GO -- duplicate with 30000's entry
     // set.Met3[12] += " (-)"; // Pokémon GO -- duplicate with 40000's entry
    }

    private void SanitizeMetGen8(LocationSet6 set)
    {
        // SW/SH duplicates -- elaborate!
        for (int i = 88; i < set.Met0.Length; i += 2)
        {
            var nextLoc = set.Met0[i + 1];
            if (nextLoc.Length == 0)
                continue;
            set.Met0[i + 1] = string.Empty;
            set.Met0[i] += $" ({nextLoc})";
        }

        set.Met3[1] += $" ({NPC})";      // Anything from an NPC
        set.Met3[2] += $" ({EggName})";  // Egg From Link Trade
        for (int i = 3; i <= 6; i++) // distinguish first set of regions (unused) from second (used)
            set.Met3[i] += " (-)";
        set.Met3[19] += " (?)"; // Kanto for the third time

        for (int i = 55; i < 61; i++) // distinguish Event year duplicates
            set.Met4[i] += " (-)";
        set.Met4[30] += " (-)"; // a Video game Event (in spanish etc.) -- duplicate with line 39
        set.Met4[53] += " (-)"; // a Pokémon event -- duplicate with line 37

        // Collision between 40002 (legal) and 00004 (illegal) "Faraway place"
        set.Met0[4] += " (-)";
        set.Met4[81] += " (-)"; // Pokémon GO -- duplicate with 30000's entry
        set.Met4[86] += " (-)"; // Pokémon HOME -- duplicate with 30000's entry
     // set.Met3[12] += " (-)"; // Pokémon GO -- duplicate with 40000's entry
     // set.Met3[18] += " (-)"; // Pokémon HOME -- duplicate with 40000's entry
    }

    private void SanitizeMetGen8b(LocationSet6 set)
    {
        set.Met3[1] += $" ({NPC})";      // Anything from an NPC
        set.Met3[2] += $" ({EggName})";  // Egg From Link Trade

        Deduplicate(set.Met0, 00000);
        Deduplicate(set.Met3, 30000);
        Deduplicate(set.Met4, 40000);
        Deduplicate(set.Met6, 60000);

        set.Met4[81] += " (-)"; // Pokémon GO -- duplicate with 30000's entry
        set.Met4[86] += " (-)"; // Pokémon HOME -- duplicate with 30000's entry
     // set.Met3[12] += " (-)"; // Pokémon GO -- duplicate with 40000's entry
     // set.Met3[18] += " (-)"; // Pokémon HOME -- duplicate with 40000's entry
    }

    private void SanitizeMetGen8a(LocationSet6 set)
    {
        set.Met0[31] += " (2)"; // in Floaro Gardens
        set.Met3[1] += $" ({NPC})";      // Anything from an NPC
        set.Met3[2] += $" ({EggName})";  // Egg From Link Trade
        for (int i = 3; i <= 6; i++) // distinguish first set of regions (unused) from second (used)
            set.Met3[i] += " (-)";
        set.Met3[19] += " (?)"; // Kanto for the third time

        set.Met4[30] += " (-)"; // a Video game Event (in spanish etc.) -- duplicate with line 39
        set.Met4[53] += " (-)"; // a Pokémon event -- duplicate with line 37

        set.Met4[81] += " (-)"; // Pokémon GO -- duplicate with 30000's entry
        set.Met4[86] += " (-)"; // Pokémon HOME -- duplicate with 30000's entry
     // set.Met3[12] += " (-)"; // Pokémon GO -- duplicate with 40000's entry
     // set.Met3[18] += " (-)"; // Pokémon HOME -- duplicate with 40000's entry

        // Collision between 40002 (legal) and 00004 (illegal) "Faraway place"
        if (set.Met0[4] == set.Met4[2])
            set.Met0[4] += " (-)";
        for (int i = 55; i <= 60; i++) // distinguish second set of YYYY Event from the first
            set.Met4[i] += " (-)";

        if (Language is English or Spanish or SpanishL or German or Italian or French)
        {
            // Final four locations are not nouns, rather the same location reference (at the...) as prior entries.
            set.Met0[152] += " (152)"; // Galaxy Hall
            set.Met0[153] += " (153)"; // Front Gate
            set.Met0[154] += " (154)"; // Farm
            set.Met0[155] += " (155)"; // Training Grounds
        }
    }

    private void SanitizeMetGen9(LocationSet6 set)
    {
        var m = set.Met0;
        m[110] += " (1)"; // Area Zero
        m[112] += " (2)"; // Area Zero
        m[114] += " (3)"; // Area Zero
        m[116] += " (4)"; // Area Zero
        m[124] += " (5)"; // Area Zero
        m[126] += " (6)"; // Area Zero
        m[128] += " (7)"; // Area Zero

        m[040] += " (1)"; // Casseroya Lake
        m[108] += " (2)"; // Casseroya Lake
        m[034] += " (1)"; // East Province (Area One)
        m[104] += " (2)"; // East Province (Area One)
        m[038] += " (1)"; // Glaseado Mountain
        m[042] += " (2)"; // Glaseado Mountain
        m[068] += " (3)"; // Glaseado Mountain
        m[008] += " (1)"; // Mesagoza
        m[072] += " (2)"; // Mesagoza
        m[074] += " (3)"; // Mesagoza
        m[044] += " (1)"; // North Province (Area Three)
        m[102] += " (2)"; // North Province (Area Three)
        m[047] += " (1)"; // North Province (Area Two)
        m[098] += " (2)"; // North Province (Area Two)
        m[016] += " (1)"; // South Province (Area Six)
        m[066] += " (2)"; // South Province (Area Six)
        m[030] += " (1)"; // Tagtree Thicket
        m[106] += " (2)"; // Tagtree Thicket
        m[022] += " (1)"; // West Province (Area One)
        m[100] += " (2)"; // West Province (Area One)
        m[052] += " (1)"; // Zero Gate
        m[054] += " (2)"; // Zero Gate
        m[118] += " (1)"; // Zero Lab
        m[120] += " (2)"; // Zero Lab
        m[122] += " (3)"; // Zero Lab

        m[144] += " (1)"; // Oni Mountain
        m[147] += " (2)"; // Oni Mountain
        m[149] += " (3)"; // Oni Mountain
        m[150] += " (4)"; // Oni Mountain
        m[169] += " (5)"; // Oni Mountain

        m[152] += " (1)"; // Crystal Pool
        m[154] += " (2)"; // Crystal Pool
        m[153] += " (1)"; // Oni Mountain Summit
        m[155] += " (2)"; // Oni Mountain Summit
        m[164] += " (1)"; // Kitakami Wilds
        m[167] += " (2)"; // Kitakami Wilds

        m[196] += " (1)"; // Area Zero Underdepths
        m[198] += " (2)"; // Area Zero Underdepths

        set.Met3[1] += $" ({NPC})";      // Anything from an NPC
        set.Met3[2] += $" ({EggName})";  // Egg From Link Trade
        for (int i = 3; i <= 6; i++) // distinguish first set of regions (unused) from second (used)
            set.Met3[i] += " (-)";
        set.Met3[19] += " (?)"; // Kanto for the third time

        for (int i = 49; i <= 54; i++) // distinguish Event year duplicates
            set.Met4[i] += " (-)";

        set.Met4[27] += " (-)"; // a Video game Event (in spanish etc.) -- duplicate with line 36
        set.Met4[48] += " (-)"; // a Pokémon event -- duplicate with line 34

        // Collision between 40002 (legal) and 00004 (illegal) "Faraway place"
        set.Met0[4] += " (-)";
        set.Met4[73] += " (-)"; // Pokémon GO -- duplicate with 30000's entry
        set.Met4[78] += " (-)"; // Pokémon HOME -- duplicate with 30000's entry
     // set.Met3[12] += " (-)"; // Pokémon GO -- duplicate with 40000's entry
     // set.Met3[18] += " (-)"; // Pokémon HOME -- duplicate with 40000's entry
    }

    private static void SanitizeMetGen9a(LocationSet6 set)
    {
        // ZA: Truncated list to remove all after 235 (no encounters there).

        // Resolve collisions across sets
        set.Met0[4] += " (-)"; // Faraway Place -- duplicate with 40002
        set.Met3[27] += " (-)"; // (Laboratory) -- duplicate with 103 in all languages besides English
        set.Met3[28] += " (-)"; // Wild Zone 11 (30028) -- duplicate with 210
        set.Met3[29] += " (-)"; // (Museum) -- duplicate with 100 in all languages besides English
        set.Met3[30] += " (-)"; // Académie Étoile (30030) -- duplicate with 76
        set.Met3[31] += " (-)"; // Magenta Sector 2 (30031) -- duplicate with 59
        set.Met3[32] += " (-)"; // North Boulevard (30032) -- duplicate with 65
        set.Met4[65] += " (-)"; // Pokémon GO -- duplicate with 30012
        set.Met4[70] += " (-)"; // Pokémon HOME -- duplicate with 30018
        set.Met6[05] += " (-)"; // Lumiose City (6005) -- duplicate with 30026

        Deduplicate(set.Met0, 00000);
        Deduplicate(set.Met3, 30000);
        Deduplicate(set.Met4, 40000);
        Deduplicate(set.Met6, 60000);
    }

    private static void Deduplicate(Span<string> arr, int group)
    {
        var counts = new Dictionary<string, int>();

        foreach (var s in arr)
        {
            if (s.Length == 0)
                continue;
            counts.TryGetValue(s, out var value);
            counts[s] = value + 1;
        }

#if !DEBUG
        var maxCounts = new Dictionary<string, int>(counts);
#endif
        for (var i = arr.Length - 1; i >= 0; i--)
        {
#if DEBUG
            arr[i] += $" ({group + i:00000})";
#else
            var s = arr[i];
            if (s.Length == 0)
                continue;
            var count = counts[s]--;
            if (count == 1)
                continue;
            var format = maxCounts[s] switch
            {
                >= 100 => " ({0:000})",
                >=  10 => " ({0:00})",
                     _ => " ({0})",
            };
            arr[i] += string.Format(format, count);
#endif
        }
    }

    public string[] GetItemStrings(EntityContext context, GameVersion version = GameVersion.Any) => context switch
    {
        EntityContext.Gen1 => g1items,
        EntityContext.Gen2 => g2items,
        EntityContext.Gen3 => GetItemStrings3(version),
        EntityContext.Gen4 => g4items, // mail names changed 4->5
        EntityContext.Gen8b => GetItemStrings8b(),
        EntityContext.Gen9 => GetItemStrings9(),
        EntityContext.Gen9a => GetItemStrings9a(),
        _ => itemlist,
    };

    private string[] GetItemStrings8b()
    {
        // Item Indexes 
        var clone = (string[])itemlist.Clone();
        var tm = clone[419][..2];
        for (int i = 420; i <= 427; i++)
            clone[i] = $"{tm}{i - 420 + 93}";

        clone[618] += "(-)"; // TM93
        clone[619] += "(-)"; // TM94
        clone[620] += "(-)"; // TM95
        clone[690] += "(-)"; // TM96
        clone[691] += "(-)"; // TM97
        clone[692] += "(-)"; // TM98
        clone[693] += "(-)"; // TM99
        clone[694] += "(-)"; // TM100
        return clone;
    }

    private string[] GetItemStrings9()
    {
        // in Generation 9, TM #'s are padded to 3 digits; format them appropriately here
        var clone = (string[])itemlist.Clone();
        var span = clone.AsSpan();
        var zero = Language is Japanese or ChineseS or ChineseT ? "０" : "0";
        InsertZero(span[328..420], zero); // 01-92
        InsertZero(span[618..621], zero); // 93-95
        InsertZero(span[690..694], zero); // 96-99
        return clone;

        static void InsertZero(Span<string> arr, string insert)
        {
            // TM #'s are always ending with ##, so insert before the last 2 characters
            foreach (ref var str in arr)
                str = str.Insert(str.Length - 2, insert);
        }
    }

    private string[] GetItemStrings9a()
    {
        // in Generation 9, TM #'s are padded to 3 digits; format them appropriately here
        var clone = (string[])itemlist.Clone();
        var span = clone.AsSpan();
        var zero = Language is Japanese or ChineseS or ChineseT ? '０' : '0';
        var prefix = span[328].AsSpan(0, 2);

        InsertZero(prefix, span[328..420], zero, 1); // 01-92
        InsertZero(prefix, span[618..621], zero, 93); // 93-95
        InsertZero(prefix, span[690..694], zero, 96); // 96-99
        InsertZero(prefix, span[2160..2290], zero, 100); // 100-229
        return clone;

        static void InsertZero(ReadOnlySpan<char> prefix, Span<string> arr, char zero, int start)
        {
            int i = 0;
            foreach (ref var item in arr)
            {
                var index = start + i;
                var remap = ItemConverter.RemapTechnicalMachineItemName9a;
                if (index >= remap.Length)
                    break;
                index += remap[index];
                item = GetActualName(prefix, index, zero);
                i++;
            }
            arr[i..].Clear();
        }
        static string GetActualName(ReadOnlySpan<char> prefix, int value, char pad)
        {
            Span<char> buffer = stackalloc char[5];
            prefix.CopyTo(buffer);
            var arr = buffer[2..];
            arr.Fill(pad);
            arr[2] += (char)(value % 10); value /= 10;
            arr[1] += (char)(value % 10); value /= 10;
            arr[0] += (char)(value % 10);
            return new string(buffer);
        }
    }

    private string[] GetItemStrings3(GameVersion version)
    {
        switch (version)
        {
            case GameVersion.COLO:
                return g3coloitems;
            case GameVersion.XD:
                return g3xditems;
            default:
                if (EReaderBerrySettings.IsEnigma)
                    return g3items;

                var g3ItemsWithEBerry = (string[])g3items.Clone();
                g3ItemsWithEBerry[175] = GetEnigmaBerryName3(Language, EReaderBerrySettings.Name);
                return g3ItemsWithEBerry;
        }
    }

    private static string GetEnigmaBerryName3(LanguageID language, string berryName) => string.Format(language switch
    {
        Japanese => "{0}のみ",
        English => "{0} BERRY",
        German => "{0}BEERE",
        French => "BAIE {0}",
        Italian => "BACCA{0}",
        Spanish => "BAYA {0}",
        Korean => "{0}열매",
        ChineseS => "{0}果",
        ChineseT => "{0}果",
        _ => throw new ArgumentOutOfRangeException(nameof(language), language, null),
    }, berryName);

    /// <summary>
    /// Gets the location name for the specified parameters.
    /// </summary>
    /// <param name="isEggLocation">Location is from the <see cref="PKM.EggLocation"/></param>
    /// <param name="location">Location value</param>
    /// <param name="format">Current <see cref="PKM.Format"/></param>
    /// <param name="generation"><see cref="PKM.Generation"/> of origin</param>
    /// <param name="version">Version within <see cref="generation"/>, if needed to differentiate.</param>
    /// <returns>Location name. Potentially an empty string if no location name is known for that location value.</returns>
    public string GetLocationName(bool isEggLocation, ushort location, byte format, byte generation, GameVersion version)
    {
        if (format == 1)
        {
            // Legality binaries have Location IDs that were manually remapped to Gen3 location IDs.
            if (location == 0)
                return string.Empty;
            return Gen3.GetLocationName(location);
        }

        generation = GetGeneration(generation, isEggLocation, format);
        var set = GetLocations(generation, version);
        if (set is null)
            return string.Empty;
        return set.GetLocationName(location);
    }

    private static byte GetGeneration(byte generation, bool isEggLocation, byte format)
    {
        if (format == 2)
            return 2;
        if (format == 3)
            return 3;
        if (generation == 4 && (isEggLocation || format == 4))
            return 4;
        if (generation >= 5)
            return generation;
        if (format >= 5)
            return format;
        return 0; // Nonsensical inputs.
    }

    /// <summary>
    /// Gets the location names array for a specified generation.
    /// </summary>
    /// <param name="generation">Generation to get location names for.</param>
    /// <param name="version">Version of origin</param>
    /// <returns>List of location names.</returns>
    public ILocationSet? GetLocations(byte generation, GameVersion version) => generation switch
    {
        2 => Gen2,
        3 => version is (GameVersion.COLO or GameVersion.XD or GameVersion.CXD) ? CXD : Gen3,
        4 => Gen4,
        5 => Gen5,
        6 => Gen6,
        7 => GameVersion.Gen7b.Contains(version) ? Gen7b : Gen7,

        8 when version is GameVersion.PLA => Gen8a,
        8 when GameVersion.BDSP.Contains(version) => Gen8b,
        8 => Gen8,
        9 when version is GameVersion.ZA => Gen9a,
        9 => Gen9,

        _ => null,
    };

    /// <summary>
    /// Gets the location names array for a specified generation.
    /// </summary>
    /// <param name="generation">Generation to get location names for.</param>
    /// <param name="version">Version of origin</param>
    /// <param name="bankID">BankID used to choose the text bank.</param>
    /// <returns>List of location names.</returns>
    public ReadOnlySpan<string> GetLocationNames(byte generation, GameVersion version, int bankID = 0)
    {
        var set = GetLocations(generation, version);
        if (set is null)
            return [];
        return set.GetLocationNames(bankID);
    }

    /// <summary>
    /// Gets the Met Location display name for the Pokétransporter.
    /// </summary>
    public static string GetTransporterName(LanguageID language) => language switch
    {
        Japanese => "ポケシフター", // ja
        French => "Poké Fret", // fr
        Italian => "Pokétrasporto", // it
        German => "Poképorter", // de
        Spanish => "Pokétransfer", // es
        Korean => "포케시프터", // ko
        ChineseS => "宝可传送", // zh-Hans
        ChineseT => "寶可傳送", // zh-Hant

        _ => "Poké Transfer", // en
    };
}
