import csv
import enum
import logging
from dataclasses import dataclass, field
from pathlib import Path
from random import Random
from typing import Dict, List, Protocol, Union, Set, Optional

from BaseClasses import Item, ItemClassification
from . import data
from .content.feature import friendsanity
from .content.game_content import StardewContent
from .data.game_item import ItemTag
from .logic.logic_event import all_events
from .mods.mod_data import ModNames
from .options import StardewValleyOptions, TrapItems, FestivalLocations, ExcludeGingerIsland, SpecialOrderLocations, SeasonRandomization, Museumsanity, \
    BuildingProgression, SkillProgression, ToolProgression, ElevatorProgression, BackpackProgression, ArcadeMachineLocations, Monstersanity, Goal, \
    Chefsanity, Craftsanity, BundleRandomization, EntranceRandomization, Shipsanity, Walnutsanity, EnabledFillerBuffs
from .strings.ap_names.ap_option_names import OptionName
from .strings.ap_names.ap_weapon_names import APWeapon
from .strings.ap_names.buff_names import Buff
from .strings.ap_names.community_upgrade_names import CommunityUpgrade
from .strings.ap_names.mods.mod_items import SVEQuestItem
from .strings.currency_names import Currency
from .strings.wallet_item_names import Wallet

ITEM_CODE_OFFSET = 717000

logger = logging.getLogger(__name__)
world_folder = Path(__file__).parent


class Group(enum.Enum):
    RESOURCE_PACK = enum.auto()
    FRIENDSHIP_PACK = enum.auto()
    COMMUNITY_REWARD = enum.auto()
    TRASH = enum.auto()
    FOOTWEAR = enum.auto()
    HATS = enum.auto()
    RING = enum.auto()
    WEAPON = enum.auto()
    WEAPON_GENERIC = enum.auto()
    WEAPON_SWORD = enum.auto()
    WEAPON_CLUB = enum.auto()
    WEAPON_DAGGER = enum.auto()
    WEAPON_SLINGSHOT = enum.auto()
    PROGRESSIVE_TOOLS = enum.auto()
    SKILL_LEVEL_UP = enum.auto()
    SKILL_MASTERY = enum.auto()
    BUILDING = enum.auto()
    WIZARD_BUILDING = enum.auto()
    ARCADE_MACHINE_BUFFS = enum.auto()
    BASE_RESOURCE = enum.auto()
    WARP_TOTEM = enum.auto()
    GEODE = enum.auto()
    ORE = enum.auto()
    FERTILIZER = enum.auto()
    SEED = enum.auto()
    CROPSANITY = enum.auto()
    FISHING_RESOURCE = enum.auto()
    SEASON = enum.auto()
    TRAVELING_MERCHANT_DAY = enum.auto()
    MUSEUM = enum.auto()
    FRIENDSANITY = enum.auto()
    FESTIVAL = enum.auto()
    RARECROW = enum.auto()
    TRAP = enum.auto()
    BONUS = enum.auto()
    MAXIMUM_ONE = enum.auto()
    EXACTLY_TWO = enum.auto()
    DEPRECATED = enum.auto()
    RESOURCE_PACK_USEFUL = enum.auto()
    SPECIAL_ORDER_BOARD = enum.auto()
    SPECIAL_ORDER_QI = enum.auto()
    BABY = enum.auto()
    GINGER_ISLAND = enum.auto()
    WALNUT_PURCHASE = enum.auto()
    TV_CHANNEL = enum.auto()
    QI_CRAFTING_RECIPE = enum.auto()
    CHEFSANITY = enum.auto()
    CHEFSANITY_STARTER = enum.auto()
    CHEFSANITY_QOS = enum.auto()
    CHEFSANITY_PURCHASE = enum.auto()
    CHEFSANITY_FRIENDSHIP = enum.auto()
    CHEFSANITY_SKILL = enum.auto()
    CRAFTSANITY = enum.auto()
    BOOK_POWER = enum.auto()
    LOST_BOOK = enum.auto()
    PLAYER_BUFF = enum.auto()
    # Mods
    MAGIC_SPELL = enum.auto()
    MOD_WARP = enum.auto()


@dataclass(frozen=True)
class ItemData:
    code_without_offset: Optional[int]
    name: str
    classification: ItemClassification
    mod_name: Optional[str] = None
    groups: Set[Group] = field(default_factory=frozenset)

    def __post_init__(self):
        if not isinstance(self.groups, frozenset):
            super().__setattr__("groups", frozenset(self.groups))

    @property
    def code(self):
        return ITEM_CODE_OFFSET + self.code_without_offset if self.code_without_offset is not None else None

    def has_any_group(self, *group: Group) -> bool:
        groups = set(group)
        return bool(groups.intersection(self.groups))


class StardewItemFactory(Protocol):
    def __call__(self, name: Union[str, ItemData], override_classification: ItemClassification = None) -> Item:
        raise NotImplementedError


class StardewItemDeleter(Protocol):
    def __call__(self, item: Item):
        raise NotImplementedError


def load_item_csv():
    try:
        from importlib.resources import files
    except ImportError:
        from importlib_resources import files  # noqa

    items = []
    with files(data).joinpath("items.csv").open() as file:
        item_reader = csv.DictReader(file)
        for item in item_reader:
            id = int(item["id"]) if item["id"] else None
            classification = ItemClassification[item["classification"]]
            groups = {Group[group] for group in item["groups"].split(",") if group}
            mod_name = str(item["mod_name"]) if item["mod_name"] else None
            items.append(ItemData(id, item["name"], classification, mod_name, groups))
    return items


events = [
    ItemData(None, e, ItemClassification.progression)
    for e in sorted(all_events)
]

all_items: List[ItemData] = load_item_csv() + events
item_table: Dict[str, ItemData] = {}
items_by_group: Dict[Group, List[ItemData]] = {}


def initialize_groups():
    for item in all_items:
        for group in item.groups:
            item_group = items_by_group.get(group, list())
            item_group.append(item)
            items_by_group[group] = item_group


def initialize_item_table():
    item_table.update({item.name: item for item in all_items})


initialize_item_table()
initialize_groups()


def get_too_many_items_error_message(locations_count: int, items_count: int) -> str:
    return f"There should be at least as many locations [{locations_count}] as there are mandatory items [{items_count}]"


def create_items(item_factory: StardewItemFactory, item_deleter: StardewItemDeleter, locations_count: int, items_to_exclude: List[Item],
                 options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]:
    items = []
    unique_items = create_unique_items(item_factory, options, content, random)

    remove_items(item_deleter, items_to_exclude, unique_items)

    remove_items_if_no_room_for_them(item_deleter, unique_items, locations_count, random)

    items += unique_items
    logger.debug(f"Created {len(unique_items)} unique items")

    unique_filler_items = create_unique_filler_items(item_factory, options, random, locations_count - len(items))
    items += unique_filler_items
    logger.debug(f"Created {len(unique_filler_items)} unique filler items")

    resource_pack_items = fill_with_resource_packs_and_traps(item_factory, options, random, items, locations_count)
    items += resource_pack_items
    logger.debug(f"Created {len(resource_pack_items)} resource packs")

    return items


def remove_items(item_deleter: StardewItemDeleter, items_to_remove, items):
    for item in items_to_remove:
        if item in items:
            items.remove(item)
            item_deleter(item)


def remove_items_if_no_room_for_them(item_deleter: StardewItemDeleter, unique_items: List[Item], locations_count: int, random: Random):
    if len(unique_items) <= locations_count:
        return

    number_of_items_to_remove = len(unique_items) - locations_count
    removable_items = [item for item in unique_items if item.classification == ItemClassification.filler or item.classification == ItemClassification.trap]
    if len(removable_items) < number_of_items_to_remove:
        logger.debug(f"Player has more items than locations, trying to remove {number_of_items_to_remove} random non-progression items")
        removable_items = [item for item in unique_items if not item.classification & ItemClassification.progression]
    else:
        logger.debug(f"Player has more items than locations, trying to remove {number_of_items_to_remove} random filler items")
    assert len(removable_items) >= number_of_items_to_remove, get_too_many_items_error_message(locations_count, len(unique_items))
    items_to_remove = random.sample(removable_items, number_of_items_to_remove)
    remove_items(item_deleter, items_to_remove, unique_items)


def create_unique_items(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, random: Random) -> List[Item]:
    items = []

    items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD])
    items.append(item_factory(CommunityUpgrade.movie_theater))  # It is a community reward, but we need two of them
    create_raccoons(item_factory, options, items)
    items.append(item_factory(Wallet.metal_detector))  # Always offer at least one metal detector

    create_backpack_items(item_factory, options, items)
    create_weapons(item_factory, options, items)
    items.append(item_factory("Skull Key"))
    create_elevators(item_factory, options, items)
    create_tools(item_factory, options, items)
    create_skills(item_factory, options, items)
    create_wizard_buildings(item_factory, options, items)
    create_carpenter_buildings(item_factory, options, items)
    items.append(item_factory("Railroad Boulder Removed"))
    items.append(item_factory(CommunityUpgrade.fruit_bats))
    items.append(item_factory(CommunityUpgrade.mushroom_boxes))
    items.append(item_factory("Beach Bridge"))
    create_tv_channels(item_factory, options, items)
    create_quest_rewards(item_factory, options, items)
    create_stardrops(item_factory, options, content, items)
    create_museum_items(item_factory, options, items)
    create_arcade_machine_items(item_factory, options, items)
    create_movement_buffs(item_factory, options, items)
    create_traveling_merchant_items(item_factory, items)
    items.append(item_factory("Return Scepter"))
    create_seasons(item_factory, options, items)
    create_seeds(item_factory, content, items)
    create_friendsanity_items(item_factory, options, content, items, random)
    create_festival_rewards(item_factory, options, items)
    create_special_order_board_rewards(item_factory, options, items)
    create_special_order_qi_rewards(item_factory, options, items)
    create_walnuts(item_factory, options, items)
    create_walnut_purchase_rewards(item_factory, options, items)
    create_crafting_recipes(item_factory, options, items)
    create_cooking_recipes(item_factory, options, items)
    create_shipsanity_items(item_factory, options, items)
    create_booksanity_items(item_factory, content, items)
    create_goal_items(item_factory, options, items)
    items.append(item_factory("Golden Egg"))
    items.append(item_factory(CommunityUpgrade.mr_qi_plane_ride))

    create_sve_special_items(item_factory, options, items)
    create_magic_mod_spells(item_factory, options, items)
    create_deepwoods_pendants(item_factory, options, items)
    create_archaeology_items(item_factory, options, items)

    return items


def create_raccoons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    number_progressive_raccoons = 9
    if options.quest_locations < 0:
        number_progressive_raccoons = number_progressive_raccoons - 1

    items.extend(item_factory(item) for item in [CommunityUpgrade.raccoon] * number_progressive_raccoons)


def create_backpack_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if (options.backpack_progression == BackpackProgression.option_progressive or
            options.backpack_progression == BackpackProgression.option_early_progressive):
        items.extend(item_factory(item) for item in ["Progressive Backpack"] * 2)
        if ModNames.big_backpack in options.mods:
            items.append(item_factory("Progressive Backpack"))


def create_weapons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    weapons = weapons_count(options)
    items.extend(item_factory(item) for item in [APWeapon.slingshot] * 2)
    monstersanity = options.monstersanity
    if monstersanity == Monstersanity.option_none:  # Without monstersanity, might not be enough checks to split the weapons
        items.extend(item_factory(item) for item in [APWeapon.weapon] * weapons)
        items.extend(item_factory(item) for item in [APWeapon.footwear] * 3)  # 1-2 | 3-4 | 6-7-8
        return

    items.extend(item_factory(item) for item in [APWeapon.sword] * weapons)
    items.extend(item_factory(item) for item in [APWeapon.club] * weapons)
    items.extend(item_factory(item) for item in [APWeapon.dagger] * weapons)
    items.extend(item_factory(item) for item in [APWeapon.footwear] * 4)  # 1-2 | 3-4 | 6-7-8 | 11-13
    if monstersanity == Monstersanity.option_goals or monstersanity == Monstersanity.option_one_per_category or \
            monstersanity == Monstersanity.option_short_goals or monstersanity == Monstersanity.option_very_short_goals:
        return
    if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
        rings_items = [item for item in items_by_group[Group.RING] if item.classification is not ItemClassification.filler]
    else:
        rings_items = [item for item in items_by_group[Group.RING]]
    items.extend(item_factory(item) for item in rings_items)


def create_elevators(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if options.elevator_progression == ElevatorProgression.option_vanilla:
        return

    items.extend([item_factory(item) for item in ["Progressive Mine Elevator"] * 24])
    if ModNames.deepwoods in options.mods:
        items.extend([item_factory(item) for item in ["Progressive Woods Obelisk Sigils"] * 10])
    if ModNames.skull_cavern_elevator in options.mods:
        items.extend([item_factory(item) for item in ["Progressive Skull Cavern Elevator"] * 8])


def create_tools(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if options.tool_progression & ToolProgression.option_progressive:
        for item_data in items_by_group[Group.PROGRESSIVE_TOOLS]:
            name = item_data.name
            if "Trash Can" in name:
                items.extend([item_factory(item) for item in [item_data] * 3])
                items.append(item_factory(item_data, ItemClassification.useful))
            else:
                items.extend([item_factory(item) for item in [item_data] * 4])
        if options.skill_progression == SkillProgression.option_progressive_with_masteries:
            items.append(item_factory("Progressive Scythe"))
            items.append(item_factory("Progressive Fishing Rod"))
    items.append(item_factory("Progressive Scythe"))


def create_skills(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if options.skill_progression == SkillProgression.option_vanilla:
        return

    for item in items_by_group[Group.SKILL_LEVEL_UP]:
        if item.mod_name not in options.mods and item.mod_name is not None:
            continue
        items.extend(item_factory(item) for item in [item.name] * 10)

    if options.skill_progression != SkillProgression.option_progressive_with_masteries:
        return

    for item in items_by_group[Group.SKILL_MASTERY]:
        if item.mod_name not in options.mods and item.mod_name is not None:
            continue
        items.append(item_factory(item))


def create_wizard_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    useless_buildings_classification = ItemClassification.progression_skip_balancing if world_is_perfection(options) else ItemClassification.useful
    items.append(item_factory("Earth Obelisk", useless_buildings_classification))
    items.append(item_factory("Water Obelisk", useless_buildings_classification))
    items.append(item_factory("Desert Obelisk"))
    items.append(item_factory("Junimo Hut"))
    items.append(item_factory("Gold Clock", useless_buildings_classification))
    if options.exclude_ginger_island == ExcludeGingerIsland.option_false:
        items.append(item_factory("Island Obelisk"))
    if ModNames.deepwoods in options.mods:
        items.append(item_factory("Woods Obelisk"))


def create_carpenter_buildings(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    building_option = options.building_progression
    if not building_option & BuildingProgression.option_progressive:
        return
    items.append(item_factory("Progressive Coop"))
    items.append(item_factory("Progressive Coop"))
    items.append(item_factory("Progressive Coop"))
    items.append(item_factory("Progressive Barn"))
    items.append(item_factory("Progressive Barn"))
    items.append(item_factory("Progressive Barn"))
    items.append(item_factory("Well"))
    items.append(item_factory("Silo"))
    items.append(item_factory("Mill"))
    items.append(item_factory("Progressive Shed"))
    items.append(item_factory("Progressive Shed", ItemClassification.useful))
    items.append(item_factory("Fish Pond"))
    items.append(item_factory("Stable"))
    items.append(item_factory("Slime Hutch"))
    items.append(item_factory("Shipping Bin"))
    items.append(item_factory("Progressive House"))
    items.append(item_factory("Progressive House"))
    items.append(item_factory("Progressive House"))
    if ModNames.tractor in options.mods:
        items.append(item_factory("Tractor Garage"))


def create_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    create_special_quest_rewards(item_factory, options, items)
    create_help_wanted_quest_rewards(item_factory, options, items)

    create_quest_rewards_sve(item_factory, options, items)


def create_special_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if options.quest_locations < 0:
        return
    # items.append(item_factory("Adventurer's Guild")) # Now unlocked always!
    items.append(item_factory(Wallet.club_card))
    items.append(item_factory(Wallet.magnifying_glass))
    if ModNames.sve in options.mods:
        items.append(item_factory(Wallet.bears_knowledge))
    else:
        items.append(item_factory(Wallet.bears_knowledge, ItemClassification.useful))  # Not necessary outside of SVE
    items.append(item_factory(Wallet.iridium_snake_milk))
    items.append(item_factory("Dark Talisman"))
    if options.exclude_ginger_island == ExcludeGingerIsland.option_false:
        items.append(item_factory("Fairy Dust Recipe"))


def create_help_wanted_quest_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if options.quest_locations <= 0:
        return

    number_help_wanted = options.quest_locations.value
    quest_per_prize_ticket = 3
    number_prize_tickets = number_help_wanted // quest_per_prize_ticket
    items.extend(item_factory(item) for item in [Currency.prize_ticket] * number_prize_tickets)


def create_stardrops(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item]):
    stardrops_classification = get_stardrop_classification(options)
    items.append(item_factory("Stardrop", stardrops_classification))  # The Mines level 100
    items.append(item_factory("Stardrop", stardrops_classification))  # Old Master Cannoli
    items.append(item_factory("Stardrop", stardrops_classification))  # Krobus Stardrop
    if content.features.fishsanity.is_enabled:
        items.append(item_factory("Stardrop", stardrops_classification))  # Master Angler Stardrop
    if ModNames.deepwoods in options.mods:
        items.append(item_factory("Stardrop", stardrops_classification))  # Petting the Unicorn
    if content.features.friendsanity.is_enabled:
        items.append(item_factory("Stardrop", stardrops_classification))  # Spouse Stardrop


def create_museum_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    items.append(item_factory(Wallet.rusty_key))
    items.append(item_factory(Wallet.dwarvish_translation_guide))
    items.append(item_factory("Ancient Seeds Recipe"))
    items.append(item_factory("Stardrop", get_stardrop_classification(options)))
    if options.museumsanity == Museumsanity.option_none:
        return
    items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 10)
    items.extend(item_factory(item) for item in ["Ancient Seeds"] * 5)
    items.append(item_factory(Wallet.metal_detector))


def create_friendsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, content: StardewContent, items: List[Item], random: Random):
    if not content.features.friendsanity.is_enabled:
        return

    create_babies(item_factory, items, random)

    for villager in content.villagers.values():
        item_name = friendsanity.to_item_name(villager.name)

        for _ in content.features.friendsanity.get_randomized_hearts(villager):
            items.append(item_factory(item_name, ItemClassification.progression))

    need_pet = options.goal == Goal.option_grandpa_evaluation
    pet_item_classification = ItemClassification.progression_skip_balancing if need_pet else ItemClassification.useful

    for _ in content.features.friendsanity.get_pet_randomized_hearts():
        items.append(item_factory(friendsanity.pet_heart_item_name, pet_item_classification))


def create_babies(item_factory: StardewItemFactory, items: List[Item], random: Random):
    baby_items = [item for item in items_by_group[Group.BABY]]
    for i in range(2):
        chosen_baby = random.choice(baby_items)
        items.append(item_factory(chosen_baby))


def create_arcade_machine_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if options.arcade_machine_locations == ArcadeMachineLocations.option_full_shuffling:
        items.append(item_factory("JotPK: Progressive Boots"))
        items.append(item_factory("JotPK: Progressive Boots"))
        items.append(item_factory("JotPK: Progressive Gun"))
        items.append(item_factory("JotPK: Progressive Gun"))
        items.append(item_factory("JotPK: Progressive Gun"))
        items.append(item_factory("JotPK: Progressive Gun"))
        items.append(item_factory("JotPK: Progressive Ammo"))
        items.append(item_factory("JotPK: Progressive Ammo"))
        items.append(item_factory("JotPK: Progressive Ammo"))
        items.append(item_factory("JotPK: Extra Life"))
        items.append(item_factory("JotPK: Extra Life"))
        items.append(item_factory("JotPK: Increased Drop Rate"))
        items.extend(item_factory(item) for item in ["Junimo Kart: Extra Life"] * 8)


def create_movement_buffs(item_factory, options: StardewValleyOptions, items: List[Item]):
    movement_buffs: int = options.movement_buff_number.value
    items.extend(item_factory(item) for item in [Buff.movement] * movement_buffs)


def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]):
    items.extend([*(item_factory(item) for item in items_by_group[Group.TRAVELING_MERCHANT_DAY]),
                  *(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6)])


def create_seasons(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if options.season_randomization == SeasonRandomization.option_disabled:
        return

    if options.season_randomization == SeasonRandomization.option_progressive:
        items.extend([item_factory(item) for item in ["Progressive Season"] * 3])
        return

    items.extend([item_factory(item) for item in items_by_group[Group.SEASON]])


def create_seeds(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
    if not content.features.cropsanity.is_enabled:
        return

    items.extend(item_factory(item_table[seed.name]) for seed in content.find_tagged_items(ItemTag.CROPSANITY_SEED))


def create_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    items.append(item_factory("Deluxe Scarecrow Recipe"))
    if options.festival_locations == FestivalLocations.option_disabled:
        return

    festival_rewards = [item_factory(item) for item in items_by_group[Group.FESTIVAL] if item.classification != ItemClassification.filler]
    items.extend([*festival_rewards, item_factory("Stardrop", get_stardrop_classification(options))])


def create_walnuts(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    walnutsanity = options.walnutsanity
    if options.exclude_ginger_island == ExcludeGingerIsland.option_true or walnutsanity == Walnutsanity.preset_none:
        return

    # Give baseline walnuts just to be nice
    num_single_walnuts = 0
    num_triple_walnuts = 2
    num_penta_walnuts = 1
    # https://stardewvalleywiki.com/Golden_Walnut
    # Totals should be accurate, but distribution is slightly offset to make room for baseline walnuts
    if OptionName.walnutsanity_puzzles in walnutsanity:  # 61
        num_single_walnuts += 6  # 6
        num_triple_walnuts += 5  # 15
        num_penta_walnuts += 8  # 40
    if OptionName.walnutsanity_bushes in walnutsanity:  # 25
        num_single_walnuts += 16  # 16
        num_triple_walnuts += 3  # 9
    if OptionName.walnutsanity_dig_spots in walnutsanity:  # 18
        num_single_walnuts += 18  # 18
    if OptionName.walnutsanity_repeatables in walnutsanity:  # 33
        num_single_walnuts += 30  # 30
        num_triple_walnuts += 1  # 3

    items.extend([item_factory(item) for item in ["Golden Walnut"] * num_single_walnuts])
    items.extend([item_factory(item) for item in ["3 Golden Walnuts"] * num_triple_walnuts])
    items.extend([item_factory(item) for item in ["5 Golden Walnuts"] * num_penta_walnuts])


def create_walnut_purchase_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
        return

    items.extend([item_factory("Boat Repair"),
                  item_factory("Open Professor Snail Cave"),
                  item_factory("Ostrich Incubator Recipe"),
                  item_factory("Treehouse"),
                  *[item_factory(item) for item in items_by_group[Group.WALNUT_PURCHASE]]])


def create_special_order_board_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if options.special_order_locations & SpecialOrderLocations.option_board:
        special_order_board_items = [item for item in items_by_group[Group.SPECIAL_ORDER_BOARD]]
        items.extend([item_factory(item) for item in special_order_board_items])


def special_order_board_item_classification(item: ItemData, need_all_recipes: bool) -> ItemClassification:
    if item.classification is ItemClassification.useful:
        return ItemClassification.useful
    if item.name == "Special Order Board":
        return ItemClassification.progression
    if need_all_recipes and "Recipe" in item.name:
        return ItemClassification.progression_skip_balancing
    if item.name == "Monster Musk Recipe":
        return ItemClassification.progression_skip_balancing
    return ItemClassification.useful


def create_special_order_qi_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if options.exclude_ginger_island == ExcludeGingerIsland.option_true:
        return
    qi_gem_rewards = []
    if options.bundle_randomization >= BundleRandomization.option_remixed:
        qi_gem_rewards.append("15 Qi Gems")
        qi_gem_rewards.append("15 Qi Gems")

    if options.special_order_locations & SpecialOrderLocations.value_qi:
        qi_gem_rewards.extend(["100 Qi Gems", "10 Qi Gems", "40 Qi Gems", "25 Qi Gems", "25 Qi Gems",
                               "40 Qi Gems", "20 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems"])

    qi_gem_items = [item_factory(reward) for reward in qi_gem_rewards]
    items.extend(qi_gem_items)


def create_tv_channels(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    channels = [channel for channel in items_by_group[Group.TV_CHANNEL]]
    if options.entrance_randomization == EntranceRandomization.option_disabled:
        channels = [channel for channel in channels if channel.name != "The Gateway Gazette"]
    items.extend([item_factory(item) for item in channels])


def create_crafting_recipes(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    has_craftsanity = options.craftsanity == Craftsanity.option_all
    crafting_recipes = []
    crafting_recipes.extend([recipe for recipe in items_by_group[Group.QI_CRAFTING_RECIPE]])
    if has_craftsanity:
        crafting_recipes.extend([recipe for recipe in items_by_group[Group.CRAFTSANITY]])
    crafting_recipes = remove_excluded_items(crafting_recipes, options)
    items.extend([item_factory(item) for item in crafting_recipes])


def create_cooking_recipes(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    chefsanity = options.chefsanity
    if chefsanity == Chefsanity.option_none:
        return

    chefsanity_recipes_by_name = {recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_STARTER]}  # Dictionary to not make duplicates

    if chefsanity & Chefsanity.option_queen_of_sauce:
        chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_QOS]})
    if chefsanity & Chefsanity.option_purchases:
        chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_PURCHASE]})
    if chefsanity & Chefsanity.option_friendship:
        chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_FRIENDSHIP]})
    if chefsanity & Chefsanity.option_skills:
        chefsanity_recipes_by_name.update({recipe.name: recipe for recipe in items_by_group[Group.CHEFSANITY_SKILL]})

    filtered_chefsanity_recipes = remove_excluded_items(list(chefsanity_recipes_by_name.values()), options)
    items.extend([item_factory(item) for item in filtered_chefsanity_recipes])


def create_shipsanity_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    shipsanity = options.shipsanity
    if shipsanity != Shipsanity.option_everything:
        return

    items.append(item_factory(Wallet.metal_detector))


def create_booksanity_items(item_factory: StardewItemFactory, content: StardewContent, items: List[Item]):
    booksanity = content.features.booksanity
    if not booksanity.is_enabled:
        return

    items.extend(item_factory(item_table[booksanity.to_item_name(book.name)]) for book in content.find_tagged_items(ItemTag.BOOK_POWER))
    progressive_lost_book = item_table[booksanity.progressive_lost_book]
    items.extend(item_factory(progressive_lost_book) for _ in content.features.booksanity.get_randomized_lost_books())


def create_goal_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    goal = options.goal
    if goal != Goal.option_perfection and goal != Goal.option_complete_collection:
        return

    items.append(item_factory(Wallet.metal_detector))


def create_archaeology_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    mods = options.mods
    if ModNames.archaeology not in mods:
        return

    items.append(item_factory(Wallet.metal_detector))


def create_filler_festival_rewards(item_factory: StardewItemFactory, options: StardewValleyOptions) -> List[Item]:
    if options.festival_locations == FestivalLocations.option_disabled:
        return []

    return [item_factory(item) for item in items_by_group[Group.FESTIVAL] if
            item.classification == ItemClassification.filler]


def create_magic_mod_spells(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if ModNames.magic not in options.mods:
        return
    items.extend([item_factory(item) for item in items_by_group[Group.MAGIC_SPELL]])


def create_deepwoods_pendants(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if ModNames.deepwoods not in options.mods:
        return
    items.extend([item_factory(item) for item in ["Pendant of Elders", "Pendant of Community", "Pendant of Depths"]])


def create_sve_special_items(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if ModNames.sve not in options.mods:
        return

    items.extend([item_factory(item) for item in items_by_group[Group.MOD_WARP] if item.mod_name == ModNames.sve])


def create_quest_rewards_sve(item_factory: StardewItemFactory, options: StardewValleyOptions, items: List[Item]):
    if ModNames.sve not in options.mods:
        return

    exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
    items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items])
    if not exclude_ginger_island:
        items.extend([item_factory(item) for item in SVEQuestItem.sve_always_quest_items_ginger_island])

    if options.quest_locations < 0:
        return

    items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items])
    if exclude_ginger_island:
        return
    items.extend([item_factory(item) for item in SVEQuestItem.sve_quest_items_ginger_island])


def create_unique_filler_items(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
                               available_item_slots: int) -> List[Item]:
    items = []

    items.extend(create_filler_festival_rewards(item_factory, options))

    if len(items) > available_item_slots:
        items = random.sample(items, available_item_slots)
    return items


def weapons_count(options: StardewValleyOptions):
    weapon_count = 5
    if ModNames.sve in options.mods:
        weapon_count += 1
    return weapon_count


def fill_with_resource_packs_and_traps(item_factory: StardewItemFactory, options: StardewValleyOptions, random: Random,
                                       items_already_added: List[Item],
                                       number_locations: int) -> List[Item]:
    include_traps = options.trap_items != TrapItems.option_no_traps
    items_already_added_names = [item.name for item in items_already_added]
    useful_resource_packs = [pack for pack in items_by_group[Group.RESOURCE_PACK_USEFUL]
                             if pack.name not in items_already_added_names]
    trap_items = [trap for trap in items_by_group[Group.TRAP]
                  if trap.name not in items_already_added_names and
                  (trap.mod_name is None or trap.mod_name in options.mods)]
    player_buffs = get_allowed_player_buffs(options.enabled_filler_buffs)

    priority_filler_items = []
    priority_filler_items.extend(useful_resource_packs)
    priority_filler_items.extend(player_buffs)

    if include_traps:
        priority_filler_items.extend(trap_items)

    exclude_ginger_island = options.exclude_ginger_island == ExcludeGingerIsland.option_true
    all_filler_packs = remove_excluded_items(get_all_filler_items(include_traps, exclude_ginger_island), options)
    all_filler_packs.extend(player_buffs)
    priority_filler_items = remove_excluded_items(priority_filler_items, options)

    number_priority_items = len(priority_filler_items)
    required_resource_pack = number_locations - len(items_already_added)
    if required_resource_pack < number_priority_items:
        chosen_priority_items = [item_factory(resource_pack) for resource_pack in
                                 random.sample(priority_filler_items, required_resource_pack)]
        return chosen_priority_items

    items = []
    chosen_priority_items = [item_factory(resource_pack,
                                          ItemClassification.trap if resource_pack.classification == ItemClassification.trap else ItemClassification.useful)
                             for resource_pack in priority_filler_items]
    items.extend(chosen_priority_items)
    required_resource_pack -= number_priority_items
    all_filler_packs = [filler_pack for filler_pack in all_filler_packs
                        if Group.MAXIMUM_ONE not in filler_pack.groups or
                        (filler_pack.name not in [priority_item.name for priority_item in
                                                  priority_filler_items] and filler_pack.name not in items_already_added_names)]

    while required_resource_pack > 0:
        resource_pack = random.choice(all_filler_packs)
        exactly_2 = Group.EXACTLY_TWO in resource_pack.groups
        while exactly_2 and required_resource_pack == 1:
            resource_pack = random.choice(all_filler_packs)
            exactly_2 = Group.EXACTLY_TWO in resource_pack.groups
        classification = ItemClassification.useful if resource_pack.classification == ItemClassification.progression else resource_pack.classification
        items.append(item_factory(resource_pack, classification))
        required_resource_pack -= 1
        if exactly_2:
            items.append(item_factory(resource_pack, classification))
            required_resource_pack -= 1
        if exactly_2 or Group.MAXIMUM_ONE in resource_pack.groups:
            all_filler_packs.remove(resource_pack)

    return items


def filter_deprecated_items(items: List[ItemData]) -> List[ItemData]:
    return [item for item in items if Group.DEPRECATED not in item.groups]


def filter_ginger_island_items(exclude_island: bool, items: List[ItemData]) -> List[ItemData]:
    return [item for item in items if not exclude_island or Group.GINGER_ISLAND not in item.groups]


def filter_mod_items(mods: Set[str], items: List[ItemData]) -> List[ItemData]:
    return [item for item in items if item.mod_name is None or item.mod_name in mods]


def remove_excluded_items(items, options: StardewValleyOptions):
    return remove_excluded_items_island_mods(items, options.exclude_ginger_island == ExcludeGingerIsland.option_true, options.mods.value)


def remove_excluded_items_island_mods(items, exclude_ginger_island: bool, mods: Set[str]):
    deprecated_filter = filter_deprecated_items(items)
    ginger_island_filter = filter_ginger_island_items(exclude_ginger_island, deprecated_filter)
    mod_filter = filter_mod_items(mods, ginger_island_filter)
    return mod_filter


def remove_limited_amount_packs(packs):
    return [pack for pack in packs if Group.MAXIMUM_ONE not in pack.groups and Group.EXACTLY_TWO not in pack.groups]


def get_all_filler_items(include_traps: bool, exclude_ginger_island: bool) -> List[ItemData]:
    all_filler_items = [pack for pack in items_by_group[Group.RESOURCE_PACK]]
    all_filler_items.extend(items_by_group[Group.TRASH])
    if include_traps:
        all_filler_items.extend(items_by_group[Group.TRAP])
    all_filler_items = remove_excluded_items_island_mods(all_filler_items, exclude_ginger_island, set())
    return all_filler_items


def get_allowed_player_buffs(buff_option: EnabledFillerBuffs) -> List[ItemData]:
    allowed_buffs = []
    if OptionName.buff_luck in buff_option:
        allowed_buffs.append(item_table[Buff.luck])
    if OptionName.buff_damage in buff_option:
        allowed_buffs.append(item_table[Buff.damage])
    if OptionName.buff_defense in buff_option:
        allowed_buffs.append(item_table[Buff.defense])
    if OptionName.buff_immunity in buff_option:
        allowed_buffs.append(item_table[Buff.immunity])
    if OptionName.buff_health in buff_option:
        allowed_buffs.append(item_table[Buff.health])
    if OptionName.buff_energy in buff_option:
        allowed_buffs.append(item_table[Buff.energy])
    if OptionName.buff_bite in buff_option:
        allowed_buffs.append(item_table[Buff.bite_rate])
    if OptionName.buff_fish_trap in buff_option:
        allowed_buffs.append(item_table[Buff.fish_trap])
    if OptionName.buff_fishing_bar in buff_option:
        allowed_buffs.append(item_table[Buff.fishing_bar])
    if OptionName.buff_quality in buff_option:
        allowed_buffs.append(item_table[Buff.quality])
    if OptionName.buff_glow in buff_option:
        allowed_buffs.append(item_table[Buff.glow])
    return allowed_buffs


def get_stardrop_classification(options) -> ItemClassification:
    return ItemClassification.progression_skip_balancing if world_is_perfection(options) or world_is_stardrops(options) else ItemClassification.useful


def world_is_perfection(options) -> bool:
    return options.goal == Goal.option_perfection


def world_is_stardrops(options) -> bool:
    return options.goal == Goal.option_mystery_of_the_stardrops
