import math
from collections import namedtuple
from math import ceil

from dungeonsheets.armor import Armor, HeavyArmor, NoArmor, NoShield, Shield
from dungeonsheets.features import (
    AmbushMaster,
    Defense,
    DraconicResilience,
    DreadAmbusher,
    FastMovement,
    FeralInstinct,
    GiftOfTheDepths,
    JackOfAllTrades,
    NaturalArmor,
    NaturalExplorerRevised,
    QuickDraw,
    RakishAudacity,
    RemarkableAthelete,
    SeaSoul,
    SoulOfTheForge,
    SuperiorMobility,
    UnarmoredDefenseBarbarian,
    UnarmoredDefenseMonk,
    UnarmoredMovement,
)
from dungeonsheets.weapons import Weapon


def findattr(obj, name):
    """Similar to builtin getattr(obj, name) but more forgiving to
    whitespace and capitalization.

    """
    # Come up with several options
    name = name.strip()
    # check for +X weapons, armor, shields
    bonus = 0
    for i in range(1, 11):
        if (f"+{i}" in name) or (f"+ {i}" in name):
            bonus = i
            name = name.replace(f"+{i}", "").replace(f"+ {i}", "")
            break
    py_name = name.replace("-", "_").replace(" ", "_").replace("'", "")
    camel_case = "".join([s.capitalize() for s in py_name.split("_")])
    if hasattr(obj, py_name):
        # Direct lookup
        attr = getattr(obj, py_name)
    elif hasattr(obj, camel_case):
        # CamelCase lookup
        attr = getattr(obj, camel_case)
    else:
        raise AttributeError(f"{obj} has no attribute {name}")
    if bonus > 0:
        if (
            issubclass(attr, Weapon)
            or issubclass(attr, Shield)
            or issubclass(attr, Armor)
        ):
            attr = attr.improved_version(bonus)
    return attr


def mod_str(modifier):
    """Converts a modifier to a string, eg 2 -> '+2'."""
    return "{:+d}".format(modifier)
    if modifier == 0:
        return str(modifier)
    else:
        return "{:+}".format(modifier)


AbilityScore = namedtuple("AbilityScore", ("value", "modifier", "saving_throw"))


class Ability:
    ability_name = None

    def __init__(self, default_value=10):
        self.default_value = default_value

    def __set_name__(self, character, name):
        self.ability_name = name

    def _check_dict(self, obj):
        if not hasattr(obj, "_ability_scores"):
            # No ability score dictionary exists
            obj._ability_scores = {self.ability_name: self.default_value}
        elif self.ability_name not in obj._ability_scores.keys():
            # ability score dictionary exists but doesn't have this ability
            obj._ability_scores[self.ability_name] = self.default_value

    def __get__(self, character, Character):
        self._check_dict(character)
        score = character._ability_scores[self.ability_name]
        modifier = math.floor((score - 10) / 2)
        # Check for proficiency
        saving_throw = modifier
        if self.ability_name is not None and hasattr(
            character, "saving_throw_proficiencies"
        ):
            is_proficient = self.ability_name in character.saving_throw_proficiencies
            if is_proficient:
                saving_throw += character.proficiency_bonus
        # Create the named tuple
        value = AbilityScore(modifier=modifier, value=score, saving_throw=saving_throw)
        return value

    def __set__(self, character, val):
        self._check_dict(character)
        character._ability_scores[self.ability_name] = val
        self.value = val


class Skill:
    """An ability-based skill, such as athletics."""

    def __init__(self, ability):
        self.ability_name = ability

    def __set_name__(self, character, name):
        self.skill_name = name.lower().replace("_", " ")
        self.character = character

    def __get__(self, character, owner):
        ability = getattr(character, self.ability_name)
        modifier = ability.modifier
        # Check for proficiency
        is_proficient = self.skill_name in character.skill_proficiencies
        if is_proficient:
            modifier += character.proficiency_bonus
        elif character.has_feature(JackOfAllTrades):
            modifier += character.proficiency_bonus // 2
        elif character.has_feature(RemarkableAthelete):
            if self.ability_name.lower() in ("strength", "dexterity", "constitution"):
                modifier += ceil(character.proficienc_bonus / 2.0)

        # Check for expertise
        is_expert = self.skill_name in character.skill_expertise
        if is_expert:
            modifier += character.proficiency_bonus
        return modifier


class ArmorClass:
    """
    The Armor Class of a character
    """

    def __get__(self, char, Character):
        armor = char.armor or NoArmor()
        ac = armor.base_armor_class
        # calculate and apply modifiers
        if armor.dexterity_mod_max is None:
            ac += char.dexterity.modifier
        else:
            ac += min(char.dexterity.modifier, armor.dexterity_mod_max)
        if char.has_feature(NaturalArmor):
            ac = max(ac, 13 + char.dexterity.modifier)
        shield = char.shield or NoShield()
        ac += shield.base_armor_class
        # Compute feature-specific additions
        if char.has_feature(UnarmoredDefenseMonk):
            if isinstance(armor, NoArmor) and isinstance(shield, NoShield):
                ac += char.wisdom.modifier
        if char.has_feature(UnarmoredDefenseBarbarian):
            if isinstance(armor, NoArmor):
                ac += char.constitution.modifier
        if char.has_feature(DraconicResilience):
            if isinstance(armor, NoArmor):
                ac += 3
        if char.has_feature(Defense):
            if not isinstance(armor, NoArmor):
                ac += 1
        if char.has_feature(SoulOfTheForge):
            if isinstance(armor, HeavyArmor):
                ac += 1
        # Check if any magic items add to AC
        for mitem in char.magic_items:
            if hasattr(mitem, "ac_bonus"):
                ac += mitem.ac_bonus
        return ac


class Speed:
    """
    The speed of a character
    """

    def __get__(self, char, Character):
        speed = char.race.speed
        other_speed = ""
        if isinstance(speed, str):
            other_speed = speed[2:]
            speed = int(speed[:2])  # ignore other speeds, like fly
        if char.has_feature(FastMovement):
            if not isinstance(char.armor, HeavyArmor):
                speed += 10
        if char.has_feature(SuperiorMobility):
            speed += 10
        if isinstance(char.armor, NoArmor) or (char.armor is None):
            for f in char.features:
                if isinstance(f, UnarmoredMovement):
                    speed += f.speed_bonus
        if char.has_feature(GiftOfTheDepths):
            if "swim" not in other_speed:
                other_speed += " ({:d} swim)".format(speed)
        if char.has_feature(SeaSoul):
            if "swim" not in other_speed:
                other_speed += " (30 swim)"
        return "{:d}{:s}".format(speed, other_speed)


class Initiative:
    """A character's initiative"""

    def __get__(self, char, Character):
        ini = char.dexterity.modifier
        if char.has_feature(QuickDraw):
            ini += char.proficiency_bonus
        if char.has_feature(DreadAmbusher):
            ini += char.wisdom.modifier
        if char.has_feature(RakishAudacity):
            ini += char.charisma.modifier
        ini = "{:+d}".format(ini)
        has_advantage = (
            char.has_feature(NaturalExplorerRevised)
            or char.has_feature(FeralInstinct)
            or char.has_feature(AmbushMaster)
        )
        if has_advantage:
            ini += "(A)"
        return ini
