from typing import Dict, Union, List
from BaseClasses import MultiWorld
from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict, OptionList
from schema import Schema, And, Optional, Or


class StartWithJewelryBox(Toggle):
    "Start with Jewelry Box unlocked"
    display_name = "Start with Jewelry Box"


class DownloadableItems(DefaultOnToggle):
    "With the tablet you will be able to download items at terminals"
    display_name = "Downloadable items"


class EyeSpy(Toggle):
    "Requires Oculus Ring in inventory to be able to break hidden walls."
    display_name = "Eye Spy"


class StartWithMeyef(Toggle):
    "Start with Meyef, ideal for when you want to play multiplayer."
    display_name = "Start with Meyef"


class QuickSeed(Toggle):
    "Start with Talaria Attachment, Nyoom!"
    display_name = "Quick seed"


class SpecificKeycards(Toggle):
    "Keycards can only open corresponding doors"
    display_name = "Specific Keycards"


class Inverted(Toggle):
    "Start in the past"
    display_name = "Inverted"


class GyreArchives(Toggle):
    "Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo"
    display_name = "Gyre Archives"


class Cantoran(Toggle):
    "Cantoran's fight and check are available upon revisiting his room"
    display_name = "Cantoran"


class LoreChecks(Toggle):
    "Memories and journal entries contain items."
    display_name = "Lore Checks"


class BossRando(Choice):
    "Wheter all boss locations are shuffled, and if their damage/hp should be scaled."
    display_name = "Boss Randomization"
    option_off = 0
    option_scaled = 1
    option_unscaled = 2
    alias_true = 1


class EnemyRando(Choice):
    "Wheter enemies will be randomized, and if their damage/hp should be scaled."
    display_name = "Enemy Randomization"
    option_off = 0
    option_scaled = 1
    option_unscaled = 2
    option_ryshia = 3
    alias_true = 1


class DamageRando(Choice):
    "Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings."
    display_name = "Damage Rando"
    option_off = 0
    option_allnerfs = 1
    option_mostlynerfs = 2
    option_balanced = 3
    option_mostlybuffs = 4
    option_allbuffs = 5
    option_manual = 6
    alias_true = 2


class DamageRandoOverrides(OptionDict):
    """Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that
    you don't specify will roll with 1/1/1 as odds"""
    schema = Schema({
        Optional("Blue"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Blade"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Fire"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Plasma"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Iron"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Ice"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Wind"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Gun"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Umbra"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Empire"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Eye"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Blood"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("ForbiddenTome"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Shattered"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Nether"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
        Optional("Radiant"): { 
            "MinusOdds": And(int, lambda n: n >= 0), 
            "NormalOdds": And(int, lambda n: n >= 0), 
            "PlusOdds": And(int, lambda n: n >= 0) 
        },
    })
    display_name = "Damage Rando Overrides"
    default = {
        "Blue": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Blade": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Fire": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Plasma": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Iron": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Ice": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Wind": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Gun": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Umbra": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Empire": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Eye": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Blood": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "ForbiddenTome": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Shattered": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Nether": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
        "Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 },
    }


class HpCap(Range):
    "Sets the number that Lunais's HP maxes out at."
    display_name = "HP Cap"
    range_start = 1
    range_end = 999
    default = 999


class LevelCap(Range):
    """Sets the max level Lunais can achieve."""
    display_name = "Level Cap"
    range_start = 1
    range_end = 99
    default = 99


class ExtraEarringsXP(Range):
    """Adds additional XP granted by Galaxy Earrings."""
    display_name = "Extra Earrings XP"
    range_start = 0
    range_end = 24
    default = 0
    

class BossHealing(DefaultOnToggle):
    "Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled."
    display_name = "Heal After Bosses"


class ShopFill(Choice):
    """Sets the items for sale in Merchant Crow's shops.
    Default: No sunglasses or trendy jacket, but sand vials for sale.
    Randomized: Up to 4 random items in each shop.
    Vanilla: Keep shops the same as the base game.
    Empty: Sell no items at the shop."""
    display_name = "Shop Inventory"
    option_default = 0
    option_randomized = 1
    option_vanilla = 2
    option_empty = 3


class ShopWarpShards(DefaultOnToggle):
    "Shops always sell warp shards (when keys possessed), ignoring inventory setting."
    display_name = "Always Sell Warp Shards"


class ShopMultiplier(Range):
    "Multiplier for the cost of items in the shop. Set to 0 for free shops."
    display_name = "Shop Price Multiplier"
    range_start = 0
    range_end = 10
    default = 1


class LootPool(Choice):
    """Sets the items that drop from enemies (does not apply to boss reward checks)
    Vanilla: Drops are the same as the base game
    Randomized: Each slot of every enemy's drop table is given a random use item or piece of equipment.
    Empty: Enemies drop nothing."""
    display_name = "Loot Pool"
    option_vanilla = 0
    option_randomized = 1
    option_empty = 2


class DropRateCategory(Choice):
    """Sets the drop rate when 'Loot Pool' is set to 'Random'
    Tiered: Based on item rarity/value
    Vanilla: Based on bestiary slot the item is placed into
    Random: Assigned a random tier/drop rate
    Fixed: Set by the 'Fixed Drop Rate' setting
    """
    display_name = "Drop Rate Category"
    option_tiered = 0
    option_vanilla = 1
    option_randomized = 2
    option_fixed = 3


class FixedDropRate(Range):
    "Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'"
    display_name = "Fixed Drop Rate"
    range_start = 0
    range_end = 100
    default = 5


class LootTierDistro(Choice):
    """Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random'
    Default Weight: Rarer items will be assigned to enemy drop slots less frequently than common items
    Full Random: Any item has an equal chance of being placed in an enemy's drop slot
    Inverted Weight: Rarest items show up the most frequently, while common items are the rarest
    """
    display_name = "Loot Tier Distrubution"
    option_default_weight = 0
    option_full_random = 1
    option_inverted_weight = 2


class ShowBestiary(Toggle):
    "All entries in the bestiary are visible, without needing to kill one of a given enemy first"
    display_name = "Show Bestiary Entries"


class ShowDrops(Toggle):
    "All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first"
    display_name = "Show Bestiary Item Drops"


class EnterSandman(Toggle):
    "The Ancient Pyramid is unlocked by the Twin Pyramid Keys, but the final boss door opens if you have all 5 Timespinner pieces"
    display_name = "Enter Sandman"


class DadPercent(Toggle):
    """The win condition is beating the boss of Emperor's Tower"""
    display_name = "Dad Percent"


class RisingTides(Toggle):
    """Random areas are flooded or drained, can be further specified with RisingTidesOverrides"""
    display_name = "Rising Tides"


def rising_tide_option(location: str, with_save_point_option: bool = False) -> Dict[Optional, Or]:
    if with_save_point_option:
        return {
            Optional(location): Or(
                And({
                    Optional("Dry"): And(int, lambda n: n >= 0),
                    Optional("Flooded"): And(int, lambda n: n >= 0),
                    Optional("FloodedWithSavePointAvailable"): And(int, lambda n: n >= 0)
                }, lambda d: any(v > 0 for v in d.values())),
                "Dry",
                "Flooded",
                "FloodedWithSavePointAvailable")
        }
    else:
        return {
            Optional(location): Or(
                And({
                    Optional("Dry"): And(int, lambda n: n >= 0),
                    Optional("Flooded"): And(int, lambda n: n >= 0)
                }, lambda d: any(v > 0 for v in d.values())),
                "Dry",
                "Flooded")
        }


class RisingTidesOverrides(OptionDict):
    """Odds for specific areas to be flooded or drained, only has effect when RisingTides is on.
    Areas that are not specified will roll with the default 33% chance of getting flooded or drained"""
    display_name = "Rising Tides Overrides"
    schema = Schema({
        **rising_tide_option("Xarion"),
        **rising_tide_option("Maw"),
        **rising_tide_option("AncientPyramidShaft"),
        **rising_tide_option("Sandman"),
        **rising_tide_option("CastleMoat"),
        **rising_tide_option("CastleBasement", with_save_point_option=True),
        **rising_tide_option("CastleCourtyard"),
        **rising_tide_option("LakeDesolation"),
        **rising_tide_option("LakeSerene"),
        **rising_tide_option("LakeSereneBridge"),
        **rising_tide_option("Lab"),
    })
    default = {
        "Xarion": { "Dry": 67, "Flooded": 33 },
        "Maw": { "Dry": 67, "Flooded": 33 },
        "AncientPyramidShaft": { "Dry": 67, "Flooded": 33 },
        "Sandman": { "Dry": 67, "Flooded": 33 },
        "CastleMoat": { "Dry": 67, "Flooded": 33 },
        "CastleBasement": { "Dry": 66, "Flooded": 17, "FloodedWithSavePointAvailable": 17 },
        "CastleCourtyard": { "Dry": 67, "Flooded": 33 },
        "LakeDesolation": { "Dry": 67, "Flooded": 33 },
        "LakeSerene": { "Dry": 33, "Flooded": 67 },
        "LakeSereneBridge": { "Dry": 67, "Flooded": 33 },
        "Lab": { "Dry": 67, "Flooded": 33 },
    }


class UnchainedKeys(Toggle):
    """Start with Twin Pyramid Key, which does not give free warp;
    warp items for Past, Present, (and ??? with Enter Sandman) can be found."""
    display_name = "Unchained Keys"


class TrapChance(Range):
    """Chance of traps in the item pool.
    Traps will only replace filler items such as potions, vials and antidotes"""
    display_name = "Trap Chance"
    range_start = 0
    range_end = 100
    default = 10


class Traps(OptionList):
    """List of traps that may be in the item pool to find"""
    display_name = "Traps Types"
    valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" }
    default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ]


class PresentAccessWithWheelAndSpindle(Toggle):
    """When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired."""
    display_name = "Past Wheel & Spindle Warp"


# Some options that are available in the timespinner randomizer arent currently implemented
timespinner_options: Dict[str, Option] = {
    "StartWithJewelryBox": StartWithJewelryBox,
    "DownloadableItems": DownloadableItems,
    "EyeSpy": EyeSpy,
    "StartWithMeyef": StartWithMeyef,
    "QuickSeed": QuickSeed,
    "SpecificKeycards": SpecificKeycards,
    "Inverted": Inverted,
    "GyreArchives": GyreArchives,
    "Cantoran": Cantoran,
    "LoreChecks": LoreChecks,
    "BossRando": BossRando,
    "EnemyRando": EnemyRando,
    "DamageRando": DamageRando,
    "DamageRandoOverrides": DamageRandoOverrides,
    "HpCap": HpCap,
    "LevelCap": LevelCap,
    "ExtraEarringsXP": ExtraEarringsXP,
    "BossHealing": BossHealing,
    "ShopFill": ShopFill,
    "ShopWarpShards": ShopWarpShards,
    "ShopMultiplier": ShopMultiplier,
    "LootPool": LootPool,
    "DropRateCategory": DropRateCategory,
    "FixedDropRate": FixedDropRate,
    "LootTierDistro": LootTierDistro,
    "ShowBestiary": ShowBestiary,
    "ShowDrops": ShowDrops,
    "EnterSandman": EnterSandman,
    "DadPercent": DadPercent,
    "RisingTides": RisingTides,
    "RisingTidesOverrides": RisingTidesOverrides,
    "UnchainedKeys": UnchainedKeys,
    "TrapChance": TrapChance,
    "Traps": Traps,
    "PresentAccessWithWheelAndSpindle": PresentAccessWithWheelAndSpindle,
    "DeathLink": DeathLink,
}


def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool:
    return get_option_value(world, player, name) > 0


def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, Dict, List]:
    option = getattr(world, name, None)
    if option == None:
        return 0

    return option[player].value
