using static pkNX.Structures.FlatBuffers.SWSH.EncounterTable8Util.SWSHSlotType;

namespace pkNX.Structures.FlatBuffers.SWSH;

public static class EncounterTable8Util
{
    public static byte[][] GetBytes(IReadOnlyDictionary<ulong, byte> zone_loc, IReadOnlyDictionary<ulong, byte> zone_type, EncounterArchive t, bool hiddenTreeFix = false)
    {
        var result = new List<DumpableLocation>();
        foreach (var zone in t.EncounterTables)
        {
            var entry = GetDumpable(zone, zone_loc, zone_type);
            if (entry.Slots.Count == 0)
                continue;
            result.Add(entry);
        }

        if (hiddenTreeFix)
        {
            // The Berry Trees in Bridge Field are right against the map boundary, and can be accessed on the adjacent Map ID (Stony Wilderness)
            // Copy the two Berry Tree encounters from Bridge to Stony, as these aren't overworld (wandering) crossover encounters.
            var bridge = result.Find(z => z.Location == 142);
            var stony = result.Find(z => z.Location == 144);
            if (bridge is null)
                throw new ArgumentException("Bridge Field not found");
            if (stony is null)
                throw new ArgumentException("Stony Wilderness not found");

            var slots = bridge.Slots;
            stony.Slots.AddRange(slots.Where(z => z.EncounterType == SWSHEncounterType.Shaking_Trees));
        }

        return result.ConvertAll(z => z.Serialize()).ToArray();
    }

    private static DumpableLocation GetDumpable(EncounterTable zone, IReadOnlyDictionary<ulong, byte> zoneLoc, IReadOnlyDictionary<ulong, byte> zoneType)
    {
        // Don't dump data that we can't correlate to a zone
        if (!zoneLoc.TryGetValue(zone.ZoneID, out var location))
            return DumpableLocation.Empty;

        // Try to get the table type. Skip inaccessible tables.
        if (!zoneType.TryGetValue(zone.ZoneID, out var slottype) || slottype == (byte)Inaccessible)
            return DumpableLocation.Empty;

        var list = new List<Slot8>();
        for (int i = 0; i < zone.SubTables.Count; i++)
        {
            var weather = (SWSHEncounterType)(1 << i);

            if (!IsPermittedWeather(location, weather, slottype))
                continue;

            var table = zone.SubTables[i];
            var min = table.LevelMin;
            var max = table.LevelMax;
            foreach (var s in table.Slots)
            {
                if (s.Species == 0)
                    continue;

                var s8 = new Slot8(s.Species, s.Form, min, max) {EncounterType = weather};
                var match = list.Find(z => z.Equals(s8));
                if (match == null)
                    list.Add(s8);
                else
                    match.EncounterType |= weather;
            }
        }

        return new DumpableLocation(list, location, slottype);
    }

    private static bool IsPermittedWeather(byte locID, SWSHEncounterType weather, byte slotType)
    {
        // Only keep fishing slots for any encounters that are FishingOnly.
        if (slotType == (byte)OnlyFishing)
            return weather == SWSHEncounterType.Fishing;

        // Otherwise, keep all fishing and shaking tree encounters.
        if (weather is SWSHEncounterType.Shaking_Trees or SWSHEncounterType.Fishing)
            return true;

        // If we didn't find the weather in the general table, only allow Normal.
        if (!WeatherbyArea.TryGetValue(locID, out var permit))
            permit = SWSHEncounterType.Normal;
        if (permit.HasFlag(weather))
            return true;

        // Check bleed conditions otherwise.
        return IsWeatherBleedPossible((SWSHSlotType)slotType, permit, locID);
    }

    private static bool IsWeatherBleedPossible(SWSHSlotType type, SWSHEncounterType permit, int location) => type switch
    {
        SymbolMain or SymbolMain2 or SymbolMain3 => WeatherBleedSymbol        .TryGetValue(location, out var weather) && weather.HasFlag(permit),
        HiddenMain or HiddenMain2                => WeatherBleedHiddenGrass   .TryGetValue(location, out var weather) && weather.HasFlag(permit),
        Surfing                                  => WeatherBleedSymbolSurfing .TryGetValue(location, out var weather) && weather.HasFlag(permit),
        Sharpedo                                 => WeatherBleedSymbolSharpedo.TryGetValue(location, out var weather) && weather.HasFlag(permit),
        _ => false,
    };

    private class DumpableLocation
    {
        public static readonly DumpableLocation Empty = new(new List<Slot8>(), 0, 0);

        public readonly List<Slot8> Slots;
        public readonly byte Location;
        public readonly byte SlotType;

        public DumpableLocation(List<Slot8> slots, byte location, byte slotType)
        {
            Slots = slots;
            Location = location;
            SlotType = slotType;
        }

        public byte[] Serialize() => SerializeSlot8(Location, Slots, SlotType);
    }

    private static byte[] SerializeSlot8(byte locID, IEnumerable<Slot8> list, byte slotType)
    {
        using var ms = new MemoryStream();
        using var bw = new BinaryWriter(ms);
        int ctr = 0;
        bw.Write(locID);
        bw.Write((byte) 0); // count tmp
        var groups = list.GroupBy(z => (int) z.EncounterType | (z.Min << 16) | (z.Max << 24));
        foreach (var g in groups)
        {
            var slots = g.ToArray();
            var type = g.Key & 0xFFFF;
            var min = (g.Key >> 16) & 0xFF;
            var max = g.Key >> 24;
            bw.Write((ushort) type);
            bw.Write((byte) min);
            bw.Write((byte) max);
            bw.Write((byte) slots.Length);
            bw.Write(slotType);

            foreach (var slot in slots)
                bw.Write((ushort)(slot.Species | (slot.Form << 11)));
            ctr += slots.Length;
        }

        bw.BaseStream.Seek(1, SeekOrigin.Begin);
        bw.Write((byte) ctr);
        return ms.ToArray();
    }

    [Flags]
#pragma warning disable RCS1154 // Sort enum members.
    private enum SWSHEncounterType
#pragma warning restore RCS1154 // Sort enum members.
    {
        None = 0,
        Normal = 1,
        Overcast = 1 << 1,
        Raining = 1 << 2,
        Thunderstorm = 1 << 3,
        Intense_Sun = 1 << 4,
        Snowing = 1 << 5,
        Snowstorm = 1 << 6,
        Sandstorm = 1 << 7,
        Heavy_Fog = 1 << 8,
        Shaking_Trees = 1 << 9,
        Fishing = 1 << 10,

        All = Normal | Overcast | Raining | Thunderstorm | Intense_Sun | Snowing | Snowstorm | Sandstorm | Heavy_Fog,
        Stormy = Raining | Thunderstorm,
        Icy = Snowing | Snowstorm,
        All_IoA = Normal | Overcast | Stormy | Intense_Sun | Sandstorm | Heavy_Fog,         // IoA can have everything but snow
        All_CT = Normal | Overcast | Stormy | Intense_Sun | Icy | Heavy_Fog,                // CT can have everything but sand
        No_Sun_Sand = Normal | Overcast | Stormy | Icy | Heavy_Fog,                         // Everything but sand and sun
        All_Ballimere = Normal | Overcast | Stormy | Intense_Sun | Snowing | Heavy_Fog,     // All Ballimere Lake weather
    }

    public enum SWSHSlotType : ushort
    {
        SymbolMain,
        SymbolMain2,
        SymbolMain3,

        HiddenMain, // Table with the tree/fishing slots
        HiddenMain2,

        Surfing,
        Surfing2,
        Sky,
        Sky2,
        Ground,
        Ground2,
        Sharpedo,

        OnlyFishing,
        Inaccessible,
    }

    private class Slot8 : IEquatable<Slot8>
    {
        public readonly int Species;
        public readonly int Form;
        public readonly int Min;
        public readonly int Max;
        public SWSHEncounterType EncounterType;

        public Slot8(int s, int f, int n, int x)
        {
            Species = s;
            Form = f;
            Min = n;
            Max = x;
        }

        public bool Equals(Slot8? other)
        {
            if (other is null) return false;
            if (ReferenceEquals(this, other)) return true;
            return Species == other.Species && Form == other.Form && Min == other.Min && Max == other.Max;
        }

        public override bool Equals(object? obj)
        {
            if (obj is null) return false;
            if (ReferenceEquals(this, obj)) return true;
            return obj.GetType() == GetType() && Equals((Slot8) obj);
        }

        public override int GetHashCode() => HashCode.Combine(Species, Form, Min, Max);
    }

    public static IEnumerable<string> GetLines(EncounterArchive t, IReadOnlyDictionary<ulong, string> zone_names, string[] subtable_names, string[] species)
    {
        for (var i = 0; i < t.EncounterTables.Count; i++)
        {
            var enc = t.EncounterTables[i];
            bool known = zone_names.TryGetValue(enc.ZoneID, out var zoneName);
            if (!known)
                zoneName = enc.ZoneID.ToString("X16");
            yield return $"{i:000} - {zoneName}:";

            if (enc.SubTables.Count != 0)
            {
                var j = 0;
                const int NUM_WEATHER_TABLES = 9;
                if (AllWeatherTablesIdentical(enc.SubTables, NUM_WEATHER_TABLES))
                {
                    foreach (var line in GetSubTableSummary(enc.SubTables[0], "All Weather", species))
                        yield return $"\t{line}";
                    j = NUM_WEATHER_TABLES;
                }

                while (j < enc.SubTables.Count)
                {
                    foreach (var line in GetSubTableSummary(enc.SubTables[j], subtable_names[j], species))
                        yield return $"\t{line}";
                    j++;
                }
            }

            yield return string.Empty;
        }
    }

    private static IEnumerable<string> GetSubTableSummary(EncounterSubTable subtable, string name, string[] species)
    {
        if (subtable.LevelMin == 0 || subtable.LevelMax == 0) yield break;

        yield return $"{name} ({GetSubSummary(subtable.LevelMin, subtable.LevelMax)}):";
        foreach (var line in GetLines(subtable.Slots, species))
            yield return $"\t{line}";
    }

    private static bool AllWeatherTablesIdentical(IList<EncounterSubTable> subtables, int numWeatherTables)
    {
        if (numWeatherTables >= subtables.Count)
            throw new ArgumentOutOfRangeException(nameof(numWeatherTables), numWeatherTables, message: "Must be less than the number of sub-tables");

        var first_table = subtables[0];
        for (var i = 1; i < numWeatherTables; i++)
        {
            var cur_table = subtables[i];
            if (cur_table.LevelMin != first_table.LevelMin) return false;
            if (cur_table.LevelMax != first_table.LevelMax) return false;
            if (cur_table.Slots.Count != first_table.Slots.Count) return false;
            for (var j = 0; j < cur_table.Slots.Count; j++)
            {
                if (cur_table.Slots[j].Species != first_table.Slots[j].Species) return false;
                if (cur_table.Slots[j].Form != first_table.Slots[j].Form) return false;
                if (cur_table.Slots[j].Probability != first_table.Slots[j].Probability) return false;
            }
        }

        return true;
    }

    private static string GetSubSummary(int min, int max) => $"Lv. {min}-{max}";

    private static IEnumerable<string> GetLines(IList<EncounterSlot> arr, IReadOnlyList<string> species)
    {
        foreach (var slot in arr.OrderByDescending(sl => sl.Probability))
        {
            if (slot.Species == 0)
                continue;
            string form = slot.Form == 0 ? string.Empty : $"-{slot.Form}";
            var spec_form = $"{species[slot.Species]}{form}";
            yield return $"- {spec_form,-12}\t{slot.Probability:00}%";
        }

        yield return string.Empty;
    }

    private static readonly Dictionary<int, SWSHEncounterType> WeatherbyArea = new()
    {
        { 68, SWSHEncounterType.Intense_Sun }, // Route 6
        { 88, SWSHEncounterType.Snowing }, // Route 8 (Steamdrift Way)
        { 90, SWSHEncounterType.Snowing }, // Route 9
        { 92, SWSHEncounterType.Snowing }, // Route 9 (Circhester Bay)
        { 94, SWSHEncounterType.Overcast }, // Route 9 (Outer Spikemuth)
        { 106, SWSHEncounterType.Snowstorm }, // Route 10
        { 122, SWSHEncounterType.All }, // Rolling Fields
        { 124, SWSHEncounterType.All }, // Dappled Grove
        { 126, SWSHEncounterType.All }, // Watchtower Ruins
        { 128, SWSHEncounterType.All }, // East Lake Axewell
        { 130, SWSHEncounterType.All }, // West Lake Axewell
        { 132, SWSHEncounterType.All }, // Axew's Eye
        { 134, SWSHEncounterType.All }, // South Lake Miloch
        { 136, SWSHEncounterType.All }, // Giant's Seat
        { 138, SWSHEncounterType.All }, // North Lake Miloch
        { 140, SWSHEncounterType.All }, // Motostoke Riverbank
        { 142, SWSHEncounterType.All }, // Bridge Field
        { 144, SWSHEncounterType.All }, // Stony Wilderness
        { 146, SWSHEncounterType.All }, // Dusty Bowl
        { 148, SWSHEncounterType.All }, // Giant's Mirror
        { 150, SWSHEncounterType.All }, // Hammerlocke Hills
        { 152, SWSHEncounterType.All }, // Giant's Cap
        { 154, SWSHEncounterType.All }, // Lake of Outrage
        { 164, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Stormy | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Heavy_Fog }, // Fields of Honor
        { 166, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Stormy | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Heavy_Fog }, // Soothing Wetlands
        { 168, SWSHEncounterType.All_IoA }, // Forest of Focus
        { 170, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Stormy | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Heavy_Fog }, // Challenge Beach
        { 174, SWSHEncounterType.All_IoA }, // Challenge Road
        { 178, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Stormy | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Heavy_Fog }, // Loop Lagoon
        { 180, SWSHEncounterType.All_IoA }, // Training Lowlands
        { 184, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Raining | SWSHEncounterType.Sandstorm | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Heavy_Fog }, // Potbottom Desert
        { 186, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Stormy | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Heavy_Fog }, // Workout Sea
        { 188, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Stormy | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Heavy_Fog }, // Stepping-Stone Sea
        { 190, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Stormy | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Heavy_Fog }, // Insular Sea
        { 192, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Stormy | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Heavy_Fog }, // Honeycalm Sea
        { 194, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Stormy | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Heavy_Fog }, // Honeycalm Island
        { 204, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Icy | SWSHEncounterType.Heavy_Fog }, // Slippery Slope
        { 208, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Icy | SWSHEncounterType.Heavy_Fog }, // Frostpoint Field
        { 210, SWSHEncounterType.All_CT }, // Giant's Bed
        { 212, SWSHEncounterType.All_CT }, // Old Cemetery
        { 214, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Icy | SWSHEncounterType.Heavy_Fog }, // Snowslide Slope
        { 216, SWSHEncounterType.Overcast }, // Tunnel to the Top
        { 218, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Icy | SWSHEncounterType.Heavy_Fog }, // Path to the Peak
        { 222, SWSHEncounterType.All_CT }, // Giant's Foot
        { 224, SWSHEncounterType.Overcast }, // Roaring-Sea Caves
        { 226, SWSHEncounterType.No_Sun_Sand }, // Frigid Sea
        { 228, SWSHEncounterType.All_CT }, // Three-Point Pass
        { 230, SWSHEncounterType.All_Ballimere }, // Ballimere Lake
        { 232, SWSHEncounterType.Overcast }, // Lakeside Cave
    };

    /// <summary>
    /// Weather types that may bleed into each location from adjacent locations for standard symbol encounter slots.
    /// </summary>
    private static readonly Dictionary<int, SWSHEncounterType> WeatherBleedSymbol = new()
    {
        { 166, SWSHEncounterType.All_IoA }, // Soothing Wetlands from Forest of Focus
        { 170, SWSHEncounterType.All_IoA }, // Challenge Beach from Forest of Focus
        { 182, SWSHEncounterType.All_IoA }, // Warm-Up Tunnel from Training Lowlands
        { 208, SWSHEncounterType.All_CT }, // Frostpoint Field from Giant's Bed
        { 216, SWSHEncounterType.Normal | SWSHEncounterType.Overcast | SWSHEncounterType.Intense_Sun | SWSHEncounterType.Icy | SWSHEncounterType.Heavy_Fog }, // Tunnel to the Top from Path to the Peak
        { 224, SWSHEncounterType.All_CT }, // Roaring-Sea Caves from Three-Point Pass
        { 230, SWSHEncounterType.All_CT }, // Ballimere Lake from Giant's Bed
        { 232, SWSHEncounterType.All_Ballimere }, // Lakeside Cave from Ballimere Lake
    };

    /// <summary>
    /// Weather types that may bleed into each location from adjacent locations for surfing symbol encounter slots.
    /// </summary>
    private static readonly Dictionary<int, SWSHEncounterType> WeatherBleedSymbolSurfing = new()
    {
        { 192, SWSHEncounterType.All_IoA }, // Honeycalm Sea from Training Lowlands
    };

    /// <summary>
    /// Weather types that may bleed into each location from adjacent locations for Sharpedo symbol encounter slots.
    /// </summary>
    private static readonly Dictionary<int, SWSHEncounterType> WeatherBleedSymbolSharpedo = new()
    {
        { 192, SWSHEncounterType.All_IoA }, // Honeycalm Sea from Training Lowlands
    };

    /// <summary>
    /// Weather types that may bleed into each location from adjacent locations, for standard hidden grass encounter slots.
    /// </summary>
    private static readonly Dictionary<int, SWSHEncounterType> WeatherBleedHiddenGrass = new()
    {
        { 166, SWSHEncounterType.All_IoA }, // Soothing Wetlands from Forest of Focus
        { 170, SWSHEncounterType.All_IoA }, // Challenge Beach from Forest of Focus
        { 208, SWSHEncounterType.All_CT }, // Frostpoint Field from Giant's Bed
        { 230, SWSHEncounterType.All_CT }, // Ballimere Lake from Giant's Bed
    };
}
