"""Feature classes handle the different features in the game."""

import datetime
import math
import re
import time

from collections import deque, namedtuple
from typing      import Dict, List, Tuple
from PIL.Image   import Image as PILImage

from deprecated import deprecated

import constants    as const
import coordinates  as coords
import usersettings as userset

from classes.inputs     import Inputs
from classes.navigation import Navigation
from classes.window     import Window


class FightBoss:
    @staticmethod
    def get_current_boss() -> int:
        """Go to fight and read current boss number."""
        Navigation.menu("fight")
        boss = Inputs.ocr(*coords.OCR_BOSS, debug=False)
        return Inputs.remove_letters(boss)

    @staticmethod
    def nuke(boss :int =None) -> None:
        """Navigate to Fight Boss and Nuke or Fast Fight.

        Keyword arguments
        boss -- If provided, will fight until reached
                If omitted, will hit nuke instead.
        """
        Navigation.menu("fight")
        if boss:
            for _ in range(boss):
                Inputs.click(*coords.FIGHT, fast=True)
            time.sleep(userset.SHORT_SLEEP)
            try:
                current_boss = int(FightBoss.get_current_boss())
            except ValueError:
                current_boss = 1
            x = 0
            while current_boss < boss:
                bossdiff = boss - current_boss
                for _ in range(0, bossdiff):
                    Inputs.click(*coords.FIGHT, fast=True)
                time.sleep(userset.SHORT_SLEEP)
                try:
                    current_boss = int(FightBoss.get_current_boss())
                except ValueError:
                    current_boss = 1
                x += 1
                if x > 7:  # Safeguard if number is too low to reach target boss, otherwise we get stuck here
                    print("Couldn't reach the target boss, something probably went wrong the last rebirth.")
                    break
        else:
            Inputs.click(*coords.NUKE)

    @staticmethod
    def fight() ->None:
        """Navigate to Fight Boss and click fight."""
        Navigation.menu("fight")
        Inputs.click(*coords.FIGHT)

class MoneyPit:
    @staticmethod
    def pit(loadout :int =0) -> None:
        """Throws money into the pit.

        Keyword arguments:
        loadout -- The loadout you wish to equip before throwing gold
                   into the pit, for gear you wish to shock. Make
                   sure that you don't get cap-blocked by either using
                   the unassign setting in the game or swapping gear that
                   doesn't have e/m cap.
        """
        if Inputs.check_pixel_color(*coords.IS_PIT_READY):
            if loadout:
                Inventory.loadout(loadout)
            Navigation.menu("pit")
            Inputs.click(*coords.PIT)
            Inputs.click(*coords.CONFIRM)

    @staticmethod
    def spin() -> None:
        """Spin the wheel."""
        if Inputs.check_pixel_color(*coords.IS_SPIN_READY):
           Navigation.menu("pit")
           Inputs.click(*coords.SPIN_MENU)
           Inputs.click(*coords.SPIN)

class Adventure:
    current_adventure_zone = 0
    itopod_tier_counts = {}
    itopod_tier_map = {
        1: 0,
        2: 50,
        3: 100,
        4: 150,
        5: 200,
        6: 250,
        7: 300,
        8: 350,
        9: 400,
        10: 450,
        11: 500,
        12: 550,
        13: 600,
        14: 650,
        15: 700,
        16: 750,
        17: 800,
        18: 850,
        19: 900,
        20: 950,
    }
    itopod_ap_gained = 0
    itopod_kills = 0

    mega_buff_unlocked = False
    oh_shit_unlocked = False

    @staticmethod
    def adventure(zone=-1, highest :bool =False, itopod :int =None, itopodauto :bool =False) -> None:
        """Go to an adventure zone to idle.
 
        Keyword arguments
        zone -- Zone to idle in, 0 is safe zone, 1 is tutorial and so on.
        highest -- If true, will go to your highest available non-titan zone.
        itopod -- If set to true, it will override other settings and will
                  instead enter the specified ITOPOD floor.
        itopodauto -- If set to true it will click the "optimal" floor button.
        """
        Navigation.menu("adventure")
        Misc.waste_click()
        if not Inputs.check_pixel_color(*coords.IS_IDLE):
            Inputs.click(*coords.ABILITY_IDLE_MODE)
        if itopod or itopodauto:
            Adventure.current_adventure_zone = 0
            Inputs.click(*coords.ITOPOD)
            if itopodauto:
                Inputs.click(*coords.ITOPOD_END)
                # set end to 0 in case it's higher than start
                Inputs.send_string("0")
                Inputs.click(*coords.ITOPOD_AUTO)
                Inputs.click(*coords.ITOPOD_ENTER)
                return
            Inputs.click(*coords.ITOPOD_START)
            Inputs.send_string(str(itopod))
            Inputs.click(*coords.ITOPOD_END)
            Inputs.send_string(str(itopod))
            Inputs.click(*coords.ITOPOD_ENTER)
            return
        if zone == -1 or highest:
            Adventure.current_adventure_zone = 0
            Inputs.click(*coords.RIGHT_ARROW, button="right")
            return
        else:
            Adventure.current_adventure_zone = zone
            Inputs.click(*coords.LEFT_ARROW, button="right")
            for _ in range(zone):
                Inputs.click(*coords.RIGHT_ARROW, fast=True)
            return

    @staticmethod
    def snipe(
        zone :int,
        duration :int,
        once :bool =False,
        highest :bool =False,
        bosses :bool =False,
        manual :bool =False,
        fast :bool =False) -> None:
        """Go to adventure and snipe bosses in specified zone.

        Keyword arguments
        zone -- Zone to snipe, 0 is safe zone, 1 is turorial and so on.
                If 0, it will use the current zone (to maintain guffin counter)
        duration -- The duration in minutes the sniping will run before
                    returning.
        once -- If true it will only kill one boss before returning.
        highest -- If set to true, it will go to your highest available
                   non-titan zone.
        bosses -- If set to true, it will only kill bosses
        manual -- If set to true it will use all available abilities to kill the enemy.
                  In addition it will return after killing one enemy.
        fast   -- If your respawn is lower than your attack speed, use
                  this to remove the overhead from check_pixel_color().
                  It should give you higher xp/h. Remember that move CD
                  is capped at 0.8s, so there's no reason to go lower.
        """
        Navigation.menu("adventure")
        if highest:
            Inputs.click(*coords.LEFT_ARROW, button="right")
            Inputs.click(*coords.RIGHT_ARROW, button="right")
        elif zone > 0 and zone != Adventure.current_adventure_zone:
            Inputs.click(*coords.LEFT_ARROW, button="right")
            for _ in range(zone):
                Inputs.click(*coords.RIGHT_ARROW, fast=True)
        Adventure.current_adventure_zone = zone
        Inputs.click(625, 500)  # click somewhere to move tooltip
        
        if Inputs.check_pixel_color(*coords.IS_IDLE):
            Inputs.click(*coords.ABILITY_IDLE_MODE)
        
        end = time.time() + duration * 60
        while time.time() < end:
            if fast:
                Inputs.click(*coords.ABILITY_REGULAR_ATTACK, fast=True)
                continue

            Inputs.click(625, 500)  # click somewhere to move tooltip
            if not Inputs.check_pixel_color(*coords.IS_DEAD):
                if bosses:
                    if Inputs.check_pixel_color(*coords.IS_BOSS_CROWN):
                        enemy_alive = True
                        if manual:
                            Adventure.kill_enemy()
                        else:
                            while enemy_alive:
                                enemy_alive = not Inputs.check_pixel_color(*coords.IS_DEAD)
                                if Inputs.check_pixel_color(*coords.COLOR_REGULAR_ATTACK_READY):
                                    Inputs.click(*coords.ABILITY_REGULAR_ATTACK)
                                time.sleep(0.1)
                        if once:
                            break
                    else:
                        # Send left arrow and right arrow to refresh monster.
                        Inputs.send_arrow_press(left=True)
                        Inputs.send_arrow_press(left=False)
                else:
                    if manual:
                        Adventure.kill_enemy()
                    else:
                        Inputs.click(*coords.ABILITY_REGULAR_ATTACK)
            time.sleep(0.01)
        
        Inputs.click(*coords.ABILITY_IDLE_MODE)
    
    @staticmethod
    def itopod_snipe(duration :int, auto :bool =False, fast :bool =False) -> None:
        """Manually snipes ITOPOD for increased speed PP/h.
        
        Keyword arguments:
        duration -- Duration in seconds to snipe, before toggling idle mode
                    back on and returning.
        auto     -- Make sure you're on the optimal floor even if you're
                    already in the ITOPOD
        fast     -- If your respawn is lower than your attack speed, use
                    this to remove the overhead from check_pixel_color().
                    It should give you higher xp/h. Remember that move CD
                    is capped at 0.8s, so there's no reason to go lower.
        """
        end = time.time() + duration
        Adventure.current_adventure_zone = 0
        Navigation.menu("adventure")
        Inputs.click(625, 500)  # click somewhere to move tooltip
        
        # check if we're already in ITOPOD, otherwise enter
        # if auto is true, re-enter ITOPOD to make sure floor is optimal
        if auto or not Inputs.check_pixel_color(*coords.IS_ITOPOD_ACTIVE):
            Inputs.click(*coords.ITOPOD)
            Inputs.click(*coords.ITOPOD_END)
            # set end to 0 in case it's higher than start
            Inputs.send_string("0")
            Inputs.click(*coords.ITOPOD_AUTO)
            Inputs.click(*coords.ITOPOD_ENTER)
        
        if Inputs.check_pixel_color(*coords.IS_IDLE):
            Inputs.click(*coords.ABILITY_IDLE_MODE)
        
        while time.time() < end:
            if fast:
                Inputs.click(*coords.ABILITY_REGULAR_ATTACK, fast=True)
                continue
            if (Inputs.check_pixel_color(*coords.IS_ENEMY_ALIVE) and
               Inputs.check_pixel_color(*coords.COLOR_REGULAR_ATTACK_READY)):
                Inputs.click(*coords.ABILITY_REGULAR_ATTACK)
            else:
                time.sleep(0.01)
        
        Inputs.click(*coords.ABILITY_IDLE_MODE)
    
    @staticmethod
    def kill_enemy() -> None:
        """Attempt to kill enemy in adventure using abilities."""
        start = time.time()
        if Inputs.check_pixel_color(*coords.IS_IDLE):
            Inputs.click(*coords.ABILITY_IDLE_MODE)
        while Inputs.check_pixel_color(*coords.IS_DEAD):
            time.sleep(.1)
            if time.time() > start + 5:
                print("Couldn't detect enemy in kill_enemy()")
                return
        queue = deque(Adventure.get_ability_queue())
        while not Inputs.check_pixel_color(*coords.IS_DEAD):
            if not queue:
                queue = deque(Adventure.get_ability_queue())
            ability = queue.popleft()
            if ability <= 4:
                x = coords.ABILITY_ROW1X + ability * coords.ABILITY_OFFSETX
                y = coords.ABILITY_ROW1Y
            
            if ability >= 5 and ability <= 10:
                x = coords.ABILITY_ROW2X + (ability - 5) * coords.ABILITY_OFFSETX
                y = coords.ABILITY_ROW2Y
            
            if ability > 10:
                x = coords.ABILITY_ROW3X + (ability - 11) * coords.ABILITY_OFFSETX
                y = coords.ABILITY_ROW3Y
            
            Inputs.click(x, y)
            time.sleep(userset.LONG_SLEEP)
            color = Inputs.get_pixel_color(coords.ABILITY_ROW1X,
                                           coords.ABILITY_ROW1Y)
            
            while color != coords.ABILITY_ROW1_READY_COLOR:
                time.sleep(0.03)
                color = Inputs.get_pixel_color(coords.ABILITY_ROW1X,
                                               coords.ABILITY_ROW1Y)
    
    @staticmethod
    def check_titan_status() -> List[int]:
        """Check to see if any titans are ready."""
        Inputs.click(*coords.MENU_ITEMS["adventure"], button="right")
        text = Inputs.ocr(*coords.OCR_TITAN_RESPAWN).lower()
        ready = []
        i = 1
        for line in text.split('\n'):
            if line == '' or line == '\n':
                continue
            if "ready" in line:
                ready.append(i)
            if "spawn" in line:
                i += 1
        return ready
    
    @staticmethod
    def kill_titan(target :int, mega :bool =True) -> None:
        """Attempt to kill the target titan.
        
        Keyword arguments:
        target -- The id of the titan you wish to kill. 1 for GRB, 2 for GCT and so on.
        mega   -- Use Mega Buff
        """
        Navigation.menu("adventure")
        Inputs.click(*coords.WASTE_CLICK) # close any tooltips
        
        if Inputs.check_pixel_color(*coords.IS_IDLE):
            Inputs.click(*coords.ABILITY_IDLE_MODE)
        
        Inputs.click(*coords.LEFT_ARROW, button="right")
        charge = False
        parry = False
        if mega:
            while not Inputs.check_pixel_color(*coords.COLOR_MEGA_BUFF_READY) or not charge or not parry:
                queue = Adventure.get_ability_queue()
                Inputs.click(625, 600)
                if 2 in queue and not parry:
                    x = coords.ABILITY_ROW1X + 2 * coords.ABILITY_OFFSETX
                    y = coords.ABILITY_ROW1Y
                    Inputs.click(x, y)
                    parry = True
                    time.sleep(1)  # wait for global cooldown
                if 9 in queue and not charge:
                    x = coords.ABILITY_ROW2X + (9 - 5) * coords.ABILITY_OFFSETX
                    y = coords.ABILITY_ROW2Y
                    Inputs.click(x, y)
                    charge = True
                    time.sleep(1)  # wait for global cooldown
                time.sleep(userset.MEDIUM_SLEEP)
        
        else:
            while not Inputs.check_pixel_color(*coords.COLOR_ULTIMATE_BUFF_READY) or not charge or not parry:
                queue = Adventure.get_ability_queue()
                Inputs.click(625, 600)
                if 2 in queue and not parry:
                    x = coords.ABILITY_ROW1X + 2 * coords.ABILITY_OFFSETX
                    y = coords.ABILITY_ROW1Y
                    Inputs.click(x, y)
                    parry = True
                    time.sleep(1)  # wait for global cooldown
                if 9 in queue and not charge:
                    x = coords.ABILITY_ROW2X + 4 * coords.ABILITY_OFFSETX
                    y = coords.ABILITY_ROW2Y
                    Inputs.click(x, y)
                    charge = True
                    time.sleep(1)  # wait for global cooldown
                time.sleep(userset.MEDIUM_SLEEP)
        
        buffs = [2, 9]
        print("Waiting for charge and parry to be ready")
        while not all(x in Adventure.get_ability_queue() for x in buffs):
            time.sleep(.5)
        
        for _ in range(const.TITAN_ZONE[target - 1]):
            Inputs.click(*coords.RIGHT_ARROW, fast=True)
        Adventure.current_adventure_zone = const.TITAN_ZONE[target - 1]
        time.sleep(userset.LONG_SLEEP)
        start = time.time()
        while Inputs.check_pixel_color(*coords.IS_DEAD):  # wait for titan to spawn
            time.sleep(0.05)
            if time.time() > start + 5:
                print("Couldn't detect enemy in kill_titan()")
                return
        
        queue = deque(Adventure.get_ability_queue())
        while not Inputs.check_pixel_color(*coords.IS_DEAD):
            if not queue:
                queue = deque(Adventure.get_ability_queue())
            
            ability = queue.popleft()
            if ability <= 4:
                x = coords.ABILITY_ROW1X + ability * coords.ABILITY_OFFSETX
                y = coords.ABILITY_ROW1Y
            
            if ability >= 5 and ability <= 10:
                x = coords.ABILITY_ROW2X + (ability - 5) * coords.ABILITY_OFFSETX
                y = coords.ABILITY_ROW2Y
            
            if ability > 10:
                x = coords.ABILITY_ROW3X + (ability - 11) * coords.ABILITY_OFFSETX
                y = coords.ABILITY_ROW3Y
            
            Inputs.click(x, y)
            time.sleep(userset.LONG_SLEEP)
            color = Inputs.get_pixel_color(coords.ABILITY_ROW1X,
                                           coords.ABILITY_ROW1Y)
            
            while color != coords.ABILITY_ROW1_READY_COLOR:
                time.sleep(0.05)
                color = Inputs.get_pixel_color(coords.ABILITY_ROW1X,
                                               coords.ABILITY_ROW1Y)
    
    @staticmethod
    def get_ability_queue() -> List[int]:
        """Return a queue of usable abilities."""
        ready = []
        queue = []
        
        # Add all abilities that are ready to the ready array
        for i in range(1, 16):
            if i <= 4:
                x = coords.ABILITY_ROW1X + i * coords.ABILITY_OFFSETX
                y = coords.ABILITY_ROW1Y
                color = Inputs.get_pixel_color(x, y)
                if color == coords.ABILITY_ROW1_READY_COLOR:
                    ready.append(i)
            if i >= 5 and i <= 10:
                if Adventure.mega_buff_unlocked and i == 6:
                    continue
                x = coords.ABILITY_ROW2X + (i - 5) * coords.ABILITY_OFFSETX
                y = coords.ABILITY_ROW2Y
                color = Inputs.get_pixel_color(x, y)
                if color == coords.ABILITY_ROW2_READY_COLOR:
                    ready.append(i)
            if i > 10:
                x = coords.ABILITY_ROW3X + (i - 11) * coords.ABILITY_OFFSETX
                y = coords.ABILITY_ROW3Y
                color = Inputs.get_pixel_color(x, y)
                if color == coords.ABILITY_ROW3_READY_COLOR:
                    ready.append(i)
        
        if 15 in ready:
            Adventure.oh_shit_unlocked = True
        if 14 in ready:
            Adventure.mega_buff_unlocked = True
        # heal if we need to heal
        if Inputs.check_pixel_color(*coords.PLAYER_HEAL_THRESHOLD):
            if 15 in ready:
                queue.append(15)
            elif 12 in ready:
                queue.append(12)
            elif 7 in ready:
                queue.append(7)
        
        # check if offensive buff and ultimate buff are both ready
        buffs = [8, 10]
        if 14 in ready:
            queue.append(14)
        elif all(i in ready for i in buffs) and not Adventure.mega_buff_unlocked:
            queue.extend(buffs)
        
        d = coords.ABILITY_PRIORITY
        # Sort the abilities by the set priority
        abilities = sorted(d, key=d.get, reverse=True)
        # Only add the abilities that are ready to the queue
        queue.extend([a for a in abilities if a in ready])
        
        # If nothing is ready, return a regular attack
        if not queue:
            queue.append(0)
        return queue
    
    @staticmethod
    def itopod_ap(duration :int) -> None:
        """Abuse an oversight in the kill counter for AP rewards for mucher higher AP/h in ITOPOD.
        If you use this method, make sure you do not retoggle idle mode in adventure in other parts
        of your script. If you have to, make sure to empty itopod_tier_counts with:
        itopod_tier_counts = {}
        
        Keyword arguments:
        duration -- Duration in seconds to run, before toggling idle mode
                    back on and returning.
        """
        print("WARNING: itopod_ap() is largely untested")
        end = time.time() + duration * 60
        Adventure.current_adventure_zone = 0
        Navigation.menu("adventure")
        Inputs.click(625, 500)  # click somewhere to move tooltip
        if Inputs.check_pixel_color(*coords.IS_IDLE):
            Inputs.click(*coords.ABILITY_IDLE_MODE)
        # check if we're already in ITOPOD, otherwise enter
        if not Adventure.itopod_tier_counts:
            for tier, floor in Adventure.itopod_tier_map.items():
                Inputs.click(*coords.ITOPOD)
                Inputs.click(*coords.ITOPOD_START)
                Inputs.send_string(floor)
                # set end to 0 in case it's higher than start
                Inputs.click(*coords.ITOPOD_ENTER)
                Inputs.click(*coords.ADVENTURE_TOOLTIP)
                count = Inputs.remove_letters(Inputs.ocr(*coords.OCR_AP_KILL_COUNT))
                print(f"Tier {tier}: {count}")
                try:
                    count = int(count)
                except ValueError:
                    print(f"couldn't convert '{count}' to int")
                Adventure.itopod_tier_counts[tier] = count
        print(Adventure.itopod_tier_counts)
        while time.time() < end:
            next_tier = min(Adventure.itopod_tier_counts, key=Adventure.itopod_tier_counts.get)
            print(f"going to itopod tier {next_tier}")
            Inputs.click(*coords.ITOPOD)
            Inputs.click(*coords.ITOPOD_START)
            Inputs.send_string(Adventure.itopod_tier_map[next_tier])
            # set end to 0 in case it's higher than start
            Inputs.click(*coords.ITOPOD_ENTER)
            time.sleep(userset.LONG_SLEEP)
            kc = Adventure.itopod_tier_counts[next_tier]
            while kc > 0:
                if Inputs.check_pixel_color(*coords.IS_ENEMY_ALIVE):
                    Inputs.click(*coords.ABILITY_REGULAR_ATTACK)
                    
                    Adventure.itopod_kills += 1
                    kc -= 1
                    if kc > 0:
                        time.sleep(.7 - userset.MEDIUM_SLEEP)  # Make sure we wait long enough
                    for tier, count in Adventure.itopod_tier_counts.items():
                        Adventure.itopod_tier_counts[tier] -= 1
                        if Adventure.itopod_tier_counts[tier] < 1:
                            Adventure.itopod_tier_counts[tier] = 40 - tier
                else:
                    time.sleep(0.06)
            Adventure.itopod_ap_gained += 1
            print(f"Kills: {Adventure.itopod_kills}\nAP gained: {Adventure.itopod_ap_gained}")
        return

class Inventory:
    @staticmethod
    def merge_equipment() -> None:
        """Navigate to inventory and merge equipment."""
        Navigation.menu("inventory")
        for slot in coords.EQUIPMENT_SLOTS:
            if slot == "cube":
                return
            Inputs.click(*coords.EQUIPMENT_SLOTS[slot])
            Inputs.send_string("d")
    
    @staticmethod
    def boost_equipment(boost_cube :bool =True) -> None:
        """Boost all equipment.
        
        Keyword arguments
        boost_cube -- If True (default), will boost cube after all equipment
                      has been boosted.
        """
        Navigation.menu("inventory")
        for slot in coords.EQUIPMENT_SLOTS:
            if slot == "cube":
                if boost_cube:
                    Inventory.boost_cube()
                return
            Inputs.click(*coords.EQUIPMENT_SLOTS[slot])
            Inputs.send_string("a")
    
    @staticmethod
    def boost_cube() -> None:
        """Boost cube."""
        Navigation.menu("inventory")
        Inputs.click(*coords.EQUIPMENT_SLOTS["cube"], "right")
    
    @staticmethod
    def loadout(target :int) -> None:
        """Equip a loadout.
        
        Keyword arguments
        target -- The loadout to be equiped
        """
        Navigation.menu("inventory")
        Inputs.click(*coords.LOADOUT[target])
    
    @staticmethod
    def get_inventory_slots(slots :int) -> None:
        """Get coords for inventory slots from 1 to slots."""
        point = namedtuple("p", ("x", "y"))
        i = 1
        row = 1
        x_pos, y_pos = coords.INVENTORY_SLOTS
        res = []
        
        while i <= slots:
            x = x_pos + (i - (12 * (row - 1))) * 50
            y = y_pos + ((row - 1) * 50)
            res.append(point(x, y))
            if i % 12 == 0:
                row += 1
            i += 1
        return res
    
    @staticmethod
    def merge_inventory(slots :int) -> None:
        """Merge all inventory slots starting from 1 to slots.
        
        Keyword arguments:
        slots -- The amount of slots you wish to merge
        """
        Navigation.menu("inventory")
        coord = Inventory.get_inventory_slots(slots)
        for slot in coord:
            Inputs.click(*slot)
            Inputs.send_string("d")
    
    @staticmethod
    def boost_inventory(slots :int) -> None:
        """Merge all inventory slots starting from 1 to slots.
        
        Keyword arguments:
        slots -- The amount of slots you wish to merge
        """
        Navigation.menu("inventory")
        coord = Inventory.get_inventory_slots(slots)
        for slot in coord:
            Inputs.click(*slot)
            Inputs.send_string("a")
    
    @staticmethod
    def transform_slot(slot :int, threshold :float =0.8, consume :bool =False) -> None:
        """Check if slot is transformable and transform if it is.
        Be careful using this, make sure the item you want to transform is
        not protected, and that all other items are protected, this might
        delete items otherwise. Another note, consuming items will show
        a special tooltip that will block you from doing another check
        for a few seconds, keep this in mind if you're checking multiple
        slots in succession.
        
        Keyword arguments:
        slot      -- The slot you wish to transform, if possible
        threshold -- The fuzziness in the image search, I recommend a value
                     between 0.7 - 0.95.
        consume   -- Set to true if item is consumable instead.
        """
        Navigation.menu("inventory")
        slot = Inventory.get_inventory_slots(slot)[-1]
        Inputs.click(*slot)
        time.sleep(userset.SHORT_SLEEP)
        
        if consume:
            coord = Inputs.image_search(Window.x, Window.y, Window.x + 960, Window.y + 600,
                                        Inputs.get_file_path("images", "consumable.png"), threshold)
        else:
            coord = Inputs.image_search(Window.x, Window.y, Window.x + 960, Window.y + 600,
                                        Inputs.get_file_path("images", "transformable.png"), threshold)
        
        if coord:
            Inputs.ctrl_click(*slot)

class Augmentation:
    @staticmethod
    def augments(augments :Dict[str, float], energy :int) -> None:
        """Dump energy into augmentations.
        
        Keyword arguments
        augments -- Dictionary that contains which augments you wish to use and
                    a ratio that tells how much of the total energy you
                    allocated you wish to send. Example:
                    {"SS": 0, "DS": 0, "MI": 0, "DTMT": 0, "CI": 0, "M": 0,
                     "SM": 0, "AA": 0, "EB": 0, "CS": 0, "AE": 0, "ES": 0,
                     "LS": 0.9, "QSL": 0.1}
        Energy -- The total amount of energy you want to use for all augments.
        """
        Navigation.menu("augmentations")
        for k in augments:
            val = math.floor(augments[k] * energy)
            Misc.set_input(val)
            # Scroll down if we have to.
            bottom_augments = ["AE", "ES", "LS", "QSL"]
            i = 0
            if k in bottom_augments:
                color = Inputs.get_pixel_color(*coords.AUG_SCROLL_SANITY_BOT)
                while color not in coords.SANITY_AUG_SCROLL_COLORS:
                    Inputs.click(*coords.AUG_SCROLL_BOT)
                    time.sleep(userset.MEDIUM_SLEEP)
                    color = Inputs.get_pixel_color(*coords.AUG_SCROLL_SANITY_BOT)
                    i += 1
                    if i > 5 and i <= 10:  # Safeguard if something goes wrong with augs
                        Navigation.current_menu = ""
                        Navigation.menu("augmentations")
                    elif i > 10:
                        print("Couldn't assign augments")
                        break
            
            else:
                color = Inputs.get_pixel_color(*coords.AUG_SCROLL_SANITY_TOP)
                while color not in coords.SANITY_AUG_SCROLL_COLORS:
                    Inputs.click(*coords.AUG_SCROLL_TOP)
                    time.sleep(userset.MEDIUM_SLEEP)
                    color = Inputs.get_pixel_color(*coords.AUG_SCROLL_SANITY_TOP)
                    i += 1
                    if i > 5 and i <= 10:  # Safeguard if something goes wrong with augs
                        Navigation.current_menu = ""
                        Navigation.menu("augmentations")
                    elif i > 10:
                        print("Couldn't assign augments")
                        break
            Inputs.click(*coords.AUGMENT[k])

class AdvancedTraining:
    @staticmethod
    def advanced_training(energy :int, ability :int =0) -> None:
        """Assign energy to adanced training.
        
        Keyword arguments
        energy  -- Set the total energy to assign to AT.
        ability -- The AT ability to be trained.
                   If this is zero, it'll split the energy evenly between
                   Adv Toughness, Adv Power, Wandoos Energy and Wandoos Magic.
                   Splitting energy is the default behavior.
        """
        Navigation.menu("advtraining")
        if ability == 0:
            energy = energy // 4
            Misc.set_input(energy)
            Inputs.click(*coords.ADV_TRAINING_POWER)
            Inputs.click(*coords.ADV_TRAINING_TOUGHNESS)
            Inputs.click(*coords.ADV_TRAINING_WANDOOS_ENERGY)
            Inputs.click(*coords.ADV_TRAINING_WANDOOS_MAGIC)
        
        else:
            Misc.set_input(energy)
            if ability == 1:
                Inputs.click(*coords.ADV_TRAINING_TOUGHNESS)
            if ability == 2:
                Inputs.click(*coords.ADV_TRAINING_POWER)
            if ability == 3:
                Inputs.click(*coords.ADV_TRAINING_BLOCK)
            if ability == 4:
                Inputs.click(*coords.ADV_TRAINING_WANDOOS_ENERGY)
            if ability == 5:
                Inputs.click(*coords.ADV_TRAINING_WANDOOS_MAGIC)

class TimeMachine:
    @staticmethod
    def time_machine(e :int, m :int =0, magic :bool =False) -> None:
        """Add energy and/or magic to TM.
        
        Example: TimeMachine.time_machine(1000, 2000)
                 TimeMachine.time_machine(1000, magic=True)
                 TimeMachine.time_machine(1000)
        
        First example will add 1000 energy and 2000 magic to TM.
        Second example will add 1000 energy and 1000 magic to TM.
        Third example will add 1000 energy to TM.
        
        Keyword arguments:
        e -- The amount of energy to put into TM.
        m -- The amount of magic to put into TM, if this is 0, it will use the
             energy value to save unnecessary clicks to the input box.
        magic -- Set to true if you wish to add magic as well
        """
        Navigation.menu("timemachine")
        Misc.set_input(e)
        Inputs.click(*coords.TM_SPEED)
        if magic or m:
            if m:
                Misc.set_input(m)
            Inputs.click(*coords.TM_MULT)

class BloodMagic:
    @staticmethod
    def blood_magic(target :int) -> None:
        """Assign magic to BM.
        
        Keyword arguments
        target -- Will cap all rituals till the target ritual
                  Usually run as blood_magic(8)
        """
        Navigation.menu("bloodmagic")
        for i in range(target):
            Inputs.click(*coords.BM[i])
    
    @staticmethod
    def activate_all_bm() -> None:
        """Click activate all in BM menu."""
        Navigation.menu("bloodmagic")
        Inputs.click(*coords.BM_CAP_ALL)

    @staticmethod
    @deprecated(version='0.1', reason="speedrun_bloodpill is deprecated, use iron_pill() instead")
    def speedrun_bloodpill():
        """Deprecated"""
        return
    
    @staticmethod
    @deprecated(version='0.1', reason="iron_pill is deprecated, use cast_spell() instead")
    def iron_pill():
        """Deprecated"""
        return
    
    @staticmethod
    def toggle_auto_spells(number :bool =True, drop :bool =True, gold :bool =True) -> None:
        """Enable/Disable autospells
        
        Keyword arguments
        number, drop, gold -- Spells to be enabled or disabled.
                              If True, enable the spell. If False, disable the spell.
                              If None, ignore the spell.
        """
        Navigation.spells()
        Inputs.click(600, 600)  # move tooltip
        
        if number is not None:
            number_active = Inputs.check_pixel_color(*coords.COLOR_BM_AUTO_NUMBER)
            if (number and not number_active) or (not number and number_active):
                Inputs.click(*coords.BM_AUTO_NUMBER)
        if drop is not None:
            drop_active = Inputs.check_pixel_color(*coords.COLOR_BM_AUTO_DROP)
            if (drop and not drop_active) or (not drop and drop_active):
                Inputs.click(*coords.BM_AUTO_DROP)
        
        if gold is not None:
            gold_active = Inputs.check_pixel_color(*coords.COLOR_BM_AUTO_GOLD)
            if (gold and not gold_active) or (not gold and gold_active):
                Inputs.click(*coords.BM_AUTO_GOLD)
    
    @staticmethod
    def check_spells_ready() -> List[int]:
        """Check which spells are ready to cast.
        
        Returns a list with integers corresponding to which spell is ready. The values on the
        list can be:
            1 - Iron pill
            2 - MacGuffin alpha
            3 - MacGuffin beta
        """
        if Inputs.check_pixel_color(*coords.COLOR_SPELL_READY):
            Navigation.spells()
            Inputs.click(*coords.BM_PILL, button="right")
            spells = []
            res = Inputs.ocr(*coords.OCR_BM_SPELL_TEXT)
            if "cooldown: 0.0s" in res.lower():
                spells.append(1)
            
            Inputs.click(*coords.BM_GUFFIN_A, button="right")
            res = Inputs.ocr(*coords.OCR_BM_SPELL_TEXT)
            if "cooldown: 0.0s" in res.lower():
                spells.append(2)
            
            Inputs.click(*coords.BM_GUFFIN_B, button="right")
            res = Inputs.ocr(*coords.OCR_BM_SPELL_TEXT)
            if "cooldown: 0.0s" in res.lower():
                spells.append(3)
            
            return spells
        else:
            return []
    
    @staticmethod
    def cast_spell(target :int) -> None:
        """Cast target spell.
        
        This method will allocate any idle magic into BM and wait for the
        time set in usersettings.py. Remember to re-enable auto spells after
        calling this method, using toggle_auto_spells().
        
        Keyword arguments
        number -- The spell to be cast. Possible values are:
            1 - Iron pill
            2 - MacGuffin alpha
            3 - MacGuffin beta
        """
        if Inputs.check_pixel_color(*coords.COLOR_SPELL_READY):
            targets = [0, coords.BM_PILL, coords.BM_GUFFIN_A, coords.BM_GUFFIN_B]
            start = time.time()
            BloodMagic.blood_magic(8)
            BloodMagic.toggle_auto_spells(False, False, False)  # disable all auto spells
            
            if userset.SPELL == 0:  # Default to 5 mins if not set
                duration = 300
            else:
                duration = userset.SPELL
            
            while time.time() < start + duration:
                print(f"Sniping itopod for {duration} seconds while waiting to cast spell.")
                Adventure.itopod_snipe(duration)
            Navigation.spells()
            Inputs.click(*targets[target])

class Wandoos:
    @staticmethod
    def wandoos(energy :bool =True, magic :bool =False) -> None:
        """Assign energy and/or magic to wandoos.
        
        Keyword arguments
        energy -- Assign energy to Wandoos (default: True)
        magic  -- Assign magic to Wandoos  (default: False)
        """
        Navigation.menu("wandoos")
        if energy:
            Inputs.click(*coords.WANDOOS_ENERGY)
        if magic:
            Inputs.click(*coords.WANDOOS_MAGIC)
    
    @staticmethod
    def set_wandoos(version :int) -> None:
        """Set wandoos version.
        
        Keyword arguments:
        version -- Wandoos version of choice. Possible values are:
                   0 : Wandoos 98
                   1 : Wandoos Meh
                   2 : Wandoos XL
        """
        Navigation.menu("wandoos")
        Inputs.click(*coords.WANDOOS_VERSION[version])
        Navigation.confirm()
    
    @staticmethod
    def check_wandoos_bb_status(magic :bool =False) -> None:
        """Check if wandoos is currently fully BB'd.
        
        Keyword arguments
        magic -- If True, check if Wandoos magic is BB'd
                 If False (default), check if Wandoos energy is BB'd
        """
        Navigation.menu("wandoos")
        if magic:
            return Inputs.check_pixel_color(*coords.COLOR_WANDOOS_MAGIC_BB)
        return Inputs.check_pixel_color(*coords.COLOR_WANDOOS_ENERGY_BB)

class NGU:
    @staticmethod
    def assign_ngu(value :int, targets :List[int], magic :bool =False) -> None:
        """Assign energy/magic to NGU's.
        
        Keyword arguments:
        value -- the amount of energy/magic that will get split over all NGUs.
        targets -- Array of NGU's to use (1-9).
        magic -- Set to true if these are magic NGUs
        """
        if len(targets) > 9:
            raise RuntimeError("Passing too many NGU's to assign_ngu," +
                               " allowed: 9, sent: " + str(len(targets)))
        if magic: Navigation.ngu_magic()
        else: Navigation.menu("ngu")
        
        Misc.set_input(value // len(targets))
        for i in targets:
            NGU = coords.Pixel(coords.NGU_PLUS.x, coords.NGU_PLUS.y + i * 35)
            Inputs.click(*NGU)
    
    @staticmethod
    @deprecated(version='0.1', reason="bb_ngu() is deprecated since .415 use cap_ngu() instead")
    def bb_ngu(value, targets, overcap=1, magic=False):
        """Estimates the BB value of each supplied NGU.
        It will send value into the target NGU's, which will fill the progress bar. It's very
        important that you put enough e/m into the NGU's to trigger the "anti-flicker"
        (>10% of BB cost), otherwise it will not function properly.
        
        Keyword arguments:
        value -- The amount of energy used to determine the cost of BBing the target NGU's
        targets -- Array of NGU's to BB. Example: [1, 3, 4, 5, 6]
        overcap -- Use this if you wish to assign more e/m than absolute minimum to BB
                   the NGU's. This might be useful for longer runs to make sure the cost
                   to BB doesn't exceed the assigned e/m. A value of 1.1 assigns 10% extra.
        magic -- Set to true if these are magic NGUs
        """
        if magic:
            Navigation.ngu_magic()
        else:
            Navigation.menu("ngu")
        
        Misc.set_input(value)
        
        for target in targets:
            NGU = coords.Pixel(coords.NGU_PLUS.x, coords.NGU_PLUS.y + target * 35)
            Inputs.click(*NGU)
        
        for target in targets:
            energy = 0
            for x in range(198):
                color = Inputs.get_pixel_color(coords.NGU_BAR_MIN.x + x,
                                               coords.NGU_BAR_MIN.y +
                                               coords.NGU_BAR_OFFSET_Y * target,
                                               )
                if color == coords.NGU_BAR_WHITE:
                    pixel_coefficient = x / 198
                    value_coefficient = overcap / pixel_coefficient
                    energy = (value_coefficient * value) - value
                    break
            if energy == 0:
                if magic:
                    print(f"Warning: You might be overcapping magic NGU #{target}")
                else:
                    print(f"Warning: You might be overcapping energy NGU #{target}")
                continue
      
            Misc.set_input(int(energy))
            Inputs.click(coords.NGU_PLUS.x, coords.NGU_PLUS.y + target * 35)

    @staticmethod
    def cap_ngu(targets :List[int] =None, magic :bool =False, cap_all :bool =True) -> None:
        """Cap NGUs.

        Keyword arguments
        targets -- The NGU's you wish to cap
        magic -- Set to true if these are magic NGU's
        cap_all -- Set to true if you wish to cap all NGU's
        """
        targets = targets or []
        if magic:
            Navigation.ngu_magic()
        else:
            Navigation.menu("ngu")
        
        for target in targets:
            NGU = coords.Pixel(coords.NGU_CAP.x, coords.NGU_CAP.y + target * 35)
            Inputs.click(*NGU)
        
        if cap_all and not targets:
            Inputs.click(*coords.NGU_CAP_ALL)
    
    @staticmethod
    def set_ngu_overcap(value :int) -> None:
        """Set the amount you wish to overcap your NGU's."""
        Navigation.menu("ngu")
        Inputs.click(*coords.NGU_OVERCAP)
        Inputs.send_string(value)

class Yggdrasil:
    @staticmethod
    def ygg(eat_all :bool =False, equip :int =0) -> None:
        """Navigate to inventory and handle fruits.
        
        Keyword arguments:
        eat_all  -- Set to true if you're rebirthing, it will force eat all
                   fruit.
        equip    -- Equip loadout with given index. Don't change if zero.
        """
        if equip:
            Misc.reclaim_all()
            Inventory.loadout(equip)
        
        Navigation.menu("yggdrasil")
        if eat_all:
            Inputs.click(*coords.YGG_EAT_ALL)
        else:
            Inputs.click(*coords.HARVEST)

class GoldDiggers:
    @staticmethod
    def gold_diggers(targets :List[int] =const.DEFAULT_DIGGER_ORDER, deactivate :bool =False) -> None:
        """Activate diggers.
        
        Keyword arguments:
        targets -- Array of diggers to use from 1-12. Example: [1, 2, 3, 4, 9].
        deactivate -- Set to True if you wish to deactivate these
                    diggers otherwise it will just try to up the cap.
        """
        Navigation.menu("digger")
        for i in targets:
            page = ((i - 1) // 4)
            item = i - (page * 4)
            Inputs.click(*coords.DIG_PAGE[page])
            if deactivate:
                Inputs.click(*coords.DIG_ACTIVE[item])
            else:
                Inputs.click(*coords.DIG_CAP[item])
    
    @staticmethod
    def deactivate_all_diggers() -> None:
        """Click deactivate all in digger menu."""
        Navigation.menu("digger")
        Inputs.click(*coords.DIG_DEACTIVATE_ALL)
    
    @staticmethod
    def activate_all_diggers() -> None:
        """Click activate all in digger menu."""
        Navigation.menu("digger")
        Inputs.click(*coords.DIG_CAP_ALL)
    
    @staticmethod
    def level_diggers() -> None:
        """Level all diggers."""
        Navigation.menu("digger")
        for page in coords.DIG_PAGE:
            Inputs.click(*page)
            for digger in coords.DIG_LEVEL:
                Inputs.click(*digger, button="right")

class BeardsOfPower:
    """Probably the most useful class ever. -- 4G"""

class Questing:
    inventory_cleaned = False
    
    @staticmethod
    def start_complete() -> None:
        """This starts a new quest if no quest is running.
        If a quest is running, it tries to turn it in.
        """
        Navigation.menu("questing")
        Inputs.click(*coords.QUESTING_START_QUEST)
    
    @staticmethod
    def skip() -> None:
        """This skips your current quest."""
        Navigation.menu("questing")
        time.sleep(userset.MEDIUM_SLEEP)
        Inputs.click(*coords.QUESTING_SKIP_QUEST)
        Navigation.confirm()
    
    @staticmethod
    def get_quest_text() -> str:
        """Check if we have an active quest or not."""
        Navigation.menu("questing")
        Misc.waste_click()  # move tooltip
        time.sleep(userset.SHORT_SLEEP)
        return Inputs.ocr(*coords.OCR_QUESTING_LEFT_TEXT)
    
    @staticmethod
    def get_available_majors() -> int:
        """Return the amount of available major quests."""
        Navigation.menu("questing")
        text = Inputs.ocr(*coords.OCR_QUESTING_MAJORS)
        try:
            match = re.search(r"(\d+)\/\d+", text)
            if match:
                return int(match.group(1))
        except ValueError:
            print("couldn't get current major quests available")
            return -1
    
    @staticmethod
    def questing_consume_items(cleanup :bool =False) -> None:
        """Check for items in inventory that can be turned in."""
        Navigation.menu("inventory")
        Inputs.click(*coords.INVENTORY_PAGE[0])
        bmp = Inputs.get_bitmap()
        for item in coords.QUESTING_FILENAMES:
            path = Inputs.get_file_path("images", item)
            loc = Inputs.image_search(Window.x, Window.y, Window.x + 960, Window.y + 600, path, 0.91, bmp=bmp)
            if loc:
                Inputs.click(*loc, button="right")
                if cleanup:
                    Inputs.send_string("d")
                    Inputs.ctrl_click(*loc)
                time.sleep(3)  # Need to wait for tooltip to disappear after consuming
    
    @staticmethod
    def questing(duration :int =30, major :bool =False, subcontract :bool =False, force :int =0, adv_duration :int =2, butter :bool =False) -> None:
        """Procedure for questing.
        
        ===== IMPORTANT =====
        This method uses imagesearch to find items in your inventory, it will
        both right click and ctrl-click items (quick delete keybind), so make
        sure all items are protected.
        
        The method will only check the inventory page that is currently open,
        so make sure it's set to page 1 and that your inventory has space.
        
        If your inventory fills with mcguffins/other drops while it runs, it
        will get stuck doing the same quest forever. Make sure you will have
        space for the entire duration you will leave it running unattended.
        =====================
        
        Keyword arguments:
        duration     -- The duration in minutes to run if manual mode is selected. If
                        quest gets completed, function will return prematurely.
        major        -- Set to true if you only wish to manually do main quests,
                        if False it will manually do all quests.
        subcontract  -- Set to True if you wish to subcontract all quests.
        force        -- Only quest in this zone. This will skip quests until you
                        recieve one for the selected zone, so make sure you disable
                        "Use major quests if available".
        adv_duration -- The time in minutes to spend sniping before checking inventory.
                        A higher value is good when forcing, because you spend less time
                        scanning the inventory and you will not waste any extra quest items.
                        A value around 2 minutes is good when doing majors because it's very
                        likely that the extra items are lost.
        
        Suggested usages:
        questing(major=True)
        questing(subcontract=True)
        If you only wish to manually do major quests (so you can do ITOPOD)
        then I suggest that you only call questing() every 10-15 minutes because
        subcontracting takes very long to finish. Same obviously goes for subcontracting
        only.
        Remember the default duration is 40, which is there to safeguard if something
        goes wrong to break out of the function. Set this higher/lower after your own
        preferences.
        questing(duration=40)
        This will manually complete any quest you get for 30 minutes, then it returns,
        or it returns when the quest is completed.
        Use this together with harvesting ygg, recapping diggers and so on, or even
        sniping ITOPOD.
        """
        end = time.time() + duration * 60
        Navigation.menu("questing")
        Inputs.click(*coords.WASTE_CLICK)  # move tooltip
        text = Questing.get_quest_text()
        
        if coords.QUESTING_QUEST_COMPLETE in text.lower():
            Questing.start_complete()
            time.sleep(userset.LONG_SLEEP * 2)
            text = Questing.get_quest_text()  # fetch new quest text
        
        if coords.QUESTING_NO_QUEST_ACTIVE in text.lower():  # if we have no active quest, start one
            Questing.start_complete()
            if force and not Questing.inventory_cleaned:
                Questing.questing_consume_items(True)  # we have to clean up the inventory from any old quest items
                Questing.inventory_cleaned = True
            elif not force:
                Questing.questing_consume_items(True)
            Inputs.click(960, 600)  # move tooltip
            time.sleep(userset.LONG_SLEEP)
            text = Questing.get_quest_text()  # fetch new quest text
        
        if force:
            if Inputs.check_pixel_color(*coords.COLOR_QUESTING_USE_MAJOR):
                Inputs.click(*coords.QUESTING_USE_MAJOR)
            
            while not coords.QUESTING_ZONES[force] in text.lower():
                Questing.skip()
                Questing.start_complete()
                text = Questing.get_quest_text()
        
        if subcontract:
            if Inputs.check_pixel_color(*coords.QUESTING_IDLE_INACTIVE):
                Inputs.click(*coords.QUESTING_SUBCONTRACT)
            return
        
        if major and coords.QUESTING_MINOR_QUEST in text.lower():  # check if current quest is minor
            if Inputs.check_pixel_color(*coords.QUESTING_IDLE_INACTIVE):
                Inputs.click(*coords.QUESTING_SUBCONTRACT)
            return
        
        if not Inputs.check_pixel_color(*coords.QUESTING_IDLE_INACTIVE):  # turn off idle
            Inputs.click(*coords.QUESTING_SUBCONTRACT)
        if butter:
            Inputs.click(*coords.QUESTING_BUTTER)
        for count, zone in enumerate(coords.QUESTING_ZONES, start=0):
            if zone in text.lower():
                current_time = time.time()
                while current_time < end:
                    if current_time + adv_duration * 60 > end:  # adjust adv_duration if it will cause duration to be exceeded
                        adv_duration = (end - current_time) / 60
                        if adv_duration < 0.5:
                            adv_duration = 0
                            return
                    Adventure.snipe(count, adv_duration)
                    Inventory.boost_cube()
                    Questing.questing_consume_items()
                    text = Questing.get_quest_text()
                    current_time = time.time()
                    if coords.QUESTING_QUEST_COMPLETE in text.lower():
                        try:
                            start_qp = int(Inputs.remove_letters(Inputs.ocr(*coords.OCR_QUESTING_QP)))
                        except ValueError:
                            print("Couldn't fetch current QP")
                            start_qp = 0
                        Questing.start_complete()
                        Inputs.click(605, 510)  # move tooltip
                        try:
                            current_qp = int(Inputs.remove_letters(Inputs.ocr(*coords.OCR_QUESTING_QP)))
                        except ValueError:
                            print("Couldn't fetch current QP")
                            current_qp = 0
                        
                        gained_qp = current_qp - start_qp
                        print(f"Completed quest in zone #{count} at {datetime.datetime.now().strftime('%H:%M:%S')} for {gained_qp} QP")
                        
                        return
    
    @staticmethod
    def get_use_majors() -> bool:
        """This returns whether the "Use Major Quests if Available" checkbox is toggled ON."""
        return Inputs.check_pixel_color(*coords.COLOR_QUESTING_USE_MAJOR)
    
    @staticmethod
    def toggle_use_majors() -> None:
        """This toggles ON/OFF the "Use Major Quests if Available" checkbox."""
        Navigation.menu("questing")
        Inputs.click(*coords.QUESTING_USE_MAJOR)
    
    @staticmethod
    def set_use_majors(set :bool =True) -> None:
        """This enables/disables the "Use Major Quests if Available" checkbox.
        
        Keyword arguments
        set -- If True, enable the checkbox. If False, disable it.
        """
        Navigation.menu("questing")
        if Questing.get_use_majors() != set:  # Toggle if only one is True
            Questing.toggle_use_majors()

class Hacks:
    @staticmethod
    def hacks(targets :List[int] =None, value :int =1e12) -> None:
        """Activate hacks.
        
        Keyword arguments
        targets -- List of hacks to level up. Default value is [1, 2, 3, 4, 5, 6, 7, 8].
        value   -- Resource to spend, default to 1e12.
        """
        targets = targets or []
        Misc.set_input(value // len(targets))
        Navigation.menu("hacks")
        for i in targets:
            page = ((i - 1) // 8)
            item = i - (page * 8)
            Inputs.click(*coords.HACK_PAGE[page])
            Inputs.click(*coords.HACKS[item])

class Wishes:
    completed_wishes = []

class SelloutShop:
    @staticmethod
    def eat_muffin(buy :bool =False) -> None:
        """Eat a MacGuffin Muffin if it's not active.
        
        Keyword arguments:
        buy -- set to True if you wish to buy a muffin if you have enough
        AP and you currently have 0 muffins.
        """
        Navigation.sellout_boost_2()
        muffin_status = Inputs.ocr(*coords.OCR_MUFFIN).lower()
        if "have: 0" in muffin_status and "inactive" in muffin_status:
            print(muffin_status)
            if buy:
                try:
                    ap = int(Inputs.remove_letters(Inputs.ocr(*coords.OCR_AP)))
                except ValueError:
                    print("Couldn't get current AP")
                if ap >= 50000:
                    print(f"Bought MacGuffin Muffin at: {datetime.datetime.now()}")
                    Inputs.click(*coords.SELLOUT_MUFFIN_BUY)
                    Navigation.confirm()
            else:
                return
        else:
            return
        Inputs.click(*coords.SELLOUT_MUFFIN_USE)
        print(f"Used MacGuffin Muffin at: {datetime.datetime.now()}")

class Rebirth:
    @staticmethod
    def do_rebirth() -> None:
        """Start a rebirth or challenge."""
        Navigation.menu("fight")
        Inputs.click(*coords.FIGHT_STOP)
        Navigation.rebirth()
        Adventure.current_adventure_zone = 0
        Inputs.click(*coords.REBIRTH)
        Inputs.click(*coords.REBIRTH_BUTTON)
        Inputs.click(*coords.CONFIRM)
        return
    
    @staticmethod
    def check_challenge(getNum :bool =False) -> int:
        """Check if a challenge is active.
        
        Keyword arguments
        getNum. -- If true, return the number of the active challenge.
                   This is slower.
                   If False or omitted, return if a challenge is active.
        """
        Navigation.rebirth()
        Inputs.click(*coords.CHALLENGE_BUTTON)
        time.sleep(userset.LONG_SLEEP)
        active = Inputs.check_pixel_color(*coords.COLOR_CHALLENGE_ACTIVE)
        
        if not active:
            return False
        if not getNum:
            return True
        
        text = Inputs.ocr(*coords.OCR_CHALLENGE_NAME)
        if "basic" in text.lower():
            return 1
        elif "augs" in text.lower():
            return 2
        elif "24 hour" in text.lower():
            return 3
        elif "100 level" in text.lower():
            return 4
        elif "equipment" in text.lower():
            return 5
        elif "troll" in text.lower():
            return 6
        elif "rebirth" in text.lower():
            return 7
        elif "laser" in text.lower():
            return 8
        elif "blind" in text.lower():
            return 9
        elif "ngu" in text.lower():
            return 10
        elif "time machine" in text.lower():
            return 11
        
        else:
            return -1
    
    @staticmethod
    def get_rebirth_time() -> Tuple[int, time.struct_time]:
        """Get the current rebirth time.
        returns a namedtuple(days, timestamp) where days is the number
        of days displayed in the rebirth time text and timestamp is a
        time.struct_time object.
        """
        Rebirth_time = namedtuple('Rebirth_time', 'days timestamp')
        t = Inputs.ocr(*coords.OCR_REBIRTH_TIME)
        x = re.search(r"((?P<days>[0-9]+) days? )?((?P<hours>[0-9]+):)?(?P<minutes>[0-9]+):(?P<seconds>[0-9]+)", t)
        days = 0
        if x is None:
            timestamp = time.strptime("0:0:0", "%H:%M:%S")
        else:
            if x.group('days') is None:
                days = 0
            else:
                days = int(x.group('days'))
            
            if x.group('hours') is None:
                hours = "0"
            else:
                hours = x.group('hours')
            
            if x.group('minutes') is None:
                minutes = "0"
            else:
                minutes = x.group('minutes')
            
            if x.group('seconds') is None:
                seconds = "0"
            else:
                seconds = x.group('seconds')
            timestamp = time.strptime(f"{hours}:{minutes}:{seconds}", "%H:%M:%S")
        return Rebirth_time(days, timestamp)
    
    @staticmethod
    def rt_to_seconds() -> int:
        """Get the Rebirth time in seconds"""
        rt = Rebirth.get_rebirth_time()
        seconds = ((rt.days * 24 + rt.timestamp.tm_hour) * 60 + rt.timestamp.tm_min) * 60 + rt.timestamp.tm_sec
        return seconds

class Misc:
    @staticmethod
    def reclaim_all() -> None:
        """Reclaim all resources from all features."""
        Inputs.send_string("r")
        Inputs.send_string("t")
        Inputs.send_string("f")
    
    @staticmethod
    def reclaim_res(energy :bool =False, magic :bool =False, r3 :bool =False) -> None:
        """Reclaim resources of choosing from all features.
        
        Keyword arguments
        energy -- If True, reclaim energy.
        magic  -- If True, reclaim magic.
        r3     -- If True, reclaim resource 3.
        """
        if energy:
            Inputs.send_string("r")
        if magic:
            Inputs.send_string("t")
        if r3:
            Inputs.send_string("f")
    
    @staticmethod
    def reclaim_bm() -> None:
        """Remove all magic from BM."""
        Navigation.menu("bloodmagic")
        Misc.set_input(coords.INPUT_MAX)
        for coord in coords.BM_RECLAIM:
            Inputs.click(*coord)
   
    @staticmethod
    def reclaim_ngu(magic :bool =False) -> None:
        """Remove all e/m from NGUs."""
        if magic:
            Navigation.ngu_magic()
        else:
            Navigation.menu("ngu")
        Misc.set_input(coords.INPUT_MAX)
        for i in range(1, 10):
            NGU = coords.Pixel(coords.NGU_MINUS.x, coords.NGU_PLUS.y + i * 35)
            Inputs.click(*NGU)
    
    @staticmethod
    def reclaim_tm(energy :bool =True, magic :bool =False) -> None:
        """Remove all e/m from TM.
        
        Keyword arguments
        energy -- If True, reclaim energy from TM.
        magic  -- If True, reclaim magic from TM.
        """
        Navigation.menu("timemachine")
        Misc.set_input(coords.INPUT_MAX)
        if magic:
            Inputs.click(*coords.TM_MULT_MINUS)
        if energy:
            Inputs.click(*coords.TM_SPEED_MINUS)
    
    @staticmethod
    def reclaim_aug() -> None:
        """Remove all energy from augs"""
        Navigation.menu("augmentations")
        Misc.set_input(coords.INPUT_MAX)
        Inputs.click(*coords.AUG_SCROLL_TOP)
        scroll_down = False
        for i, k in enumerate(coords.AUGMENT.keys()):
            if i >= 10 and not scroll_down:
                Inputs.click(*coords.AUG_SCROLL_BOT)
                Inputs.click(*coords.AUG_SCROLL_BOT)
                time.sleep(1)
                scroll_down = True
            Inputs.click(coords.AUG_MINUS_X, coords.AUGMENT[k].y)
    
    @staticmethod
    def save_check() -> None:
        """Check if we can do the daily save for AP.
        Make sure no window in your browser pops up when you click the "Save"
        button, otherwise sit will mess with the rest of the script.
        """
        if Inputs.check_pixel_color(*coords.IS_SAVE_READY):
            Inputs.click(*coords.SAVE)
        return
    
    # crops the misc breakdown image, cutting off empty space on the right
    @staticmethod
    def __cutoff_right(bmp) -> PILImage:
        first_pix = bmp.getpixel((0, 0))
        width, height = bmp.size
        
        count = 0
        for x in range(8, width):
            dif = False
            for y in range(0, height):
                if not Inputs.rgb_equal(first_pix, bmp.getpixel((x, y))):
                    dif = True
                    break
            
            if dif: count = 0
            else:
                count += 1
                if count > 8:
                    return bmp.crop((0, 0, x , height))
        
        return bmp
    
    # splits the three parts of the resource breakdown (pow, bars, cap)
    @staticmethod
    def __split_breakdown(bmp) -> List[PILImage]:
        first_pix = bmp.getpixel((0, 0))
        width, height = bmp.size
        y1 = 1
        offset_x = coords.OCR_BREAKDOWN_NUM[0] - coords.OCR_BREAKDOWN_COLONS[0]
        
        slices = []
        for _ in range(0, 3):
            for y in range(y1, height):
                if not Inputs.rgb_equal(first_pix, bmp.getpixel((0, y))):
                    y0 = y
                    break
            
            for y in range(y0, height, coords.BREAKDOWN_OFFSET_Y):
                if Inputs.rgb_equal(first_pix, bmp.getpixel((0, y))):
                    y1 = y
                    break
            
            slice = bmp.crop((offset_x, y0 - 8, width, y1))
            slices.append(Misc.__cutoff_right(slice))
        
        return slices
    
    # Goes to stats breakdown, makes a screenshot
    # Gets it split into three containing all the numbers by calling __split_breakdown
    # Sends all thre images to OCR
    # Returns a list of lists of the numbers from stats breakdown
    @staticmethod
    def __get_res_breakdown(resource, ocrDebug=False, bmp=None, debug=False) -> List[List[str]]:
        Navigation.stat_breakdown()
        
        if   resource == 1: Inputs.click(*coords.BREAKDOWN_E)
        elif resource == 2: Inputs.click(*coords.BREAKDOWN_M)
        elif resource == 3: Inputs.click(*coords.BREAKDOWN_R)
        else : raise RuntimeError("Invalid resource")
        
        if bmp is None:
            bmp = Inputs.get_cropped_bitmap(*Window.gameCoords(*coords.OCR_BREAKDOWN_COLONS))
        if debug: bmp.show()

        imgs = Misc.__split_breakdown(bmp)
        
        ress = []
        for img in imgs:
            if debug: img.show()
            s = Inputs.ocr(0, 0, 0, 0, bmp=img, debug=ocrDebug, binf=220, sliced=True)
            s = s.splitlines()
            s2 = [x for x in s if x != ""]  # remove empty lines
            ress.append(s2)
        
        return ress
    
    # Gets the numbers on stats breakdown for the resource and value passed
    # val = 0 for power, 1 for bars and 2 for cap
    @staticmethod
    def __get_res_val(resource, val) -> int:
        s = Misc.__get_res_breakdown(resource)[val][-1]
        return Inputs.get_numbers(s)[0]
    
    @staticmethod
    def get_pow(resource :int) -> int:
        """Get the power for energy, magic, or resource 3.
        
        Keyword arguments
        resource -- The resource to get power for. 1 for energy, 2 for magic and 3 for r3.
        """
        return Misc.__get_res_val(resource, 0)
    
    @staticmethod
    def get_bars(resource :int) -> int:
        """Get the bars for energy, magic, or resource 3.
        
        Keyword arguments
        resource -- The resource to get bars for. 1 for energy, 2 for magic and 3 for r3.
        """
        return Misc.__get_res_val(resource, 1)
    
    @staticmethod
    def get_cap(resource :int) -> int:
        """Get the cap for energy, magic, or resource 3.
        
        Keyword arguments
        resource -- The resource to get cap for. 1 for energy, 2 for magic and 3 for r3.
        """
        return Misc.__get_res_val(resource, 2)
    
    @staticmethod
    def get_idle_cap(resource :int) -> int:
        """Get the available idle energy, magic, or resource 3.
        
        Keyword arguments
        resource -- The resource to get idle cap for. 1 for energy, 2 for magic and 3 for r3.
        """
        try:  # The sliced argument was meant for low values with get_pow/bars/cap
            # But also serves for low idle caps
            if   resource == 1: res = Inputs.ocr(*coords.OCR_ENERGY, sliced=True)
            elif resource == 2: res = Inputs.ocr(*coords.OCR_MAGIC , sliced=True)
            elif resource == 3: res = Inputs.ocr(*coords.OCR_R3    , sliced=True)
            else : raise RuntimeError("Invalid resource")
            
            res = Inputs.get_numbers(res)[0]
            return res
            
        except IndexError:
            print("couldn't get idle cap")
            return 0

    @staticmethod
    def set_input(value :int) -> None:
        """Sets a value in the input box.
        Requires the current menu to have an imput box.
        
        Keyword arguments
        value -- The value to be set
        """
        Navigation.input_box()
        Inputs.send_string(value)
        Misc.waste_click()
    
    @staticmethod
    def waste_click() -> None:
        """Make a click that does nothing"""
        Inputs.click(*coords.WASTE_CLICK)
        time.sleep(userset.FAST_SLEEP)