using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using StardewModdingAPI.Mods.ConsoleCommands.Framework.ItemData;
using StardewValley;
using StardewValley.GameData.FishPond;
using StardewValley.Menus;
using StardewValley.Objects;
using StardewValley.Tools;
using SObject = StardewValley.Object;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework
{
    /// Provides methods for searching and constructing items.
    internal class ItemRepository
    {
        /*********
        ** Fields
        *********/
        /// The custom ID offset for items don't have a unique ID in the game.
        private readonly int CustomIDOffset = 1000;
        /*********
        ** Public methods
        *********/
        /// Get all spawnable items.
        /// The item types to fetch (or null for any type).
        /// Whether to include flavored variants like "Sunflower Honey".
        [SuppressMessage("ReSharper", "AccessToModifiedClosure", Justification = "TryCreate invokes the lambda immediately.")]
        public IEnumerable GetAll(ItemType[] itemTypes = null, bool includeVariants = true)
        {
            //
            //
            // Be careful about closure variable capture here!
            //
            // SearchableItem stores the Func-  to create new instances later. Loop variables passed into the
            // function will be captured, so every func in the loop will use the value from the last iteration. Use the
            // TryCreate(type, id, entity => item) form to avoid the issue, or create a local variable to pass in.
            //
            //
            IEnumerable GetAllRaw()
            {
                HashSet types = itemTypes?.Any() == true ? new HashSet(itemTypes) : null;
                bool ShouldGet(ItemType type) => types == null || types.Contains(type);
                // get tools
                if (ShouldGet(ItemType.Tool))
                {
                    for (int q = Tool.stone; q <= Tool.iridium; q++)
                    {
                        int quality = q;
                        yield return this.TryCreate(ItemType.Tool, ToolFactory.axe, _ => ToolFactory.getToolFromDescription(ToolFactory.axe, quality));
                        yield return this.TryCreate(ItemType.Tool, ToolFactory.hoe, _ => ToolFactory.getToolFromDescription(ToolFactory.hoe, quality));
                        yield return this.TryCreate(ItemType.Tool, ToolFactory.pickAxe, _ => ToolFactory.getToolFromDescription(ToolFactory.pickAxe, quality));
                        yield return this.TryCreate(ItemType.Tool, ToolFactory.wateringCan, _ => ToolFactory.getToolFromDescription(ToolFactory.wateringCan, quality));
                        if (quality != Tool.iridium)
                            yield return this.TryCreate(ItemType.Tool, ToolFactory.fishingRod, _ => ToolFactory.getToolFromDescription(ToolFactory.fishingRod, quality));
                    }
                    yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset, _ => new MilkPail()); // these don't have any sort of ID, so we'll just assign some arbitrary ones
                    yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 1, _ => new Shears());
                    yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 2, _ => new Pan());
                    yield return this.TryCreate(ItemType.Tool, this.CustomIDOffset + 3, _ => new Wand());
                }
                // clothing
                if (ShouldGet(ItemType.Clothing))
                {
                    foreach (int id in this.GetShirtIds())
                        yield return this.TryCreate(ItemType.Clothing, id, p => new Clothing(p.ID));
                }
                // wallpapers
                if (ShouldGet(ItemType.Wallpaper))
                {
                    for (int id = 0; id < 112; id++)
                        yield return this.TryCreate(ItemType.Wallpaper, id, p => new Wallpaper(p.ID) { Category = SObject.furnitureCategory });
                }
                // flooring
                if (ShouldGet(ItemType.Flooring))
                {
                    for (int id = 0; id < 56; id++)
                        yield return this.TryCreate(ItemType.Flooring, id, p => new Wallpaper(p.ID, isFloor: true) { Category = SObject.furnitureCategory });
                }
                // equipment
                if (ShouldGet(ItemType.Boots))
                {
                    foreach (int id in this.TryLoad("Data\\Boots").Keys)
                        yield return this.TryCreate(ItemType.Boots, id, p => new Boots(p.ID));
                }
                if (ShouldGet(ItemType.Hat))
                {
                    foreach (int id in this.TryLoad("Data\\hats").Keys)
                        yield return this.TryCreate(ItemType.Hat, id, p => new Hat(p.ID));
                }
                // weapons
                if (ShouldGet(ItemType.Weapon))
                {
                    foreach (int id in this.TryLoad("Data\\weapons").Keys)
                    {
                        yield return this.TryCreate(ItemType.Weapon, id, p => (p.ID >= 32 && p.ID <= 34)
                            ? (Item)new Slingshot(p.ID)
                            : new MeleeWeapon(p.ID)
                        );
                    }
                }
                // furniture
                if (ShouldGet(ItemType.Furniture))
                {
                    foreach (int id in this.TryLoad("Data\\Furniture").Keys)
                        yield return this.TryCreate(ItemType.Furniture, id, p => Furniture.GetFurnitureInstance(p.ID));
                }
                // craftables
                if (ShouldGet(ItemType.BigCraftable))
                {
                    foreach (int id in Game1.bigCraftablesInformation.Keys)
                        yield return this.TryCreate(ItemType.BigCraftable, id, p => new SObject(Vector2.Zero, p.ID));
                }
                // objects
                if (ShouldGet(ItemType.Object) || ShouldGet(ItemType.Ring))
                {
                    foreach (int id in Game1.objectInformation.Keys)
                    {
                        string[] fields = Game1.objectInformation[id]?.Split('/');
                        // ring
                        if (id != 801 && fields?.Length >= 4 && fields[3] == "Ring") // 801 = wedding ring, which isn't an equippable ring
                        {
                            if (ShouldGet(ItemType.Ring))
                                yield return this.TryCreate(ItemType.Ring, id, p => new Ring(p.ID));
                        }
                        // journal scrap
                        else if (id == 842)
                        {
                            if (ShouldGet(ItemType.Object))
                            {
                                foreach (SearchableItem journalScrap in this.GetSecretNotes(isJournalScrap: true))
                                    yield return journalScrap;
                            }
                        }
                        // secret notes
                        else if (id == 79)
                        {
                            if (ShouldGet(ItemType.Object))
                            {
                                foreach (SearchableItem secretNote in this.GetSecretNotes(isJournalScrap: false))
                                    yield return secretNote;
                            }
                        }
                        // object
                        else if (ShouldGet(ItemType.Object))
                        {
                            // spawn main item
                            SObject item = null;
                            yield return this.TryCreate(ItemType.Object, id, p =>
                            {
                                return item = (p.ID == 812 // roe
                                    ? new ColoredObject(p.ID, 1, Color.White)
                                    : new SObject(p.ID, 1)
                                );
                            });
                            if (item == null)
                                continue;
                            // flavored items
                            if (includeVariants)
                            {
                                foreach (SearchableItem variant in this.GetFlavoredObjectVariants(item))
                                    yield return variant;
                            }
                        }
                    }
                }
            }
            return GetAllRaw().Where(p => p != null);
        }
        /*********
        ** Private methods
        *********/
        /// Get the individual secret note or journal scrap items.
        /// Whether to get journal scraps.
        /// Derived from .
        private IEnumerable GetSecretNotes(bool isJournalScrap)
        {
            // get base item ID
            int baseId = isJournalScrap ? 842 : 79;
            // get secret note IDs
            var ids = this
                .TryLoad("Data\\SecretNotes")
                .Keys
                .Where(isJournalScrap
                    ? id => (id >= GameLocation.JOURNAL_INDEX)
                    : id => (id < GameLocation.JOURNAL_INDEX)
                )
                .Select(isJournalScrap
                    ? id => (id - GameLocation.JOURNAL_INDEX)
                    : id => id
                );
            // build items
            foreach (int id in ids)
            {
                int fakeId = this.CustomIDOffset * 8 + id;
                if (isJournalScrap)
                    fakeId += GameLocation.JOURNAL_INDEX;
                yield return this.TryCreate(ItemType.Object, fakeId, _ =>
                {
                    SObject note = new(baseId, 1);
                    note.Name = $"{note.Name} #{id}";
                    return note;
                });
            }
        }
        /// Get flavored variants of a base item (like Blueberry Wine for Blueberry), if any.
        /// A sample of the base item.
        private IEnumerable GetFlavoredObjectVariants(SObject item)
        {
            int id = item.ParentSheetIndex;
            switch (item.Category)
            {
                // fruit products
                case SObject.FruitsCategory:
                    // wine
                    yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 2 + id, _ => new SObject(348, 1)
                    {
                        Name = $"{item.Name} Wine",
                        Price = item.Price * 3,
                        preserve = { SObject.PreserveType.Wine },
                        preservedParentSheetIndex = { id }
                    });
                    // jelly
                    yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 3 + id, _ => new SObject(344, 1)
                    {
                        Name = $"{item.Name} Jelly",
                        Price = 50 + item.Price * 2,
                        preserve = { SObject.PreserveType.Jelly },
                        preservedParentSheetIndex = { id }
                    });
                    break;
                // vegetable products
                case SObject.VegetableCategory:
                    // juice
                    yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 4 + id, _ => new SObject(350, 1)
                    {
                        Name = $"{item.Name} Juice",
                        Price = (int)(item.Price * 2.25d),
                        preserve = { SObject.PreserveType.Juice },
                        preservedParentSheetIndex = { id }
                    });
                    // pickled
                    yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, _ => new SObject(342, 1)
                    {
                        Name = $"Pickled {item.Name}",
                        Price = 50 + item.Price * 2,
                        preserve = { SObject.PreserveType.Pickle },
                        preservedParentSheetIndex = { id }
                    });
                    break;
                // flower honey
                case SObject.flowersCategory:
                    yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 5 + id, _ =>
                    {
                        SObject honey = new SObject(Vector2.Zero, 340, $"{item.Name} Honey", false, true, false, false)
                        {
                            Name = $"{item.Name} Honey",
                            preservedParentSheetIndex = { id }
                        };
                        honey.Price += item.Price * 2;
                        return honey;
                    });
                    break;
                // roe and aged roe (derived from FishPond.GetFishProduce)
                case SObject.sellAtFishShopCategory when id == 812:
                    {
                        this.GetRoeContextTagLookups(out HashSet simpleTags, out List> complexTags);
                        foreach (var pair in Game1.objectInformation)
                        {
                            // get input
                            SObject input = this.TryCreate(ItemType.Object, pair.Key, p => new SObject(p.ID, 1))?.Item as SObject;
                            var inputTags = input?.GetContextTags();
                            if (inputTags?.Any() != true)
                                continue;
                            // check if roe-producing fish
                            if (!inputTags.Any(tag => simpleTags.Contains(tag)) && !complexTags.Any(set => set.All(tag => input.HasContextTag(tag))))
                                continue;
                            // yield roe
                            SObject roe = null;
                            Color color = this.GetRoeColor(input);
                            yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + id, _ =>
                            {
                                roe = new ColoredObject(812, 1, color)
                                {
                                    name = $"{input.Name} Roe",
                                    preserve = { Value = SObject.PreserveType.Roe },
                                    preservedParentSheetIndex = { Value = input.ParentSheetIndex }
                                };
                                roe.Price += input.Price / 2;
                                return roe;
                            });
                            // aged roe
                            if (roe != null && pair.Key != 698) // aged sturgeon roe is caviar, which is a separate item
                            {
                                yield return this.TryCreate(ItemType.Object, this.CustomIDOffset * 7 + id, _ => new ColoredObject(447, 1, color)
                                {
                                    name = $"Aged {input.Name} Roe",
                                    Category = -27,
                                    preserve = { Value = SObject.PreserveType.AgedRoe },
                                    preservedParentSheetIndex = { Value = input.ParentSheetIndex },
                                    Price = roe.Price * 2
                                });
                            }
                        }
                    }
                    break;
            }
        }
        /// Get optimized lookups to match items which produce roe in a fish pond.
        /// A lookup of simple singular tags which match a roe-producing fish.
        /// A list of tag sets which match roe-producing fish.
        private void GetRoeContextTagLookups(out HashSet simpleTags, out List> complexTags)
        {
            simpleTags = new HashSet();
            complexTags = new List>();
            foreach (FishPondData data in Game1.content.Load>("Data\\FishPondData"))
            {
                if (data.ProducedItems.All(p => p.ItemID != 812))
                    continue; // doesn't produce roe
                if (data.RequiredTags.Count == 1 && !data.RequiredTags[0].StartsWith("!"))
                    simpleTags.Add(data.RequiredTags[0]);
                else
                    complexTags.Add(data.RequiredTags);
            }
        }
        /// Try to load a data file, and return empty data if it's invalid.
        /// The asset key type.
        /// The asset value type.
        /// The data asset name.
        private Dictionary TryLoad(string assetName)
        {
            try
            {
                return Game1.content.Load>(assetName);
            }
            catch (ContentLoadException)
            {
                // generally due to a player incorrectly replacing a data file with an XNB mod
                return new Dictionary();
            }
        }
        /// Create a searchable item if valid.
        /// The item type.
        /// The unique ID (if different from the item's parent sheet index).
        /// Create an item instance.
        private SearchableItem TryCreate(ItemType type, int id, Func createItem)
        {
            try
            {
                var item = new SearchableItem(type, id, createItem);
                item.Item.getDescription(); // force-load item data, so it crashes here if it's invalid
                return item;
            }
            catch
            {
                return null; // if some item data is invalid, just don't include it
            }
        }
        /// Get the color to use a given fish's roe.
        /// The fish whose roe to color.
        /// Derived from .
        private Color GetRoeColor(SObject fish)
        {
            return fish.ParentSheetIndex == 698 // sturgeon
                ? new Color(61, 55, 42)
                : (TailoringMenu.GetDyeColor(fish) ?? Color.Orange);
        }
        /// Get valid shirt IDs.
        /// 
        /// Shirts have a possible range of 1000–1999, but not all of those IDs are valid. There are two sets of IDs:
        ///
        /// 
        ///   - 
        ///     Shirts which exist in .
        ///   ///
- 
        ///     Shirts with a dynamic ID and no entry in . These automatically
        ///     use the generic shirt entry with ID -1 and are mapped to a calculated position in the
        ///     Characters/Farmer/shirts spritesheet. There's no constant we can use, but some known valid
        ///     ranges are 1000–1111 (used in  for the customization screen and
        ///     1000–1127 (used in  and ).
        ///     Based on the spritesheet, the max valid ID is 1299.
        ///   ///
 /// 
        private IEnumerable GetShirtIds()
        {
            // defined shirt items
            foreach (int id in Game1.clothingInformation.Keys)
            {
                if (id < 0)
                    continue; // placeholder data for character customization clothing below
                yield return id;
            }
            // dynamic shirts
            HashSet clothingIds = new HashSet(Game1.clothingInformation.Keys);
            for (int id = 1000; id <= 1299; id++)
            {
                if (!clothingIds.Contains(id))
                    yield return id;
            }
        }
    }
}