from collections import Counter
# import logging
from typing import TYPE_CHECKING, Any, Dict, Tuple, cast
from Options import AssembleOptions, DefaultOnToggle, Range, SpecialRange, Toggle, Choice
from zilliandomizer.options import \
    Options as ZzOptions, char_to_gun, char_to_jump, ID, \
    VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts
from zilliandomizer.options.parsing import validate as zz_validate
if TYPE_CHECKING:
    from BaseClasses import MultiWorld


class ZillionContinues(SpecialRange):
    """
    number of continues before game over

    game over teleports you to your ship, keeping items and open doors
    """
    default = 3
    range_start = 0
    range_end = 21
    display_name = "continues"
    special_range_names = {
        "vanilla": 3,
        "infinity": 21
    }


class ZillionFloppyReq(Range):
    """ how many floppy disks are required """
    range_start = 0
    range_end = 8
    default = 5
    display_name = "floppies required"


class VBLR(Choice):
    option_vanilla = 0
    option_balanced = 1
    option_low = 2
    option_restrictive = 3
    default = 1


class ZillionGunLevels(VBLR):
    """
    Zillion gun power for the number of Zillion power ups you pick up

    For "restrictive", Champ is the only one that can get Zillion gun power level 3.
    """
    display_name = "gun levels"


class ZillionJumpLevels(VBLR):
    """
    jump levels for each character level

    For "restrictive", Apple is the only one that can get jump level 3.
    """
    display_name = "jump levels"


class ZillionRandomizeAlarms(DefaultOnToggle):
    """ whether to randomize the locations of alarm sensors """
    display_name = "randomize alarms"


class ZillionMaxLevel(Range):
    """ the highest level you can get """
    range_start = 3
    range_end = 8
    default = 8
    display_name = "max level"


class ZillionOpasPerLevel(Range):
    """
    how many Opa-Opas are required to level up

    Lower makes you level up faster.
    """
    range_start = 1
    range_end = 5
    default = 2
    display_name = "Opa-Opas per level"


class ZillionStartChar(Choice):
    """ which character you start with """
    option_jj = 0
    option_apple = 1
    option_champ = 2
    display_name = "start character"
    default = "random"


class ZillionIDCardCount(Range):
    """
    how many ID Cards are in the game

    Vanilla is 63

    maximum total for all items is 144
    """
    range_start = 0
    range_end = 126
    default = 42
    display_name = "ID Card count"


class ZillionBreadCount(Range):
    """
    how many Breads are in the game

    Vanilla is 33

    maximum total for all items is 144
    """
    range_start = 0
    range_end = 126
    default = 50
    display_name = "Bread count"


class ZillionOpaOpaCount(Range):
    """
    how many Opa-Opas are in the game

    Vanilla is 26

    maximum total for all items is 144
    """
    range_start = 0
    range_end = 126
    default = 26
    display_name = "Opa-Opa count"


class ZillionZillionCount(Range):
    """
    how many Zillion gun power ups are in the game

    Vanilla is 6

    maximum total for all items is 144
    """
    range_start = 0
    range_end = 126
    default = 8
    display_name = "Zillion power up count"


class ZillionFloppyDiskCount(Range):
    """
    how many Floppy Disks are in the game

    Vanilla is 5

    maximum total for all items is 144
    """
    range_start = 0
    range_end = 126
    default = 7
    display_name = "Floppy Disk count"


class ZillionScopeCount(Range):
    """
    how many Scopes are in the game

    Vanilla is 4

    maximum total for all items is 144
    """
    range_start = 0
    range_end = 126
    default = 4
    display_name = "Scope count"


class ZillionRedIDCardCount(Range):
    """
    how many Red ID Cards are in the game

    Vanilla is 1

    maximum total for all items is 144
    """
    range_start = 0
    range_end = 126
    default = 2
    display_name = "Red ID Card count"


class ZillionEarlyScope(Toggle):
    """ make sure Scope is available early """
    display_name = "early scope"


class ZillionSkill(Range):
    """ the difficulty level of the game """
    range_start = 0
    range_end = 5
    default = 2


class ZillionStartingCards(SpecialRange):
    """
    how many ID Cards to start the game with

    Refilling at the ship also ensures you have at least this many cards.
    0 gives vanilla behavior.
    """
    default = 2
    range_start = 0
    range_end = 10
    display_name = "starting cards"
    special_range_names = {
        "vanilla": 0
    }


class ZillionRoomGen(Toggle):
    """ whether to generate rooms with random terrain """
    display_name = "room generation"


zillion_options: Dict[str, AssembleOptions] = {
    "continues": ZillionContinues,
    "floppy_req": ZillionFloppyReq,
    "gun_levels": ZillionGunLevels,
    "jump_levels": ZillionJumpLevels,
    "randomize_alarms": ZillionRandomizeAlarms,
    "max_level": ZillionMaxLevel,
    "start_char": ZillionStartChar,
    "opas_per_level": ZillionOpasPerLevel,
    "id_card_count": ZillionIDCardCount,
    "bread_count": ZillionBreadCount,
    "opa_opa_count": ZillionOpaOpaCount,
    "zillion_count": ZillionZillionCount,
    "floppy_disk_count": ZillionFloppyDiskCount,
    "scope_count": ZillionScopeCount,
    "red_id_card_count": ZillionRedIDCardCount,
    "early_scope": ZillionEarlyScope,
    "skill": ZillionSkill,
    "starting_cards": ZillionStartingCards,
    "room_gen": ZillionRoomGen,
}


def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
    tr: ZzItemCounts = {
        ID.card: ic["ID Card"],
        ID.red: ic["Red ID Card"],
        ID.floppy: ic["Floppy Disk"],
        ID.bread: ic["Bread"],
        ID.gun: ic["Zillion"],
        ID.opa: ic["Opa-Opa"],
        ID.scope: ic["Scope"],
        ID.empty: ic["Empty"],
    }
    return tr


def validate(world: "MultiWorld", p: int) -> "Tuple[ZzOptions, Counter[str]]":
    """
    adjusts options to make game completion possible

    `world` parameter is MultiWorld object that has my options on it
    `p` is my player id
    """
    for option_name in zillion_options:
        assert hasattr(world, option_name), f"Zillion option {option_name} didn't get put in world object"
    wo = cast(Any, world)  # so I don't need getattr on all the options

    skill = wo.skill[p].value

    jump_levels = cast(ZillionJumpLevels, wo.jump_levels[p])
    jump_option = jump_levels.get_current_option_name().lower()
    required_level = char_to_jump["Apple"][cast(ZzVBLR, jump_option)].index(3) + 1
    if skill == 0:
        # because of hp logic on final boss
        required_level = 8

    gun_levels = cast(ZillionGunLevels, wo.gun_levels[p])
    gun_option = gun_levels.get_current_option_name().lower()
    guns_required = char_to_gun["Champ"][cast(ZzVBLR, gun_option)].index(3)

    floppy_req = cast(ZillionFloppyReq, wo.floppy_req[p])

    card = cast(ZillionIDCardCount, wo.id_card_count[p])
    bread = cast(ZillionBreadCount, wo.bread_count[p])
    opa = cast(ZillionOpaOpaCount, wo.opa_opa_count[p])
    gun = cast(ZillionZillionCount, wo.zillion_count[p])
    floppy = cast(ZillionFloppyDiskCount, wo.floppy_disk_count[p])
    scope = cast(ZillionScopeCount, wo.scope_count[p])
    red = cast(ZillionRedIDCardCount, wo.red_id_card_count[p])
    item_counts = Counter({
        "ID Card": card,
        "Bread": bread,
        "Opa-Opa": opa,
        "Zillion": gun,
        "Floppy Disk": floppy,
        "Scope": scope,
        "Red ID Card": red
    })
    minimums = Counter({
        "ID Card": 0,
        "Bread": 0,
        "Opa-Opa": required_level - 1,
        "Zillion": guns_required,
        "Floppy Disk": floppy_req.value,
        "Scope": 0,
        "Red ID Card": 1
    })
    for key in minimums:
        item_counts[key] = max(minimums[key], item_counts[key])
    max_movables = 144 - sum(minimums.values())
    movables = item_counts - minimums
    while sum(movables.values()) > max_movables:
        # logging.warning("zillion options validate: player options item counts too high")
        total = sum(movables.values())
        scaler = max_movables / total
        for key in movables:
            movables[key] = int(movables[key] * scaler)
    item_counts = movables + minimums

    # now have required items, and <= 144

    # now fill remaining with empty
    total = sum(item_counts.values())
    diff = 144 - total
    if "Empty" not in item_counts:
        item_counts["Empty"] = 0
    item_counts["Empty"] += diff
    assert sum(item_counts.values()) == 144

    max_level = cast(ZillionMaxLevel, wo.max_level[p])
    max_level.value = max(required_level, max_level.value)

    opas_per_level = cast(ZillionOpasPerLevel, wo.opas_per_level[p])
    while (opas_per_level.value > 1) and (1 + item_counts["Opa-Opa"] // opas_per_level.value < max_level.value):
        # logging.warning(
        #     "zillion options validate: option opas_per_level incompatible with options max_level and opa_opa_count"
        # )
        opas_per_level.value -= 1

    # that should be all of the level requirements met

    start_char = cast(ZillionStartChar, wo.start_char[p])
    start_char_name = start_char.get_current_option_name()
    if start_char_name == "Jj":
        start_char_name = "JJ"
    assert start_char_name in chars
    start_char_name = cast(Chars, start_char_name)

    starting_cards = cast(ZillionStartingCards, wo.starting_cards[p])

    room_gen = cast(ZillionRoomGen, wo.room_gen[p])

    early_scope = cast(ZillionEarlyScope, wo.early_scope[p])
    if early_scope:
        world.early_items[p]["Scope"] = 1

    zz_item_counts = convert_item_counts(item_counts)
    zz_op = ZzOptions(
        zz_item_counts,
        cast(ZzVBLR, jump_option),
        cast(ZzVBLR, gun_option),
        opas_per_level.value,
        max_level.value,
        False,  # tutorial
        skill,
        start_char_name,
        floppy_req.value,
        wo.continues[p].value,
        wo.randomize_alarms[p].value,
        False,  # early scope is done with AP early_items API
        True,  # balance defense
        starting_cards.value,
        bool(room_gen.value)
    )
    zz_validate(zz_op)
    return zz_op, item_counts
