import datetime
import locale
import math
import re

import pytz

from core.decorators import instance
from core.dict_object import DictObject
from core.text import Text


@instance()
class Util:
    budatime_full_regex = re.compile("^([0-9]+[a-z]+)+$", re.IGNORECASE)
    budatime_unit_regex = re.compile("([0-9]+)([a-z]+)", re.IGNORECASE)

    def __init__(self):
        # needed for self.format_number() to work properly
        locale.setlocale(locale.LC_NUMERIC, '')

        self.abilities = [
            "Agility",
            "Intelligence",
            "Psychic",
            "Stamina",
            "Strength",
            "Sense"
        ]

        self.time_units = [
            {
                "units": ["yr", "years", "year", "y"],
                "conversion_factor": 31536000
            }, {
                "units": ["month", "months", "mo"],
                "conversion_factor": 2592000
            }, {
                "units": ["week", "weeks", "w"],
                "conversion_factor": 604800
            }, {
                "units": ["day", "days", "d"],
                "conversion_factor": 86400
            }, {
                "units": ["hr", "hours", "hour", "hrs", "h"],
                "conversion_factor": 3600
            }, {
                "units": ["min", "mins", "m"],
                "conversion_factor": 60
            }, {
                "units": ["sec", "secs", "s"],
                "conversion_factor": 1
            }
        ]

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

    def get_handler_name(self, handler):
        return handler.__module__ + "." + handler.__qualname__

    def get_module_name(self, handler):
        handler_name = self.get_handler_name(handler)
        parts = handler_name.split(".")
        return parts[1] + "." + parts[2]

    def parse_time(self, budatime, default=0):
        unixtime = 0

        if not self.budatime_full_regex.search(budatime):
            return default

        matches = self.budatime_unit_regex.finditer(budatime)

        for match in matches:
            for time_unit in self.time_units:
                if match.group(2).lower() in time_unit["units"]:
                    unixtime += int(match.group(1)) * time_unit["conversion_factor"]
                    continue

        return unixtime

    def time_to_readable(self, unixtime, min_unit="sec", max_unit="day", max_levels=2):
        if unixtime == 0:
            return "0 secs"

        # handle negative as positive, and add negative sign at the end
        is_negative = False
        if unixtime < 0:
            is_negative = True
            unixtime *= -1

        found_max_unit = False
        time_shift = ""
        levels = 0
        for time_unit in self.time_units:
            unit = time_unit["units"][0]

            if max_unit in time_unit["units"]:
                found_max_unit = True

            # continue to skip until we have found the max unit
            if not found_max_unit:
                continue

            unit_value = math.floor(unixtime / time_unit["conversion_factor"])

            if unit_value == 0:
                # do not show units where unit_value is 0
                pass
            elif unit_value == 1:
                # show singular where unit_value is 1
                time_shift += str(unit_value) + " " + unit + " "
            else:
                # show plural where unit_value is greater than 1
                time_shift += str(unit_value) + " " + unit + "s "

            unixtime %= time_unit["conversion_factor"]

            # record level after the first a unit has a length
            if levels or unit_value >= 1:
                levels += 1

            if levels == max_levels:
                break

            # if we have reached the min unit, then break, unless we have no output, in which case we continue
            if time_shift and min_unit in time_unit["units"]:
                break

        return ("-" if is_negative else "") + time_shift.strip()

    def get_ability(self, ability_str):
        ability_str = ability_str.capitalize()
        for ability in self.abilities:
            if ability.startswith(ability_str):
                return ability
        return None

    def get_all_abilities(self):
        return self.abilities.copy()

    def get_title_level(self, level):
        if level < 5:
            return 0
        elif level < 15:
            return 1
        elif level < 50:
            return 2
        elif level < 100:
            return 3
        elif level < 150:
            return 4
        elif level < 190:
            return 5
        elif level < 205:
            return 6
        else:
            return 7

    def get_level_range_tl(self, tl):
        if tl == 0:
            return 0, 4
        elif tl == 1:
            return 5, 14
        elif tl == 2:
            return 15, 49
        elif tl == 3:
            return 50, 99
        elif tl == 4:
            return 100, 149
        elif tl == 5:
            return 150, 189
        elif tl == 6:
            return 190, 204
        elif tl == 7:
            return 205, 300
        else:
            return 0, 0

    def format_number(self, number):
        return locale.format_string("%.*f", (0, number), grouping=True)

    def get_profession(self, search, long=True):
        search = search.lower()

        if search in ["adv", "advy", "adventurer"]:
            return "Adventurer" if long else "Adv"
        elif search in ["agent"]:
            return "Agent" if long else "Agent"
        elif search in ["crat", "bureaucrat"]:
            return "Bureaucrat" if long else "Crat"
        elif search in ["doc", "doctor"]:
            return "Doctor" if long else "Doc"
        elif search in ["enf", "enfo", "enforcer"]:
            return "Enforcer" if long else "Enf"
        elif search in ["eng", "engi", "engy", "engineer"]:
            return "Engineer" if long else "Eng"
        elif search in ["fix", "fixer"]:
            return "Fixer" if long else "Fix"
        elif search in ["keep", "keeper"]:
            return "Keeper" if long else "Keep"
        elif search in ["ma", "martial", "martialartist", "martial artist"]:
            return "Martial Artist" if long else "MA"
        elif search in ["mp", "meta", "metaphysicist", "meta-physicist"]:
            return "Meta-Physicist" if long else "MP"
        elif search in ["nt", "nano", "nanotechnician", "nano-technician"]:
            return "Nano-Technician" if long else "NT"
        elif search in ["sha", "shade"]:
            return "Shade" if long else "Shade"
        elif search in ["sol", "sold", "soldier"]:
            return "Soldier" if long else "Sold"
        elif search in ["tra", "trad", "trader"]:
            return "Trader" if long else "Trader"
        elif search in ["gen", "general"]:
            return "General" if long else "UKN"
        else:
            return None

    def get_prof_icon(self, search, large=False):
        large = "_LARGE" if large else ""

        search = self.get_profession(search)
        if search == "Adventurer":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_6", "tdb")
        elif search == "Agent":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_5", "tdb")
        elif search == "Bureaucrat":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_8", "tdb")
        elif search == "Doctor":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_10", "tdb")
        elif search == "Enforcer":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_9", "tdb")
        elif search == "Engineer":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_3", "tdb")
        elif search == "Fixer":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_4", "tdb")
        elif search == "Keeper":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_14", "tdb")
        elif search == "Martial Artist":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_2", "tdb")
        elif search == "Meta-Physicist":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_12", "tdb")
        elif search == "Nano-Technician":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_11", "tdb")
        elif search == "Shade":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_15", "tdb")
        elif search == "Soldier":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_1", "tdb")
        elif search == "Trader":
            return self.text.make_image(f"id:GFX_GUI_ICON_PROFESSION{large}_7", "tdb")
        else:
            return ""

    def get_all_professions(self):
        return ["Adventurer", "Agent", "Bureaucrat", "Doctor", "Enforcer", "Engineer", "Fixer", "Keeper",
                "Martial Artist", "Meta-Physicist", "Nano-Technician", "Shade", "Soldier", "Trader"]

    def format_date(self, timestamp):
        value = datetime.datetime.fromtimestamp(timestamp, tz=pytz.UTC)
        return value.strftime('%Y-%m-%d')

    def format_time(self, timestamp):
        value = datetime.datetime.fromtimestamp(timestamp, tz=pytz.UTC)
        return value.strftime('%H:%M:%S')

    def format_datetime(self, timestamp):
        value = datetime.datetime.fromtimestamp(timestamp, tz=pytz.UTC)
        return value.strftime('%Y-%m-%d %H:%M:%S %Z')

    def interpolate_value(self, interpolated_ql, interpolation_ranges, precision=0):
        min_val = None
        max_val = None
        for ql, v in interpolation_ranges.items():
            # required to avoid division by zero
            if ql == interpolated_ql:
                return v

            if interpolated_ql >= ql and (not min_val or ql > min_val.ql):
                min_val = DictObject({"ql": ql, "val": v})

            if interpolated_ql <= ql and (not max_val or ql < max_val.ql):
                max_val = DictObject({"ql": ql, "val": v})

        if not min_val or not max_val:
            return None

        return round((max_val.val - min_val.val) / (max_val.ql - min_val.ql) *
                     (interpolated_ql - min_val.ql) + min_val.val, precision)

    # noinspection PyMethodMayBeStatic
    def get_offset_limit(self, page_size, page_number):
        return (page_number - 1) * page_size, page_size
