import logging
from dataclasses import dataclass
from typing import Tuple, List, TYPE_CHECKING, Set, Dict, Optional, Union
from BaseClasses import Item, ItemClassification, Location, LocationProgressType, CollectionState
from . import StaticWitnessLogic
from .utils import weighted_sample

if TYPE_CHECKING:
    from . import WitnessWorld

CompactItemData = Tuple[str, Union[str, int], int]

joke_hints = [
    "Quaternions break my brain",
    "Eclipse has nothing, but you should do it anyway.",
    "Beep",
    "Putting in custom subtitles shouldn't have been as hard as it was...",
    "BK mode is right around the corner.",
    "You can do it!",
    "I believe in you!",
    "The person playing is cute. <3",
    "dash dot, dash dash dash,\ndash, dot dot dot dot, dot dot,\ndash dot, dash dash dot",
    "When you think about it, there are actually a lot of bubbles in a stream.",
    "Never gonna give you up\nNever gonna let you down\nNever gonna run around and desert you",
    "Thanks to the Archipelago developers for making this possible.",
    "Have you tried ChecksFinder?\nIf you like puzzles, you might enjoy it!",
    "Have you tried Dark Souls III?\nA tough game like this feels better when friends are helping you!",
    "Have you tried Donkey Kong Country 3?\nA legendary game from a golden age of platformers!",
    "Have you tried Factorio?\nAlone in an unknown multiworld. Sound familiar?",
    "Have you tried Final Fantasy?\nExperience a classic game improved to fit modern standards!",
    "Have you tried Hollow Knight?\nAnother independent hit revolutionising a genre!",
    "Have you tried A Link to the Past?\nThe Archipelago game that started it all!",
    "Have you tried Meritous?\nYou should know that obscure games are often groundbreaking!",
    "Have you tried Ocarina of Time?\nOne of the biggest randomizers, big inspiration for this one's features!",
    "Have you tried Raft?\nHaven't you always wanted to explore the ocean surrounding this island?",
    "Have you tried Risk of Rain 2?\nI haven't either. But I hear it's incredible!",
    "Have you tried Rogue Legacy?\nAfter solving so many puzzles it's the perfect way to rest your \"thinking\" brain.",
    "Have you tried Secret of Evermore?\nI haven't either. But I hear it's great!",
    "Have you tried Slay the Spire?\nExperience the thrill of combat without needing fast fingers!",
    "Have you tried SMZ3?\nWhy play one incredible game when you can play 2 at once?",
    "Have you tried Starcraft 2?\nUse strategy and management to crush your enemies!",
    "Have you tried Super Mario 64?\n3-dimensional games like this owe everything to that game.",
    "Have you tried Super Metroid?\nA classic game, yet still one of the best in the genre.",
    "Have you tried Timespinner?\nEveryone who plays it ends up loving it!",
    "Have you tried VVVVVV?\nExperience the essence of gaming distilled into its purest form!",
    "Have you tried The Witness?\nOh. I guess you already have. Thanks for playing!",
    "Have you tried Super Mario World?\nI don't think I need to tell you that it is beloved by many.",
    "Have you tried Overcooked 2?\nWhen you're done relaxing with puzzles, use your energy to yell at your friends.",
    "Have you tried Zillion?\nMe neither. But it looks fun. So, let's try something new together?",
    "Have you tried Hylics 2?\nStop motion might just be the epitome of unique art styles.",
    "Have you tried Pokemon Red&Blue?\nA cute pet collecting game that fascinated an entire generation.",
    "Have you tried Lufia II?\nRoguelites are not just a 2010s phenomenon, turns out.",
    "Have you tried Minecraft?\nI have recently learned this is a question that needs to be asked.",
    "Have you tried Subnautica?\nIf you like this game's lonely atmosphere, I would suggest you try it.",

    "Have you tried Sonic Adventure 2?\nIf the silence on this island is getting to you, "
    "there aren't many games more energetic.",

    "Waiting to get your items?\nTry BK Sudoku! Make progress even while stuck.",

    "Have you tried Adventure?\n...Holy crud, that game is 17 years older than me.",
    "Have you tried Muse Dash?\nRhythm game with cute girls!\n(Maybe skip if you don't like the Jungle panels)",
    "Have you tried Clique?\nIt's certainly a lot less complicated than this game!",
    "Have you tried Bumper Stickers?\nDecades after its inception, people are still inventing unique twists on the match-3 genre.",
    "Have you tried DLC Quest?\nI know you all like parody games.\nI got way too many requests to make a randomizer for \"The Looker\".",
    "Have you tried Doom?\nI wonder if a smart fridge can connect to Archipelago.",
    "Have you tried Kingdom Hearts II?\nI'll wait for you to name a more epic crossover.",
    "Have you tried Link's Awakening DX?\nHopefully, Link won't be obsessed with circles when he wakes up.",
    "Have you tried The Messenger?\nOld ideas made new again. It's how all art is made.",
    "Have you tried Mega Man Battle Network 3?\nIt's a Mega Man RPG. How could you not want to try that?",
    "Have you tried Noita?\nIf you like punishing yourself, you will like it.",
    "Have you tried Stardew Valley?\nThe Farming game that gave a damn. It's so easy to lose hours and days to it...",
    "Have you tried The Legend of Zelda?\nIn some sense, it was the starting point of \"adventure\" in video games.",
    "Have you tried Undertale?\nI hope I'm not the 10th person to ask you that. But it's, like, really good.",
    "Have you tried Wargroove?\nI'm glad that for every abandoned series, enough people are yearning for its return that one of them will know how to code.",
    "Have you tried Blasphemous?\nYou haven't? Blasphemy!\n...Sorry. You should try it, though!",
    "Have you tried Doom II?\nGot a good game on your hands? Just make it bigger and better.",
    "Have you tried Lingo?\nIt's an open world puzzle game. It features panels with non-verbally explained mechanics.\nIf you like this game, you'll like Lingo too.",
    "(Middle Yellow)\nYOU AILED OVERNIGHT\nH--- --- ----- -----?",
    "Have you tried Bumper Stickers?\nMaybe after spending so much time on this island, you are longing for a simpler puzzle game.",
    "Have you tried Pokemon Emerald?\nI'm going to say it: 10/10, just the right amount of water.",
    "Have you tried Terraria?\nA prime example of a survival sandbox game that beats the \"Wide as an ocean, deep as a puddle\" allegations.",
    "Have you tried Final Fantasy Mystic Quest?\nApparently, it was made in an attempt to simplify Final Fantasy for the western market.\nThey were right, I suck at RPGs.",
    "Have you tried Shivers?\nWitness 2 should totally feature a haunted Museum.",
    "Have you tried Heretic?\nWait, there is a Doom Engine game where you can look UP AND DOWN???",
    
    "One day I was fascinated by the subject of generation of waves by wind.",
    "I don't like sandwiches. Why would you think I like sandwiches? Have you ever seen me with a sandwich?",
    "Where are you right now?\nI'm at soup!\nWhat do you mean you're at soup?",
    "Remember to ask in the Archipelago Discord what the Functioning Brain does.",
    "Don't use your puzzle skips, you might need them later.",
    "For an extra challenge, try playing blindfolded.",
    "Go to the top of the mountain and see if you can see your house.",
    "Yellow = Red + Green\nCyan = Green + Blue\nMagenta = Red + Blue",
    "Maybe that panel really is unsolvable.",
    "Did you make sure it was plugged in?",
    "Do not look into laser with remaining eye.",
    "Try pressing Space to jump.",
    "The Witness is a Doom clone.\nJust replace the demons with puzzles",
    "Test Hint please ignore",
    "Shapers can never be placed outside the panel boundaries, even if subtracted.",
    "The Keep laser panels use the same trick on both sides!",
    "Can't get past a door? Try going around. Can't go around? Try building a nether portal.",
    "We've been trying to reach you about your car's extended warranty.",
    "I hate this game. I hate this game. I hate this game.\n- Chess player Bobby Fischer",
    "Dear Mario,\nPlease come to the castle. I've baked a cake for you!",
    "Have you tried waking up?\nYeah, me neither.",
    "Why do they call it The Witness, when wit game the player view play of with the game.",
    "THE WIND FISH IN NAME ONLY, FOR IT IS NEITHER",
    "Like this game?\nTry The Wit.nes, Understand, INSIGHT, Taiji What the Witness?, and Tametsi.",
    "In a race, It's survival of the Witnesst.",
    "This hint has been removed. We apologize for your inconvenience.",
    "O-----------",
    "Circle is draw\nSquare is separate\nLine is win",
    "Circle is draw\nStar is pair\nLine is win",
    "Circle is draw\nCircle is copy\nLine is win",
    "Circle is draw\nDot is eat\nLine is win",
    "Circle is start\nWalk is draw\nLine is win",
    "Circle is start\nLine is win\nWitness is you",
    "Can't find any items?\nConsider a relaxing boat trip around the island!",
    "Don't forget to like, comment, and subscribe.",
    "Ah crap, gimme a second.\n[papers rustling]\nSorry, nothing.",
    "Trying to get a hint? Too bad.",
    "Here's a hint: Get good at the game.",
    "I'm still not entirely sure what we're witnessing here.",
    "Have you found a red page yet? No? Then have you found a blue page?",
    "And here we see the Witness player, seeking answers where there are none-\nDid someone turn on the loudspeaker?",

    "Be quiet. I can't hear the elevator.",
    "Witness me.\n- The famous last words of John Witness.",
    "It's okay, I always have to skip the Rotated Shaper puzzles too.",
    "Alan please add hint.",
    "Rumor has it there's an audio log with a hint nearby.",
    "In the future, war will break out between obelisk_sides and individual EP players.\nWhich side are you on?",
    "Droplets: Low, High, Mid.\nAmbience: Mid, Low, Mid, High.",
    "Name a better game involving lines. I'll wait.",
    "\"You have to draw a line in the sand.\"\n- Arin \"Egoraptor\" Hanson",
    "Have you tried?\nThe puzzles tend to get easier if you do.",
    "Sorry, I accidentally left my phone in the Jungle.\nAnd also all my fragile dishes.",
    "Winner of the \"Most Irrelevant PR in AP History\" award!",
    "I bet you wish this was a real hint :)",
    "\"This hint is an impostor.\"- Junk hint submitted by T1mshady.\n...wait, I'm not supposed to say that part?",
    "Wouldn't you like to know, weather buoy?",
    "Give me a few minutes, I should have better material by then.",
    "Just pet the doggy! You know you want to!!!",
    "ceci n'est pas une metroidvania",
    "HINT is MELT\nYOU is HOT",
    "Who's that behind you?",
    ":3",
    "^v ^^v> >>^>v\n^^v>v ^v>> v>^> v>v^",
    "Statement #0162601, regarding a strange island that--\nOh, wait, sorry. I'm not supposed to be here.",
    "Hollow Bastion has 6 progression items.\nOr maybe it doesn't.\nI wouldn't know.",
    "Set your hint count lower so I can tell you more jokes next time.",
    "A non-edge start point is similar to a cat.\nIt must be either inside or outside, it can't be both.",
    "What if we kissed on the Bunker Laser Platform?\nJk... unless?",
    "You don't have Boat? Invisible boat time!\nYou do have boat? Boat clipping time!",
    "Cet indice est en français. Nous nous excusons de tout inconvénients engendrés par cela.",
    "How many of you have personally witnessed a total solar eclipse?",
    "In the Treehouse area, you will find 69 progression items.\nNice.\n(Source: Just trust me)",
    "Lingo\nLingoing\nLingone",
    "The name of the captain was Albert Einstein.",
    "Panel impossible Sigma plz fix",
    "Welcome Back! (:",
    "R R R U L L U L U R U R D R D R U U",
    "Have you tried checking your tracker?",
    
    "Hints suggested by:\nIHNN, Beaker, MrPokemon11, Ember, TheM8, NewSoupVi, Jasper Bird, T1mshady,"
    "KF, Yoshi348, Berserker, BowlinJim, oddGarrett, Pink Switch, Rever, Ishigh, snolid.",
]


@dataclass
class WitnessLocationHint:
    location: Location
    hint_came_from_location: bool

    # If a hint gets added to a set twice, but once as an item hint and once as a location hint, those are the same
    def __hash__(self):
        return hash(self.location)

    def __eq__(self, other):
        return self.location == other.location


@dataclass
class WitnessWordedHint:
    wording: str
    location: Optional[Location] = None
    area: Optional[str] = None
    area_amount: Optional[int] = None


def get_always_hint_items(world: "WitnessWorld") -> List[str]:
    always = [
        "Boat",
        "Caves Shortcuts",
        "Progressive Dots",
    ]

    difficulty = world.options.puzzle_randomization
    discards = world.options.shuffle_discarded_panels
    wincon = world.options.victory_condition

    if discards:
        if difficulty == "sigma_expert":
            always.append("Arrows")
        else:
            always.append("Triangles")

    if wincon == "elevator":
        always += ["Mountain Bottom Floor Pillars Room Entry (Door)", "Mountain Bottom Floor Doors"]

    if wincon == "challenge":
        always += ["Challenge Entry (Panel)", "Caves Panels"]

    return always


def get_always_hint_locations(world: "WitnessWorld") -> List[str]:
    always = [
        "Challenge Vault Box",
        "Mountain Bottom Floor Discard",
        "Theater Eclipse EP",
        "Shipwreck Couch EP",
        "Mountainside Cloud Cycle EP",
    ]

    # Add Obelisk Sides that contain EPs that are meant to be hinted, if they are necessary to complete the Obelisk Side
    if "0x339B6" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
        always.append("Town Obelisk Side 6")  # Eclipse EP

    if "0x3388F" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
        always.append("Treehouse Obelisk Side 4")  # Couch EP

    if "0x335AE" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
        always.append("Mountainside Obelisk Side 1")  # Cloud Cycle EP.

    return always


def get_priority_hint_items(world: "WitnessWorld") -> List[str]:
    priority = {
        "Caves Mountain Shortcut (Door)",
        "Caves Swamp Shortcut (Door)",
        "Swamp Entry (Panel)",
        "Swamp Laser Shortcut (Door)",
    }

    if world.options.shuffle_symbols:
        symbols = [
            "Progressive Dots",
            "Progressive Stars",
            "Shapers",
            "Rotated Shapers",
            "Negative Shapers",
            "Arrows",
            "Triangles",
            "Eraser",
            "Black/White Squares",
            "Colored Squares",
            "Sound Dots",
            "Progressive Symmetry"
        ]

        priority.update(world.random.sample(symbols, 5))

    if world.options.shuffle_lasers:
        lasers = [
            "Symmetry Laser",
            "Town Laser",
            "Keep Laser",
            "Swamp Laser",
            "Treehouse Laser",
            "Monastery Laser",
            "Jungle Laser",
            "Quarry Laser",
            "Bunker Laser",
            "Shadows Laser",
        ]

        if world.options.shuffle_doors >= 2:
            priority.add("Desert Laser")
            priority.update(world.random.sample(lasers, 5))

        else:
            lasers.append("Desert Laser")
            priority.update(world.random.sample(lasers, 6))

    return sorted(priority)


def get_priority_hint_locations(world: "WitnessWorld") -> List[str]:
    priority = [
        "Tutorial Patio Floor",
        "Tutorial Patio Flowers EP",
        "Swamp Purple Underwater",
        "Shipwreck Vault Box",
        "Town RGB House Upstairs Left",
        "Town RGB House Upstairs Right",
        "Treehouse Green Bridge 7",
        "Treehouse Green Bridge Discard",
        "Shipwreck Discard",
        "Desert Vault Box",
        "Mountainside Vault Box",
        "Mountainside Discard",
        "Tunnels Theater Flowers EP",
        "Boat Shipwreck Green EP",
        "Quarry Stoneworks Control Room Left",
    ]
    
    # Add Obelisk Sides that contain EPs that are meant to be hinted, if they are necessary to complete the Obelisk Side
    if "0x33A20" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
        priority.append("Town Obelisk Side 6")  # Theater Flowers EP

    if "0x28B29" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
        priority.append("Treehouse Obelisk Side 4")  # Shipwreck Green EP

    if "0x33600" not in world.player_logic.COMPLETELY_DISABLED_ENTITIES:
        priority.append("Town Obelisk Side 2")  # Tutorial Patio Flowers EP.

    return priority


def word_direct_hint(world: "WitnessWorld", hint: WitnessLocationHint):
    location_name = hint.location.name
    if hint.location.player != world.player:
        location_name += " (" + world.multiworld.get_player_name(hint.location.player) + ")"

    item = hint.location.item
    item_name = item.name
    if item.player != world.player:
        item_name += " (" + world.multiworld.get_player_name(item.player) + ")"

    if hint.hint_came_from_location:
        hint_text = f"{location_name} contains {item_name}."
    else:
        hint_text = f"{item_name} can be found at {location_name}."

    return WitnessWordedHint(hint_text, hint.location)


def hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]) -> Optional[WitnessLocationHint]:

    locations = [item.location for item in own_itempool if item.name == item_name and item.location]

    if not locations:
        return None

    location_obj = world.random.choice(locations)
    location_name = location_obj.name

    if location_obj.player != world.player:
        location_name += " (" + world.multiworld.get_player_name(location_obj.player) + ")"

    return WitnessLocationHint(location_obj, False)


def hint_from_location(world: "WitnessWorld", location: str) -> Optional[WitnessLocationHint]:
    location_obj = world.multiworld.get_location(location, world.player)
    item_obj = world.multiworld.get_location(location, world.player).item
    item_name = item_obj.name
    if item_obj.player != world.player:
        item_name += " (" + world.multiworld.get_player_name(item_obj.player) + ")"

    return WitnessLocationHint(location_obj, True)


def get_items_and_locations_in_random_order(world: "WitnessWorld", own_itempool: List[Item]):
    prog_items_in_this_world = sorted(
        item.name for item in own_itempool
        if item.advancement and item.code and item.location
    )
    locations_in_this_world = sorted(
        location.name for location in world.multiworld.get_locations(world.player)
        if location.address and location.progress_type != LocationProgressType.EXCLUDED
    )

    world.random.shuffle(prog_items_in_this_world)
    world.random.shuffle(locations_in_this_world)

    return prog_items_in_this_world, locations_in_this_world


def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List[Item],
                                   already_hinted_locations: Set[Location]
                                   ) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]:
    prog_items_in_this_world, loc_in_this_world = get_items_and_locations_in_random_order(world, own_itempool)

    always_locations = [
        location for location in get_always_hint_locations(world)
        if location in loc_in_this_world
    ]
    always_items = [
        item for item in get_always_hint_items(world)
        if item in prog_items_in_this_world
    ]
    priority_locations = [
        location for location in get_priority_hint_locations(world)
        if location in loc_in_this_world
    ]
    priority_items = [
        item for item in get_priority_hint_items(world)
        if item in prog_items_in_this_world
    ]

    # Get always and priority location/item hints
    always_location_hints = {hint_from_location(world, location) for location in always_locations}
    always_item_hints = {hint_from_item(world, item, own_itempool) for item in always_items}
    priority_location_hints = {hint_from_location(world, location) for location in priority_locations}
    priority_item_hints = {hint_from_item(world, item, own_itempool) for item in priority_items}

    # Combine the sets. This will get rid of duplicates
    always_hints_set = always_item_hints | always_location_hints
    priority_hints_set = priority_item_hints | priority_location_hints

    # Make sure priority hints doesn't contain any hints that are already always hints.
    priority_hints_set -= always_hints_set

    always_generator = [hint for hint in always_hints_set if hint and hint.location not in already_hinted_locations]
    priority_generator = [hint for hint in priority_hints_set if hint and hint.location not in already_hinted_locations]

    # Convert both hint types to list and then shuffle. Also, get rid of None and Tutorial Gate Open.
    always_hints = sorted(always_generator, key=lambda h: h.location)
    priority_hints = sorted(priority_generator, key=lambda h: h.location)
    world.random.shuffle(always_hints)
    world.random.shuffle(priority_hints)

    return always_hints, priority_hints


def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item],
                              already_hinted_locations: Set[Location], hints_to_use_first: List[WitnessLocationHint],
                              unhinted_locations_for_hinted_areas: Dict[str, Set[Location]]) -> List[WitnessWordedHint]:
    prog_items_in_this_world, locations_in_this_world = get_items_and_locations_in_random_order(world, own_itempool)

    next_random_hint_is_location = world.random.randrange(0, 2)

    hints = []

    # This is a way to reverse a Dict[a,List[b]] to a Dict[b,a]
    area_reverse_lookup = {v: k for k, l in unhinted_locations_for_hinted_areas.items() for v in l}

    while len(hints) < hint_amount:
        if not prog_items_in_this_world and not locations_in_this_world and not hints_to_use_first:
            player_name = world.multiworld.get_player_name(world.player)
            logging.warning(f"Ran out of items/locations to hint for player {player_name}.")
            break

        if hints_to_use_first:
            location_hint = hints_to_use_first.pop()
        elif next_random_hint_is_location and locations_in_this_world:
            location_hint = hint_from_location(world, locations_in_this_world.pop())
        elif not next_random_hint_is_location and prog_items_in_this_world:
            location_hint = hint_from_item(world, prog_items_in_this_world.pop(), own_itempool)
        # The list that the hint was supposed to be taken from was empty.
        # Try the other list, which has to still have something, as otherwise, all lists would be empty,
        # which would have triggered the guard condition above.
        else:
            next_random_hint_is_location = not next_random_hint_is_location
            continue

        if not location_hint or location_hint.location in already_hinted_locations:
            continue

        # Don't hint locations in areas that are almost fully hinted out already
        if location_hint.location in area_reverse_lookup:
            area = area_reverse_lookup[location_hint.location]
            if len(unhinted_locations_for_hinted_areas[area]) == 1:
                continue
            del area_reverse_lookup[location_hint.location]
            unhinted_locations_for_hinted_areas[area] -= {location_hint.location}

        hints.append(word_direct_hint(world, location_hint))
        already_hinted_locations.add(location_hint.location)

        next_random_hint_is_location = not next_random_hint_is_location

    return hints


def generate_joke_hints(world: "WitnessWorld", amount: int) -> List[Tuple[str, int, int]]:
    return [(x, -1, -1) for x in world.random.sample(joke_hints, amount)]


def choose_areas(world: "WitnessWorld", amount: int, locations_per_area: Dict[str, List[Location]],
                 already_hinted_locations: Set[Location]) -> Tuple[List[str], Dict[str, Set[Location]]]:
    """
    Choose areas to hint.
    This takes into account that some areas may already have had items hinted in them through location hints.
    When this happens, they are made less likely to receive an area hint.
    """

    unhinted_locations_per_area = dict()
    unhinted_location_percentage_per_area = dict()

    for area_name, locations in locations_per_area.items():
        not_yet_hinted_locations = sum(location not in already_hinted_locations for location in locations)
        unhinted_locations_per_area[area_name] = {loc for loc in locations if loc not in already_hinted_locations}
        unhinted_location_percentage_per_area[area_name] = not_yet_hinted_locations / len(locations)

    items_per_area = {area_name: [location.item for location in locations]
                      for area_name, locations in locations_per_area.items()}

    areas = sorted(area for area in items_per_area if unhinted_location_percentage_per_area[area])
    weights = [unhinted_location_percentage_per_area[area] for area in areas]

    amount = min(amount, len(weights))

    hinted_areas = weighted_sample(world.random, areas, weights, amount)

    return hinted_areas, unhinted_locations_per_area


def get_hintable_areas(world: "WitnessWorld") -> Tuple[Dict[str, List[Location]], Dict[str, List[Item]]]:
    potential_areas = list(StaticWitnessLogic.ALL_AREAS_BY_NAME.keys())

    locations_per_area = dict()
    items_per_area = dict()

    for area in potential_areas:
        regions = [
            world.regio.created_regions[region]
            for region in StaticWitnessLogic.ALL_AREAS_BY_NAME[area]["regions"]
            if region in world.regio.created_regions
        ]
        locations = [location for region in regions for location in region.get_locations() if location.address]

        if locations:
            locations_per_area[area] = locations
            items_per_area[area] = [location.item for location in locations]

    return locations_per_area, items_per_area


def word_area_hint(world: "WitnessWorld", hinted_area: str, corresponding_items: List[Item]) -> Tuple[str, int]:
    """
    Word the hint for an area using natural sounding language.
    This takes into account how much progression there is, how much of it is local/non-local, and whether there are
    any local lasers to be found in this area.
    """

    local_progression = sum(item.player == world.player and item.advancement for item in corresponding_items)
    non_local_progression = sum(item.player != world.player and item.advancement for item in corresponding_items)

    laser_names = {"Symmetry Laser", "Desert Laser", "Quarry Laser", "Shadows Laser", "Town Laser", "Monastery Laser",
                   "Jungle Laser", "Bunker Laser", "Swamp Laser", "Treehouse Laser", "Keep Laser", }

    local_lasers = sum(
        item.player == world.player and item.name in laser_names
        for item in corresponding_items
    )

    total_progression = non_local_progression + local_progression

    player_count = world.multiworld.players

    area_progression_word = "Both" if total_progression == 2 else "All"

    if not total_progression:
        hint_string = f"In the {hinted_area} area, you will find no progression items."

    elif total_progression == 1:
        hint_string = f"In the {hinted_area} area, you will find 1 progression item."

        if player_count > 1:
            if local_lasers:
                hint_string += "\nThis item is a laser for this world."
            elif non_local_progression:
                other_player_str = "the other player" if player_count == 2 else "another player"
                hint_string += f"\nThis item is for {other_player_str}."
            else:
                hint_string += "\nThis item is for this world."
        else:
            if local_lasers:
                hint_string += "\nThis item is a laser."

    else:
        hint_string = f"In the {hinted_area} area, you will find {total_progression} progression items."

        if local_lasers == total_progression:
            sentence_end = (" for this world." if player_count > 1 else ".")
            hint_string += f"\nAll of them are lasers" + sentence_end

        elif player_count > 1:
            if local_progression and non_local_progression:
                if non_local_progression == 1:
                    other_player_str = "the other player" if player_count == 2 else "another player"
                    hint_string += f"\nOne of them is for {other_player_str}."
                else:
                    other_player_str = "the other player" if player_count == 2 else "other players"
                    hint_string += f"\n{non_local_progression} of them are for {other_player_str}."
            elif non_local_progression:
                other_players_str = "the other player" if player_count == 2 else "other players"
                hint_string += f"\n{area_progression_word} of them are for {other_players_str}."
            elif local_progression:
                hint_string += f"\n{area_progression_word} of them are for this world."

            if local_lasers == 1:
                if not non_local_progression:
                    hint_string += "\nAlso, one of them is a laser."
                else:
                    hint_string += "\nAlso, one of them is a laser for this world."
            elif local_lasers:
                if not non_local_progression:
                    hint_string += f"\nAlso, {local_lasers} of them are lasers."
                else:
                    hint_string += f"\nAlso, {local_lasers} of them are lasers for this world."

        else:
            if local_lasers == 1:
                hint_string += "\nOne of them is a laser."
            elif local_lasers:
                hint_string += f"\n{local_lasers} of them are lasers."

    return hint_string, total_progression


def make_area_hints(world: "WitnessWorld", amount: int, already_hinted_locations: Set[Location]
                    ) -> Tuple[List[WitnessWordedHint], Dict[str, Set[Location]]]:
    locs_per_area, items_per_area = get_hintable_areas(world)

    hinted_areas, unhinted_locations_per_area = choose_areas(world, amount, locs_per_area, already_hinted_locations)

    hints = []

    for hinted_area in hinted_areas:
        hint_string, prog_amount = word_area_hint(world, hinted_area, items_per_area[hinted_area])

        hints.append(WitnessWordedHint(hint_string, None, f"hinted_area:{hinted_area}", prog_amount))

    if len(hinted_areas) < amount:
        player_name = world.multiworld.get_player_name(world.player)
        logging.warning(f"Was not able to make {amount} area hints for player {player_name}. "
                        f"Made {len(hinted_areas)} instead, and filled the rest with random location hints.")

    return hints, unhinted_locations_per_area


def create_all_hints(world: "WitnessWorld", hint_amount: int, area_hints: int,
                     already_hinted_locations: Set[Location]) -> List[WitnessWordedHint]:
    generated_hints: List[WitnessWordedHint] = []

    state = CollectionState(world.multiworld)

    # Keep track of already hinted locations. Consider early Tutorial as "already hinted"

    already_hinted_locations |= {
        loc for loc in world.multiworld.get_reachable_locations(state, world.player)
        if loc.address and StaticWitnessLogic.ENTITIES_BY_NAME[loc.name]["area"]["name"] == "Tutorial (Inside)"
    }

    intended_location_hints = hint_amount - area_hints

    # First, make always and priority hints.

    always_hints, priority_hints = make_always_and_priority_hints(
        world, world.own_itempool, already_hinted_locations
    )

    generated_always_hints = len(always_hints)
    possible_priority_hints = len(priority_hints)

    # Make as many always hints as possible
    always_hints_to_use = min(intended_location_hints, generated_always_hints)

    # Make up to half of the rest of the location hints priority hints, using up to half of the possibly priority hints
    remaining_location_hints = intended_location_hints - always_hints_to_use
    priority_hints_to_use = int(max(0.0, min(possible_priority_hints / 2, remaining_location_hints / 2)))

    for _ in range(always_hints_to_use):
        location_hint = always_hints.pop()
        generated_hints.append(word_direct_hint(world, location_hint))
        already_hinted_locations.add(location_hint.location)

    for _ in range(priority_hints_to_use):
        location_hint = priority_hints.pop()
        generated_hints.append(word_direct_hint(world, location_hint))
        already_hinted_locations.add(location_hint.location)

    location_hints_created_in_round_1 = len(generated_hints)

    unhinted_locations_per_area: Dict[str, Set[Location]] = dict()

    # Then, make area hints.
    if area_hints:
        generated_area_hints, unhinted_locations_per_area = make_area_hints(world, area_hints, already_hinted_locations)
        generated_hints += generated_area_hints

    # If we don't have enough hints yet, recalculate always and priority hints, then fill with random hints
    if len(generated_hints) < hint_amount:
        remaining_needed_location_hints = hint_amount - len(generated_hints)

        # Save old values for used always and priority hints for later calculations
        amt_of_used_always_hints = always_hints_to_use
        amt_of_used_priority_hints = priority_hints_to_use

        # Recalculate how many always hints and priority hints are supposed to be used
        intended_location_hints = remaining_needed_location_hints + location_hints_created_in_round_1

        always_hints_to_use = min(intended_location_hints, generated_always_hints)
        priority_hints_to_use = int(max(0.0, min(possible_priority_hints / 2, remaining_location_hints / 2)))

        # If we now need more always hints and priority hints than we thought previously, make some more.
        more_always_hints = always_hints_to_use - amt_of_used_always_hints
        more_priority_hints = priority_hints_to_use - amt_of_used_priority_hints

        extra_always_and_priority_hints: List[WitnessLocationHint] = []

        for _ in range(more_always_hints):
            extra_always_and_priority_hints.append(always_hints.pop())

        for _ in range(more_priority_hints):
            extra_always_and_priority_hints.append(priority_hints.pop())

        generated_hints += make_extra_location_hints(
            world, hint_amount - len(generated_hints), world.own_itempool, already_hinted_locations,
            extra_always_and_priority_hints, unhinted_locations_per_area
        )

    # If we still don't have enough for whatever reason, throw a warning, proceed with the lower amount
    if len(generated_hints) != hint_amount:
        player_name = world.multiworld.get_player_name(world.player)
        logging.warning(f"Couldn't generate {hint_amount} hints for player {player_name}. "
                        f"Generated {len(generated_hints)} instead.")

    return generated_hints


def make_compact_hint_data(hint: WitnessWordedHint, local_player_number: int) -> CompactItemData:
    location = hint.location
    area_amount = hint.area_amount

    # None if junk hint, address if location hint, area string if area hint
    arg_1 = location.address if location else (hint.area if hint.area else None)

    # self.player if junk hint, player if location hint, progression amount if area hint
    arg_2 = area_amount if area_amount is not None else (location.player if location else local_player_number)

    return hint.wording, arg_1, arg_2


def make_laser_hints(world: "WitnessWorld", laser_names: List[str]) -> Dict[str, WitnessWordedHint]:
    laser_hints_by_name = dict()

    for item_name in laser_names:
        location_hint = hint_from_item(world, item_name, world.own_itempool)
        if not location_hint:
            continue

        laser_hints_by_name[item_name] = word_direct_hint(world, location_hint)

    return laser_hints_by_name
