import math

from core.chat_blob import ChatBlob
from core.command_param_types import Const, Int
from core.decorators import instance, command
from core.dict_object import DictObject


@instance()
class LadderController:
    def __init__(self):
        self.grades = ["shiny", "bright", "faded"]

    def inject(self, registry):
        self.db = registry.get_instance("db")
        self.util = registry.get_instance("util")
        self.text = registry.get_instance("text")

    @command(command="ladder", params=[Const("treatment"), Int("starting_amount")], access_level="member",
             description="Show sequence of laddering implants for treatment",
             extended_description="The starting amount should be the treatment or "
                                  "ability you have with all nano buffs, "
                                  "perks, and items-buffing equipment equipped, "
                                  "but minus any buffs from implants that you have equipped.")
    def ladder_treatment_cmd(self, _, _1, treatment):
        if treatment < 11:
            return "Base treatment must be at least <highlight>11</highlight>."

        prefix = "skill_"
        result = self.optimized_steps(prefix, treatment, self.get_implant_by_max_treatment)

        blob = self.format_steps("treatment", result.steps, treatment,
                                 (treatment + self.calculate_total(result.slots, prefix)))

        return ChatBlob("Laddering Treatment", blob)

    @command(command="ladder", params=[Const("ability"), Int("starting_amount")], access_level="member",
             description="Show sequence of laddering implants for ability",
             extended_description="The starting amount should be the treatment or "
                                  "ability you have with all nano buffs, "
                                  "perks, and items-buffing equipment equipped, "
                                  "but minus any buffs from implants you have equipped.")
    def ladder_ability_cmd(self, _, _1, ability):
        if ability < 6:
            return "Base ability must be at least <highlight>6</highlight>."

        prefix = "ability_"
        result = self.optimized_steps(prefix, ability, self.get_implant_by_max_ability)

        blob = self.format_steps("ability", result.steps, ability,
                                 (ability + self.calculate_total(result.slots, prefix)))

        return ChatBlob("Laddering Ability", blob)

    def format_steps(self, label, steps, starting, ending):
        blob = f"Starting {label}: <highlight>{starting}</highlight>\n\n"
        blob += "-------------------\n\n"
        for (action, grade, implant) in steps:
            blob += f"{action.capitalize()} {grade} QL <highlight>{implant.ql:d}</highlight>\n\n"

        blob += "-------------------\n\n"
        blob += "Ending %s: <highlight>%s</highlight>\n\n" % (label, ending)
        blob += "\nInspired by a command written by Lucier of the same name"

        return blob

    def optimized_steps(self, prefix, base_value, get_max_implant):
        grade_combinations = [self.grades,
                              ["shiny", "faded", "bright"],
                              ["bright", "shiny", "faded"],
                              ["bright", "faded", "shiny"],
                              ["faded", "shiny", "bright"],
                              ["faded", "bright", "shiny"]]

        best = None
        for grade_combo in grade_combinations:
            result = self.get_steps(prefix, base_value, get_max_implant,
                                    lambda current_grade: self.get_next_grade(current_grade, grade_combo))
            if not best:
                best = result
            else:
                result_value = self.calculate_total(result.slots, prefix)
                best_value = self.calculate_total(best.slots, prefix)
                if result_value > best_value:
                    # this is here as a sanity check, but it appears
                    # that the result is always the same no matter which order you insert the implants
                    best = result
                elif best_value == result_value and len(result.steps) < len(best.steps):
                    # this optimizes for the least amount of steps/implants
                    best = result

        return best

    def get_steps(self, prefix, base_value, get_max_implant, get_next_grade):
        slots = DictObject({"shiny": None,
                            "bright": None,
                            "faded": None})

        steps = []

        num_skipped = 0
        current_grade = None
        while num_skipped < 3:
            current_grade = get_next_grade(current_grade)

            # find next highest possible implant
            new_implant = get_max_implant(self.calculate_total(slots, prefix, current_grade) + base_value)

            if not slots.get(current_grade):
                steps.append(["add", current_grade, new_implant])
                slots[current_grade] = new_implant
            elif new_implant.get(prefix + current_grade) > slots.get(current_grade).get(prefix + current_grade):
                steps.append(["remove", current_grade, slots.get(current_grade)])
                steps.append(["add", current_grade, new_implant])
                slots[current_grade] = new_implant
            else:
                num_skipped += 1

        return DictObject({"slots": slots,
                           "steps": steps})

    def get_next_grade(self, current_grade, grades):
        if not current_grade:
            next_index = 0
        else:
            next_index = grades.index(current_grade) + 1
            if next_index >= len(grades):
                next_index = 0

        return grades[next_index]

    def get_implant_by_max_treatment(self, treatment):
        return self.db.query_single("SELECT * from implant_requirement "
                                    "WHERE treatment <= ? "
                                    "ORDER BY ql DESC "
                                    "LIMIT 1",
                                    [treatment])

    def get_implant_by_max_ability(self, ability):
        return self.db.query_single("SELECT * from implant_requirement "
                                    "WHERE ability <= ? "
                                    "ORDER BY ql DESC "
                                    "LIMIT 1",
                                    [ability])

    def get_cluser_min_ql(self, ql, grade):
        if grade == "shiny":
            result = math.floor(ql * 0.86)
        elif grade == "bright":
            result = math.floor(ql * 0.84)
        elif grade == "faded":
            result = math.floor(ql * 0.82)
        else:
            raise Exception("Unknown grade: '%s'" % grade)

        if ql >= 201:
            return max(201, result)
        else:
            return result

    def calculate_total(self, slots, prefix, skip_grade=None):
        value = 0
        for grade in self.grades:
            if grade != skip_grade:
                implant = slots.get(grade)
                if implant:
                    value += implant.get(prefix + grade)

        return value
