def list_proper_tag_shortcuts(
        args: argparse.Namespace) -> Tuple[bool, List[str]]:
    """Retrieves all shortcuts and their associated tag, or the
    specified shortcut(s) and their associated tag, depending on
    arguments.

    Returns a tuple containing a boolean at the first index to show
    whether this function failed or succeeded, and a list of messages
    at the second index.

    This will not list built-in shortcuts. You can find those by
    using the 'reverse' flag to see what tags can be called using
    what shortcuts.
    """
    messages = []
    # Get all tag conversions
    shortcut_dict = {
        **read_lines_into_dict("./info/recruitops/tagShortcuts.txt", )
    }

    # To translate from zh tag to eng tag
    rev_translation_dict = read_lines_into_dict(
        "./info/recruitops/formattedTagConversions.txt", reverse=True)

    messages = ["\n\nShortcuts\n"]

    # If specified, find all the shortcuts
    if args.mode == "all":
        if len(shortcut_dict.items()) == 0:
            return (False, ["\n\nNo shortcuts found!"])
        else:
            for key, value in shortcut_dict.items():
                messages.append(f"{key:20}---> {rev_translation_dict[value]}")
    else:
        if len(args.shortcut) == 0:
            return (False, ["\n\nNo shortcuts provided."])

        # Make sure the tag exists
        for shortcut in args.shortcut:
            if shortcut not in shortcut_dict.keys():
                return (False, [f"\n\nCould not find shortcut '{shortcut}'."])

        # Format the specific shortcuts
        for shortcut in args.shortcut:
            messages.append(f"{shortcut:20} ---> " +
                            f"{rev_translation_dict[shortcut_dict[shortcut]]}")

    return (True, messages)
def parse_info_from_json(args, operator_dict, operator_key):
    """Gets information for a certain operator from various JSON
    files, and return an Operator object with the necessary information
    based on the flags in args.

    This function assumes a check was already done to ensure the
    operator exists in the JSON files.

    Like the Gamepress function, this function independantly
    gathers the barebones information (name, rarity, description, etc.),
    but then creates a 'conditional list' (That is, consisting of a
    function to call for each property, a conditional (usually a flag),
    and arguments).

    That is then passed to another function that matches those
    conditionals, calls the appropriate, necessary functions which
    return information, which is then assigned to the Operator object
    that is to be returned.
    """
    description_text = filter_description(operator_dict["description"])

    formatted_json_prof = read_lines_into_dict(
        "./info/scraper/formattedJsonProfessions.txt")
    # Set up the operator object with the good fetches
    operator = Operator(
        operator_dict["name"],
        operator_dict["rarity"] + 1,
        formatted_json_prof[operator_dict["profession"].title()],
        [
            description_text + "\n", operator_dict["itemUsage"] + "\n",
            operator_dict["itemDesc"] + "\n\n"
        ],
        operator_dict["tagList"],
    )

    # This is repeated but I feel that's fine for abstraction
    if args.vskills:
        skill_tiers_to_check = ([1, 7, 10]
                                if operator_dict["rarity"] + 1 > 3 else [1, 7])
    else:
        skill_tiers_to_check = ([10]
                                if operator_dict["rarity"] + 1 > 3 else [7])

    check_skills = args.skills or args.vskills

    conditionals = [
        [
            "skills", check_skills, parse_skills,
            [operator_dict, skill_tiers_to_check]
        ],
        ["talent", args.talent, parse_talents, [operator_dict]],
        ["base skills", args.base, parse_base_skills, [operator_key]],
    ]

    stats_requirements = [args.info, create_stats_dict, [operator_dict]]

    set_operator_properties(args, conditionals, stats_requirements, operator)

    return operator
def parse_base_skills(operator_key):
    """Using an operator's key in the info JSON, finds and assembles
    a list of strings that contain a formatted description of
    the operator's base skills, and returns them.

    Since the base skills info is split across three JSON files,
    we use the key of each operator to find all the base skills of said
    operator using one JSON. Then, we find the details of each of
    the base skills using another JSON, and format those details to
    form the final message list.

    This function will make 2 other requests to other JSON files
    in order to properly fetch base skills.
    """
    # We'll have to load in two seperate jsons...
    base_skills_json, riic_json = get_base_jsons()

    # If the jsons fail to load, or if the key can't be found in the
    # base skills json, we have to quit so our program
    # doesn't kill itself.
    if base_skills_json == {} or riic_json == {}:
        return ["\n\nBase Skills\nBase skill JSONs failed to load!"]

    if operator_key not in base_skills_json["chars"].keys():
        return ["\n\nBase Skills\nCould not find matching base skill(s)!"]

    messages = []
    messages.append("\n\nBase Skills\n")

    # Since we want to remain consistent with gamepress description,
    # we have a file that converts the shorter room names to the proper
    # room names that we'll be displaying.
    formatted_json_rooms = read_lines_into_dict(
        "./info/scraper/formattedJsonRooms.txt")

    char = base_skills_json["chars"][operator_key]
    # Looks messy, but needed for traversing the jsons
    for bchar in char["buffChar"]:
        if len(bchar["buffData"]) > 0:
            for bskill in bchar["buffData"]:
                # We're just trying to replicate what is got
                # from gamepress.gg
                bskill_info = riic_json[bskill["buffId"]]

                zh_bskill_info = \
                    base_skills_json["buffs"][bskill["buffId"]]

                messages.append(
                    bskill_info["name"] + "  " + "Lvl: " +
                    str(bskill["cond"]["level"]) + "  " + "(" +
                    zh_bskill_info["buffName"] + ")" + "  " + "Room Type:  " +
                    formatted_json_rooms[zh_bskill_info["roomType"].title()] +
                    "  " + "E" + str(bskill["cond"]["phase"]))

                messages.append(" " + bskill_info["desc"] + "\n")

    return messages
def delete_tag_shortcut(args: argparse.Namespace) -> None:
    """Either deletes a shortcut (or shortcuts) from the list of
    shortcuts, or deletes all the shortcuts from the list of shortcuts.

    Returns nothing as this function will print directly to the screen.
    """
    spinner = Halo(text="Deleting...", spinner="dots", color="red")
    spinner.start()
    # Load dicts
    shortcut_dict = read_lines_into_dict("./info/recruitops/tagShortcuts.txt")

    # Make sure the shortcuts exist if they are included
    if args.mode == "select":
        if len(args.shortcut) == 0:
            spinner.fail("Failed.")
            sys.stdout.write(f"\n\nNo shortcuts were provided!\n\n")
            return

        for shortcut in args.shortcut:
            if shortcut not in shortcut_dict.keys():
                spinner.fail("Failed.")
                sys.stdout.write(
                    f"\n\nCould not find the shortcut '{shortcut}'!\n\n")
                return

    # Only one file IO needed (disregarding the read_lines_into_dict)
    # Actually remove tags
    with open("./info/recruitops/tagShortcuts.txt", "w", encoding="utf8") as f:
        # If you open a file as w and do nothing, it simply clears
        # everything in the file. We'll simply check if args.all
        # was specified, and if it is, do nothing.
        #
        # Otherwise, actually loop through the dict and don't delete
        # everything.
        if args.mode == "select":
            for key, value in shortcut_dict.items():
                # Skip any tag that matches the shortcut, basically
                # deleting it.
                if key in args.shortcut:
                    continue  # oh no a bad continue

                f.write(key + " " + value + "\n")

    # Write success message
    spinner.succeed("Success!")
    if args.mode == "all":
        sys.stdout.write("\n\nSuccessfully deleted all tags!\n\n")
    else:
        sys.stdout.write(  # this is just to keep it under line limit
            "\n\nSuccessfully deleted " + ", ".join(args.shortcut) + "!\n\n")
def initialize_operator_list() -> Optional[List[Type[TaggedOperator]]]:
    """Initializes a list of TaggedOperators with names and tags and
    returns said list.

    This function uses a recruitment json created by Aceship to find
    each operator's tags, so if this function is unable to find the
    json, it will return None which should be caught.

    Otherwise, it will generate a list of TaggedOperator objects, each
    with a name, rarity, and recruitment tags.

    Note that hidden operators (globalHidden or hidden) will not be
    included in this list.
    """
    operatortags_rawjson = scrape_json(
        read_line_from_file("./info/recruitops/recruitTagJsonUrl.txt"))
    # with open("tags_zh.json", "r", encoding="utf8") as f:
    #     operatortags_rawjson = json.load(f)  # debug

    if operatortags_rawjson is None:
        return None

    # Getting the json content returns a list, so we call it a
    # list appropriately
    operatortags_list = operatortags_rawjson.json()
    # operatortags_list = operatortags_rawjson # debug
    operator_list = []

    name_replacements = read_lines_into_dict(
        "./info/recruitops/operatorNameReplacements.txt")

    # initialize an easy to access list of operators and their tags
    #
    # Some operators aren't available in HH and thus they are labelled
    # as 'hidden' or 'globalHidden' in the json.
    for operator in operatortags_list:
        if (operator["hidden"] or
            ("globalHidden" in operator.keys() and operator["globalHidden"])):
            continue

        operator_list.append(
            TaggedOperator(
                operator["name_en"], operator["level"], operator["tags"] +
                [operator["type"].rstrip()]) if operator["name_en"] not in
            name_replacements else TaggedOperator(
                name_replacements[operator["name_en"]], operator["level"],
                operator["tags"] + [operator["type"].rstrip()]))

    return operator_list
def get_operator_dict(operator):
    """Searches the Aceship character JSON for a specified operator,
    and returns the associated character dict and the character key
    in the JSON if found.

    If the operator was not found, return an empty dict and None
    for the key, indicating that the scraper should try using
    Gamepress instead of the JSON, as the character is not
    in the JSON yet.
    """
    # Since the JSON I first use to find info uses
    # properly formatted names, I have to convert any name
    # to a properly formatted one

    # TODO: Does this break DRY? This appears in gamepress too
    replacement_names = read_lines_into_dict(
        "./info/scraper/jsonOperatorReplacements.txt")

    formatted_name = operator.replace("-", " ").title()
    proper_name = (formatted_name
                   if formatted_name not in replacement_names.keys() else
                   replacement_names[formatted_name])

    # with open("character_table.json", "r", encoding="utf8") as f:
    #     operator_raw_json = json.load(f)  # debug
    operator_raw_json = scrape_json(
        read_line_from_file("./info/scraper/operatorJsonUrl.txt"))
    if operator_raw_json is None:
        return {}, None

    operator_dict = {}
    operator_key = None
    operator_json = operator_raw_json.json()
    # operator_json = operator_raw_json #debug
    for operator in operator_json.keys():
        # So that names like "SilverAsh" don't screw up the parser,
        # we take the key and convert it to the title form (Silverash)
        if operator_json[operator]["name"].title() == proper_name:
            operator_key = operator
            operator_dict = operator_json[operator]
            break

    return operator_dict, operator_key
def scrape_for_operator(operator):
    """Sends a GET request for a certain operator and returns the
    Response object if status code is 200.

    Returns None (as per scrape_website() implementation) if server
    responds with a different code.
    """
    url_replacement_names = read_lines_into_dict(
        "./info/scraper/urlOperatorReplacements.txt")

    operator_url = read_line_from_file("./info/scraper/url.txt")
    operator_url = (
        operator_url + "operator/" + operator
        if operator not in url_replacement_names.keys()
        else operator_url + "operator/"
        + url_replacement_names[operator]
    )

    return scrape_website(operator_url)
def get_info_from_gamepress(args, operator_name):
    """Gets information for a certain operator from a Gamepress
    page, and return an Operator object with the necessary information
    based on the flags in args.

    If no Gamepress page is found with the specified operator's name
    (or the page is down), this function will return None.

    This function independantly gathers the barebones information
    (name, rarity, description, etc.), but then creates a
    'conditional list' (That is, consisting of a function to call,
    a conditional (usually a flag), and arguments). That is then
    passed to another function that matches those conditionals, calls
    the appropriate, necessary functions which return information,
    which is then assigned to the Operator object that is
    to be returned.
    """
    response = scrape_for_operator(operator_name)
    if response is not None:  # response succeeds
        src = response.content

        images_dict = read_lines_into_dict("./info/scraper/imageToText.txt")
        replacement_names = read_lines_into_dict(
            "./info/scraper/jsonOperatorReplacements.txt")

        soup = BeautifulSoup(src, "lxml")
        # soup = BeautifulSoup(open("debug.html", "r", encoding="utf-8"), "lxml") # debugging

        # Finding the default information that should be displayed
        # for every operator (eg. tags, description, etc.)
        tags = list(
            map(lambda souptxt: souptxt.text.strip(),
                soup.find_all("div", "tag-title")))

        # We can find the rarity of an operator by finding the div
        # named rarity-cell and counting how many images of stars
        # are in it
        rarity = len(soup.find("div", "rarity-cell").find_all("img"))

        profession_text = (soup.find("div", "profession-title").text.strip())

        desc = soup.find_all("div", "description-box")

        desc_text = (["No proper description."] if (len(desc) < 3) else [
            "".join(find_siblings_of_breakpoint(desc[item])).strip() + "\n"
            for item in range(3)
        ])

        # Since the alternative JSON I use to find stats may have
        # another name for an operator, I have to convert any name
        # to a proper one recognized by that specific json
        formatted_name = operator_name.replace("-", " ").title()
        proper_name = (formatted_name
                       if formatted_name not in replacement_names.keys() else
                       replacement_names[formatted_name])

        operator = Operator(proper_name, rarity, profession_text, desc_text,
                            tags)

        # Any optional messages/properties are stored in
        # operator.properties for convenience and also to make sure
        # that printing properties doesn't take 50 lines of code.
        # Also, we can reuse the properties this way as it is stored
        # in a compact location.

        # Skills are different; since we have a mutually exclusive group
        # we need to format input a tiny bit before we continue
        # with skills info gathering.

        if args.vskills:
            skill_tiers_to_check = ([
                "skill-upgrade-tab-1", "skill-upgrade-tab-7",
                "skill-upgrade-tab-10"
            ] if rarity > 3 else [
                "skill-upgrade-tab-1", "skill-upgrade-tab-7"
            ])
        else:
            skill_tiers_to_check = (["skill-upgrade-tab-10"]
                                    if rarity > 3 else ["skill-upgrade-tab-7"])

        check_skills = args.skills or args.vskills

        # Taking advantage of python's functional programming paradigms
        # to adhere to DRY principles

        # TODO: is this even good practice???
        # I'm trying to adhere to DRY principles but this makes me
        # start to sweat and foam at the mouth
        conditionals = [
            [
                "skills", check_skills, find_skills,
                [soup, skill_tiers_to_check]
            ],
            ["talent", args.talent, find_talents, [soup, images_dict]],
            ["base skills", args.base, find_base_skills, [soup, images_dict]],
        ]

        stats_requirements = [
            args.info, create_stats_json, [soup, proper_name]
        ]
        # Set the operator object's properties based on conditional
        # list
        set_operator_properties(args, conditionals, stats_requirements,
                                operator)
    else:
        # The request failed, operator not found
        operator = None

    return operator
def find_recruitment_combos(args: argparse.Namespace) -> None:
    """Taking the specified namespace of arguments, this function will
    determine combinations of tags, find operators that match those
    combinations, and print to the screen a formatted list of
    combinations and operators, sorted by value bottom-to-top."""
    spinner = Halo(text="Fetching...", spinner="dots", color="magenta")
    spinner.start()

    op_list = initialize_operator_list()
    if op_list is None:
        spinner.fail("Failed.")
        sys.stdout.write(
            "\n\nThe tag JSON could not be fetched! Try again later.")
    else:
        spinner.text = "Calculating..."
        spinner.color = "yellow"

        tag_dict = initialize_tag_dictionary(op_list)

        # Get both a proper translation from en to zh dict with the
        # new tag shortcuts and the premade tags
        # and a reversed dict initialized for proper tag conversion
        translation_dict = {
            **read_lines_into_dict("./info/recruitops/tagConversions.txt"),
            **read_lines_into_dict("./info/recruitops/tagShortcuts.txt")
        }
        reversed_translation_dict = read_lines_into_dict(
            "./info/recruitops/formattedTagConversions.txt", reverse=True)

        # Take in the user tags and find their proper, translated names
        # so that they can be used with the json.
        proper_tags = []
        # TODO: this tag process could probably be more optimized
        for tag in args.tags:
            if tag.lower() in translation_dict.keys():
                proper_tags.append(translation_dict[tag.lower()])
            else:
                # TODO: exit nicer
                raise Exception(f"The tag '{tag.lower()}' does not exist.")

        # Find all possible combinations of each tag combo
        all_matches = get_all_combinations(proper_tags, tag_dict,
                                           translation_dict,
                                           reversed_translation_dict)

        # Sort based on priority and format all the possible
        # combinations.
        #
        # Consists of all the tag combinations and results, sorted
        # by priority.
        all_sorted_selection = sorted(all_matches, key=lambda s: s.priority)
        messages = format_selections(args, all_sorted_selection)

        # Print the recruitment results
        spinner.succeed("Success!")
        sys.stdout.write("\n\nRecruitment Results\n\n")  # padding
        sys.stdout.write("Note: the lower down the tag collection, " +
                         "the better the tags.\n\n\n")  # padding

        if len(messages) <= 0:
            sys.stdout.write("Could not find any recruitment results.\n")
        else:
            for msg in messages:
                sys.stdout.write(msg + "\n")
        sys.stdout.write("\n")  # padding
def create_tag_shortcut(args: argparse.Namespace) -> None:
    """Creates a new shortcut for a tag for later use.

    If the shortcut already exists in the shortcut list, overwrite
    the old shortcut with the new tag. If this happens, the overwriting
    process time is proportional to the size of the shortcut list,
    as the function will overwrite and rewrite the entire list.

    Prints directly to the screen. If the tag specified was
    not found, return early. Returns nothing.
    """
    spinner = Halo(text="Adding...", spinner="dots", color="red")
    spinner.start()

    # Load the dicts needed to accomplish this task
    shortcut_dict = read_lines_into_dict("./info/recruitops/tagShortcuts.txt")
    translation_dict = read_lines_into_dict(
        "./info/recruitops/tagConversions.txt")

    # Make sure the tag specified actually exists and that the
    # shortcut name doesn't already exist in the translation dict.
    if args.tag in translation_dict.keys():
        tag_name = translation_dict[args.tag]
    else:
        spinner.fail("Failed.")
        sys.stdout.write(f"\n\nCould not find the tag '{args.tag}'!\n\n")
        return

    if args.shortcut in translation_dict.keys():
        spinner.fail("Failed.")
        sys.stdout.write(
            f"\n\nShortcut '{args.shortcut}' cannot be changed!\n\n")
        return

    # Actually create the shortcut to a tag
    if args.shortcut not in shortcut_dict.keys():
        with open("./info/recruitops/tagShortcuts.txt", "a",
                  encoding="utf8") as f:
            f.write(args.shortcut + " " + tag_name + "\n")

    else:
        # If the shortcut exists, we'll have to override the tag
        # and it.
        key_list, val_list = zip(*shortcut_dict.items())

        line_num = key_list.index(args.shortcut)
        index = 0

        with open("./info/recruitops/tagShortcuts.txt", "w",
                  encoding="utf8") as f:
            while index < len(key_list):
                if index == line_num:
                    # override the old value with new one
                    f.write(args.shortcut + " " + tag_name + "\n")
                else:
                    # preserve the old values as they're unrelated
                    f.write(key_list[index] + " " + val_list[index] + "\n")

                index += 1

    # Write success message
    spinner.succeed("Success!")
    sys.stdout.write(  # this is just to keep it under line limit
        "\n\nSuccessfully added shortcut '" + args.shortcut + "' for tag '" +
        args.tag + "'!\n\n")
def list_reversed_tag_shortcuts(
        args: argparse.Namespace) -> Tuple[bool, List[str]]:
    """Retrieves either all tags and their shortcuts or a specified
    tag and its shortcut, formats them, and then prints to the screen.
    Returns nothing.

    Returns a tuple containing a boolean at the first index to show
    whether this function failed or succeeded, and a list of messages
    at the second index.

    The user is able to input a shortcut to a tag as a reference to
    that tag. However, as a result of implementation, this function
    will take more time the more shortcuts there are.

    If the shortcut to the tag is built-in, a star will be put next
    to it indicating that this tag cannot be removed or overwritten.
    """
    messages = ["\n\nTag Shortcuts\n"]

    # Load dictionaries, this time in reverse
    #
    # We avoid combining because the values are lists, and I don't
    # feel like combining them right now
    # TODO: these dict calls can probably be optimized
    rev_shortcut_dict = read_lines_into_dict(
        "./info/recruitops/tagShortcuts.txt", reverse=True, overwrite=False)
    rev_conversion_dict = read_lines_into_dict(
        "./info/recruitops/tagConversions.txt", reverse=True, overwrite=False)
    # to see what tag the user inputted
    # this could take some time depending on the amount of shortcuts
    # though... but I think it's better this way for sake of efficiency
    conversion_dict = {
        **read_lines_into_dict("./info/recruitops/tagConversions.txt", ),
        **read_lines_into_dict("./info/recruitops/tagShortcuts.txt", )
    }

    rev_translation_dict = read_lines_into_dict(
        "./info/recruitops/formattedTagConversions.txt", reverse=True)

    # Add stars after the important shortcuts that can't be
    # deleted or overwritten.
    for key in rev_conversion_dict:
        rev_conversion_dict[key] = (list(
            map(lambda sc: sc + " *", rev_conversion_dict[key])))

    # Get all tag-shortcut connections if needed
    if args.mode == "all":
        for key in rev_translation_dict:
            # It may seem redundant to go from tag to key and then key to
            # tag, but this formats the tag and also has an efficiency
            # of O(1), so it's fine.
            #
            # We know that every tag has at least one builtin shortcut,
            # so no need to check if there is a shortcut for a tag.
            #
            # We're also doing some checks here to make sure that
            # everything is properly formatted if there's only one
            # tag for it, and that commas are in a correct place,
            # and that shortcuts exist for that tag, and that
            # shortcuts are formatted well...
            messages.append(f"{rev_translation_dict[key]}\n" +
                            (",\n".join(rev_conversion_dict[key]) if key not in
                             rev_shortcut_dict.keys() else
                             (",\n".join(rev_conversion_dict[key] +
                                         rev_shortcut_dict[key]))) + "\n")
    # Get specified tags
    else:
        # Make sure there are shortcuts
        if len(args.shortcut) == 0:
            return (False, ["\n\nNo shortcuts provided."])

        # Make sure that the tag actually exists
        for tag in args.shortcut:
            if tag.lower() not in conversion_dict.keys():
                return (False, [f"\n\nCould not find tag '{tag}'."])

        # Get the actual tags-shorcut connections requested
        for tag in args.shortcut:
            # It may seem redundant to go from tag to key and then key
            # to tag, but this formats the tag and also has an
            # efficiency of O(1), so it's fine.
            key = conversion_dict[tag]

            messages.append(f"{rev_translation_dict[key]}\n" +
                            (",\n".join(rev_conversion_dict[key]) if key not in
                             rev_shortcut_dict.keys() else
                             (",\n".join(rev_conversion_dict[key] +
                                         rev_shortcut_dict[key]))) + "\n")

    # Get rid of the \n that is always present at the last element
    # for consistant formatting
    messages[-1] = messages[-1].rstrip()

    return (True, messages)