# -*- coding: utf-8 -*-
"""
Contains stat calculation and manipulation functions.
"""
import collections
import types

# Description options.
SIZES = ["Tiny", "Small", "Medium", "Large", "Huge"]
ORIGINS = ["aberrant", "elemental", "fey", "immortal", "natural", "shadow"]
TYPES = ["animate", "beast", "humanoid", "magical beast"]
# Role options.
ROLE_MODS = ["Normal", "Elite", "Solo", "Minion", "Super Minion"]
ROLES = ["Artillery", "Brute", "Controller", "Lurker", "Skirmisher", "Soldier"]
# Attribute block stuff.
BEST = 18
GREAT = 16
BETTER = 14
GOOD = 12
NORM = 10
POOR = 8
BEAST_INT = 2  # This value is special. If Int is set to this it never increases with level.
# Sane default level 1 attribute stats for each role. Key is role name (incl. custom archetypes), array order is Str/Con/Dex/Int/Wis/Cha.
DEFAULT_ROLE_STATS = collections.OrderedDict()
DEFAULT_ROLE_STATS["Artillery"] = [GOOD, NORM, BEST, NORM, GOOD, NORM]
DEFAULT_ROLE_STATS["Brute"] = [GREAT, GREAT, NORM, POOR, NORM, POOR]
DEFAULT_ROLE_STATS["Controller"] = [NORM, NORM, BETTER, GREAT, GREAT, GOOD]
DEFAULT_ROLE_STATS["Lurker"] = [GOOD, GOOD, GREAT, NORM, GOOD, NORM]
DEFAULT_ROLE_STATS["Skirmisher"] = [BETTER, GOOD, BETTER, NORM, GOOD, NORM]
DEFAULT_ROLE_STATS["Soldier"] = [GREAT, BETTER, BETTER, NORM, NORM, NORM]
DEFAULT_ROLE_STATS["Bard"] = [NORM, NORM, BETTER, GREAT, GREAT, GREAT]
DEFAULT_ROLE_STATS["Beast"] = [BETTER, GOOD, BETTER, BEAST_INT, NORM, POOR]
DEFAULT_ROLE_STATS["Commoner"] = [NORM, NORM, NORM, NORM, NORM, NORM]
DEFAULT_ROLE_STATS["Mage"] = [POOR, NORM, NORM, BEST, GREAT, GOOD]
DEFAULT_ROLE_STATS["Rogue"] = [NORM, GOOD, BEST, BETTER, GOOD, GOOD]
DEFAULT_ROLE_STATS["Shaman"] = [POOR, NORM, NORM, GREAT, BEST, GOOD]
# Stats (per role).
INIT_VALS = {"Artillery": 0, "Brute": 0, "Controller": 0, "Lurker": 4, "Skirmisher": 2, "Soldier": 2}
AC_VALS = {"Artillery": 12, "Brute": 12, "Controller": 14, "Lurker": 14, "Skirmisher": 14, "Soldier": 16}
DEF_VALS = {"Artillery": 12, "Brute": 12, "Controller": 12, "Lurker": 12, "Skirmisher": 12, "Soldier": 12}
AC_ATK_VALS = {"Artillery": 5, "Brute": 5, "Controller": 5, "Lurker": 5, "Skirmisher": 5, "Soldier": 5}
DEF_ATK_VALS = {"Artillery": 3, "Brute": 3, "Controller": 3, "Lurker": 3, "Skirmisher": 3, "Soldier": 3}
# XP by level, starting with Level 1.
XP = [100, 125, 150, 175, 200, 250, 300, 350, 400, 500, 600, 700, 800, 1000, 1200, 1400, 1600, 2000, 2400, 2800, 3200, 4150, 5100, 6050, 7000, 9000, 11000, 13000, 15000, 19000]
# All attributes (Str/Con/Dex/Int/Wis/Cha) default to 10/+0 if their value is invalid or unknown.
DEFAULT_ATTR_VAL = 10
DEFAULT_SPEED = 6

# Other sets of information.
ALIGNMENTS = ["", "Lawful Good", "Good", "Chaotic Good", "Lawful Neutral", "Neutral", "Unaligned", "Chaotic Neutral", "Lawful Evil", "Evil", "Chaotic Evil"]
# Key: Name of sense. Value: True if it has a range, False otherwise.
SENSES = {"All-Around Vision": False, "Blind": False, "Blindsight": True, "Darkvision": True, "Deaf": False, "Low-Light Vision": False, "Telepathy": True, "Tremorsense": True, "Truesight": False}
SPEEDS = ["Burrow", "Climb", "Fly", "Jump", "Overland Flight", "Swim", "Teleport"]
SPEED_MODS = [[], ["spider climb"], ["clumsy", "hover", "perfect"], ["horizontal", "vertical"], [], [], []]  # Last one is for land speed.
LAND_SPEED_KEY = 255  # Index to use for the normal land speed when looking through speed data structures.
RESIST_VULNS = ["All", "Acid", "Cold", "Fire", "Force", "Forced Movement", "Physical", "Push", "Pull", "Slide", "Healing", "Lightning", "Necrotic", "Poison", "Psychic", "Radiant", "Thunder"]
SWARM_RESIST = "half damage from Melee and Ranged attacks"
SWARM_VULN_TEMPLATE = "{0:s} from Area and Close attacks"
SWARM_DEFAULT_VULN = 10
IMMUNITIES = ["Charm", "Daze", "Disease", "Fear", "Gaze", "Illusion", "Polymorph", "Sleep", "Slow", "Stun", "Weaken"]
RESIST_VULN_IMM = RESIST_VULNS + IMMUNITIES
# Key: Name of skill. Value: Key attribute for the skill.
SKILLS = collections.OrderedDict()
SKILLS["Acrobatics"] = "dex"
SKILLS["Arcana"] = "int"
SKILLS["Athletics"] = "str"
SKILLS["Bluff"] = "cha"
SKILLS["Diplomacy"] = "cha"
SKILLS["Dungeoneering"] = "wis"
SKILLS["Endurance"] = "con"
SKILLS["Heal"] = "wis"
SKILLS["History"] = "int"
SKILLS["Insight"] = "wis"
SKILLS["Intimidate"] = "cha"
SKILLS["Nature"] = "wis"
SKILLS["Perception"] = "wis"
SKILLS["Religion"] = "int"
SKILLS["Stealth"] = "dex"
SKILLS["Streetwise"] = "cha"
SKILLS["Thievery"] = "dex"
# Possible keywords for traits & actions.
KEYWORDS_DAMAGES = ["Acid", "Cold", "Fire", "Force", "Lightning", "Necrotic", "Poison", "Psychic", "Radiant", "Thunder"]
KEYWORDS_SOURCES = ["Arcane", "Divine", "Elemental", "Martial", "Primal", "Psionic", "Shadow"]
KEYWORDS_SPECIAL_EFFECTS = ["Invigorating", "Rattling", "Reliable"]
KEYWORDS_TYPES = ["Aura", "Beast Form", "Charm", "Conjuration", "Disease", "Fear", "Gaze", "Healing", "Illusion", "Implement", "Polymorph", "Rage", "Sleep", "Stance", "Summoning", "Teleportation", "Weapon", "Zone"]

STAT_MOD_CATEGORIES = ["Very Low", "Low", "Normal", "High", "Very High"]

# This gets populated with calculated/suggested values for the monster's stats.
CALC_VALS = types.SimpleNamespace()

# ----------
# Output helping functions.
# ----------


def signed_value(val: int) -> str:
    """
    Checks if a given integer is positive or negative, prepends '+' to a
    string representation of it if non-negative so that it always has a sign.
    """
    return ('+' if val >= 0 else '') + str(val)


# ----------
# Calculation & derivation functions.
# ----------


def calc_base_hp(role: str, lvl: int, role_mod: str, mod: str = "Normal") -> int:
    """
    Calculates base HP based on the monster's role & level.

    :param role: String role of the monster (ROLES).
    :param lvl: Level of the monster.
    :param role_mod: String name of the current role mod (ROLE_MODS).
    :param mod: String modifier of HP stat (STAT_MOD_CATEGORIES).
    :return: Integer base HP.
    """
    if mod == "Very Low":
        hp_mod = .7
    elif mod == "Low":
        hp_mod = .85
    elif mod == "Normal":
        hp_mod = 1.0
    elif mod == "High":
        hp_mod = 1.15
    elif mod == "Very High":
        hp_mod = 1.3
    else:
        hp_mod = 1.0

    if role_mod == "Minion":
        return 1
    elif role_mod == "Super Minion":
        hp_role_mod = 0.33
    elif role_mod == "Elite":
        hp_role_mod = 2.0
    elif role_mod == "Solo":
        hp_role_mod = 4.0
    else:
        hp_role_mod = 1.0

    if role == "Artillery":
        return int((21 + (lvl * 6)) * hp_mod * hp_role_mod)
    elif role == "Brute":
        return int((26 + (lvl * 10)) * hp_mod * hp_role_mod)
    elif role == "Controller":
        return int((24 + (lvl * 8)) * hp_mod * hp_role_mod)
    elif role == "Lurker":
        return int((21 + (lvl * 6)) * hp_mod * hp_role_mod)
    elif role == "Skirmisher":
        return int((24 + (lvl * 8)) * hp_mod * hp_role_mod)
    elif role == "Brute":
        return int((24 + (lvl * 8)) * hp_mod * hp_role_mod)
    elif role == "Soldier":
        return int((24 + (lvl * 8)) * hp_mod * hp_role_mod)
    else:
        return 1


def _get_def_mod(mod: str) -> int:
    """
    Calculates modifier value for a defense.

    :param mod: String modifier of a defense stat (STAT_MOD_CATEGORIES).
    :return: Integer adjustment to apply to the stat.
    """
    if mod == "Very Low":
        def_mod = -6
    elif mod == "Low":
        def_mod = -3
    elif mod == "Normal":
        def_mod = 0
    elif mod == "High":
        def_mod = 3
    elif mod == "Very High":
        def_mod = 6
    else:
        def_mod = 0

    return def_mod


def calc_ac(role: str, lvl: int, mod: str = "Normal") -> int:
    """
    Calculates default AC based on the monster's role & level.

    :param role: String role of the monster (ROLES).
    :param lvl: Level of the monster.
    :param mod: String modifier of AC stat (STAT_MOD_CATEGORIES).
    :return: Integer AC.
    """
    def_mod = _get_def_mod(mod)

    return int(lvl + AC_VALS[role] + def_mod)


def calc_fort(role: str, lvl: int, mod: str = "Normal") -> int:
    """
    Calculates default Fortitude defense based on the monster's role & level.

    :param role: String role of the monster (ROLES).
    :param lvl: Level of the monster.
    :param mod: String modifier of Fort stat (STAT_MOD_CATEGORIES).
    :return: Integer Fort defense.
    """
    def_mod = _get_def_mod(mod)

    return int(lvl + DEF_VALS[role] + def_mod)


def calc_ref(role: str, lvl: int, mod: str = "Normal") -> int:
    """
    Calculates default Reflex defense based on the monster's role & level.

    :param role: String role of the monster (ROLES).
    :param lvl: Level of the monster.
    :param mod: String modifier of Ref stat (STAT_MOD_CATEGORIES).
    :return: Integer Ref defense.
    """
    def_mod = _get_def_mod(mod)

    return int(lvl + DEF_VALS[role] + def_mod)


def calc_will(role: str, lvl: int, mod: str = "Normal") -> int:
    """
    Calculates default Will defense based on the monster's role & level.

    :param role: String role of the monster (ROLES).
    :param lvl: Level of the monster.
    :param mod: String modifier of Will stat (STAT_MOD_CATEGORIES).
    :return: Integer Will defense.
    """
    def_mod = _get_def_mod(mod)

    return int(lvl + DEF_VALS[role] + def_mod)


def calc_attribute_mod(attr: int, lvl: int = 0) -> int:
    """
    Calculates the modifier of an attribute, including the "+1/2 level" bit.
    Use 0 for lvl to just get base modifier.

    :param attr: Integer attribute value.
    :param lvl: Integer level.
    :return: Integer attribute modifier.
    """
    return int(((attr - 10) // 2) + (lvl // 2))


def calc_init(lvl: int, role: str, dex: int, mod: str = "Normal") -> int:
    """
    Calculates Initiative mod based on level, role, & Dex.

    :param lvl: Integer level.
    :param role: String role of the monster (ROLES).
    :param dex: Integer Dexterity attribute.
    :param mod: String modifier of Init stat (STAT_MOD_CATEGORIES).
    :return: Integer Initiative modifier.
    """
    if mod == "Very Low":
        init_mod = -6
    elif mod == "Low":
        init_mod = -3
    elif mod == "Normal":
        init_mod = 0
    elif mod == "High":
        init_mod = 3
    elif mod == "Very High":
        init_mod = 6
    else:
        init_mod = 0

    return int(INIT_VALS[role] + calc_attribute_mod(dex, lvl) + init_mod)


def calc_xp(lvl: int, role_mod: str) -> int:
    """
    Calculates normal monster XP based on current level & role mod.

    :param lvl: Integer level.
    :param role_mod: String name of the current role mod (ROLE_MODS).
    :return: Integer XP value.
    """
    xp = XP[lvl - 1]
    if role_mod == "Elite":
        xp *= 2
    elif role_mod == "Solo":
        xp *= 4
    elif role_mod == "Minion":
        if lvl < 11:
            xp //= 4
        elif lvl < 21:
            xp //= 5
        else:
            xp //= 6
    elif role_mod == "Super Minion":
        if lvl < 11:
            xp //= 3
        elif lvl < 21:
            xp //= 4
        else:
            xp //= 5

    return xp


def _calc_attribute_score(base_attr: int, lvl: int, role_mod: str) -> int:
    """
    Calculates level-appropriate score for an attribute.
    If the base attribute is GREAT or higher (i.e., likely a "primary attribute"), adds 1 per 4 levels.
    If it's BETTER (i.e., likely a "secondary attribute"), adds 1 per 8 levels.
    Otherwise adds 1 to the attribute per post-Heroic tier.
    Elite monsters get +2 to everything, Solos get +4 to everything.

    :param base_attr: Integer base attribute score.
    :param lvl: Integer level.
    :param role_mod: String name of the current role mod (ROLE_MODS).
    :return: Integer level-adjusted attribute score.
    """
    if base_attr >= GREAT:
        mod = lvl // 4
    elif base_attr >= BETTER:
        mod = lvl // 8
    else:
        mod = lvl // 10
    if role_mod == "Elite":
        mod += 2
    elif role_mod == "Solo":
        mod += 4
    return base_attr + mod


def calc_str(lvl: int, attribute_role: str, role_mod: str) -> int:
    """
    Calculates level-appropriate Strength for a monster based on level and role.

    :param lvl: Integer level.
    :param attribute_role: String name of the monster's attribute role (key of DEFAULT_ROLE_STATS).
    :param role_mod: String name of the current role mod (ROLE_MODS).
    :return: Integer Strength score.
    """
    return _calc_attribute_score(DEFAULT_ROLE_STATS[attribute_role][0], lvl, role_mod)


def calc_con(lvl: int, attribute_role: str, role_mod: str) -> int:
    """
    Calculates level-appropriate Constitution for a monster based on level and role.

    :param lvl: Integer level.
    :param attribute_role: String name of the monster's attribute role (key of DEFAULT_ROLE_STATS).
    :param role_mod: String name of the current role mod (ROLE_MODS).
    :return: Integer Constitution score.
    """
    return _calc_attribute_score(DEFAULT_ROLE_STATS[attribute_role][1], lvl, role_mod)


def calc_dex(lvl: int, attribute_role: str, role_mod: str) -> int:
    """
    Calculates level-appropriate Dexterity for a monster based on level and role.

    :param lvl: Integer level.
    :param attribute_role: String name of the monster's attribute role (key of DEFAULT_ROLE_STATS).
    :param role_mod: String name of the current role mod (ROLE_MODS).
    :return: Integer Dexterity score.
    """
    return _calc_attribute_score(DEFAULT_ROLE_STATS[attribute_role][2], lvl, role_mod)


def calc_int(lvl: int, attribute_role: str, role_mod: str) -> int:
    """
    Calculates level-appropriate Intelligence for a monster based on level and role.
    If Int is set to BEAST_INT or lower it remains locked there regardless of level or other mods.

    :param lvl: Integer level.
    :param attribute_role: String name of the monster's attribute role (key of DEFAULT_ROLE_STATS).
    :param role_mod: String name of the current role mod (ROLE_MODS).
    :return: Integer Intelligence score.
    """
    if DEFAULT_ROLE_STATS[attribute_role][3] <= BEAST_INT:
        return DEFAULT_ROLE_STATS[attribute_role][3]
    else:
        return _calc_attribute_score(DEFAULT_ROLE_STATS[attribute_role][3], lvl, role_mod)


def calc_wis(lvl: int, attribute_role: str, role_mod: str) -> int:
    """
    Calculates level-appropriate Wisdom for a monster based on level and role.

    :param lvl: Integer level.
    :param attribute_role: String name of the monster's attribute role (key of DEFAULT_ROLE_STATS).
    :param role_mod: String name of the current role mod (ROLE_MODS).
    :return: Integer Wisdom score.
    """
    return _calc_attribute_score(DEFAULT_ROLE_STATS[attribute_role][4], lvl, role_mod)


def calc_cha(lvl: int, attribute_role: str, role_mod: str) -> int:
    """
    Calculates level-appropriate Charisma for a monster based on level and role.

    :param lvl: Integer level.
    :param attribute_role: String name of the monster's attribute role (key of DEFAULT_ROLE_STATS).
    :param role_mod: String name of the current role mod (ROLE_MODS).
    :return: Integer Charisma score.
    """
    return _calc_attribute_score(DEFAULT_ROLE_STATS[attribute_role][5], lvl, role_mod)


# noinspection PyRedundantParentheses
def is_recharge_numeric(recharge_str: str) -> tuple:
    """
    Checks if recharge frequency is a reasonable subset of d6 faces.

    :param recharge_str: The raw string the user entered for recharge frequency.
    :return: Tuple. First element True/False if it's a set of d6 faces. Second element either array of faces or raw text if can't be parsed.
    """
    is_die_face = False
    die_faces = set()
    tempstr = recharge_str.strip().lower()

    # First just try a single number.
    try:
        num = int(tempstr)
        if 0 < num <= 6:
            die_faces.add(num)
            is_die_face = True
    except ValueError:
        pass
    if is_die_face:
        return (True, sorted(list(die_faces)))

    # Next split on commas, look for ranges in there ("4-6"), and try to put a set of numbers together.
    # E.g., "1,4-6"
    comma_groups = [s.strip() for s in tempstr.split(',')]
    for comma_group in comma_groups:
        range_pair = [s.strip() for s in comma_group.split('-')]
        is_range = False
        if len(range_pair) == 2:
            try:
                low_num = int(range_pair[0])
                high_num = int(range_pair[1])
                if low_num < high_num and 0 < low_num <= 6 and 0 < high_num <= 6:
                    for die in range(low_num, high_num + 1):
                        die_faces.add(die)
                    is_range = True
            except ValueError:
                pass
            if not is_range:
                # A range-like string was found, but it's not valid, so give up parsing.
                return (False, recharge_str)
        else:
            # If it isn't a range, try seeing if it's just a single valid number.
            try:
                is_die_face = False
                num = int(comma_group)
                if 0 < num <= 6:
                    die_faces.add(num)
                    is_die_face = True
            except ValueError:
                pass
            if not is_die_face:
                # This isn't a numeric range or a normal number, give up parsing.
                return (False, recharge_str)

    # Check that we got at least one number, either return a sorted list of them or the original string otherwise.
    if len(die_faces) > 0:
        return (True, sorted(list(die_faces)))
    else:
        return (False, recharge_str)
