import random

class Die:
    @staticmethod
    def d4():
        return random.randint(1, 4)

    @staticmethod
    def d6():
        return random.randint(1, 6)

    @staticmethod
    def d8():
        return random.randint(1, 8)

    @staticmethod
    def d10():
        return random.randint(1, 10)

    @staticmethod
    def d12():
        return random.randint(1, 12)

    @staticmethod
    def d20():
        return random.randint(1, 20)


class Data(object):
    @classmethod
    def from_json(cls, obj):
        if hasattr(cls, 'FIELDS'):
            values = {}
            for name, typ in cls.FIELDS:
                values[name] = typ.from_json(obj[name])
            return cls(**values)
        else:
            raise AttributeError(
                'Class {0} has no FIELDS attribute'.format(cls.__name__))

    @classmethod
    def ensure(cls, name, obj):
        if isinstance(obj, cls):
            return obj
        else:
            raise TypeError(('Unexpected type for field {0}: '
                             'expected {1}, got {2}').format(
                                 name, type(cls), type(obj)))

    def __repr__(self):
        return '{0}({1})'.format(
            self.__class__.__name__,
            ', '.join('{0}={1}'.format(k, repr(self.__dict__[k]))
                      for k, _ in self.__class__.FIELDS))

    def __str__(self):
        return '{0}({1})'.format(
            self.__class__.__name__,
            ', '.join('{0}={1}'.format(k, str(self.__dict__[k]))
                      for k, _ in self.__class__.FIELDS))

    def __init__(self, **kwargs):
        for field, typ in self.__class__.FIELDS:
            self.__dict__[field] = typ.ensure(field, kwargs[field])

class Stat(Data):
    STATS = [
        'strength',
        'dexterity',
        'constitution',
        'intelligence',
        'wisdom',
        'charisma',
        'sanity'
    ]

    @staticmethod
    def from_json(val):
        return Stat(val)

    @staticmethod
    def random():
        sum(sorted([Die.d6(), Die.d6(), Die.d6(), Die.d6()][1:]))

    def __init__(self, score):
        self.score = score

    def modifier(self):
        return (self.score // 2) - 5

    def __repr__(self):
        return 'Stat({0})'.format(self.score)

    def __str__(self):
        return str(self.score)

def mk_block(name, fields):
    class Block(Data):
        FIELDS = fields
    Block.__name__ = name
    return Block

StatBlock = mk_block('StatBlock', [(s, Stat) for s in Stat.STATS])

class Value(Data):
    @staticmethod
    def from_json(obj):
        if (isinstance(obj, str) or
            isinstance(obj, int) or
            isinstance(obj, float)):
            return Value(obj)
        elif isinstance(obj, dict):
            if len(obj) == 1:
                k, v = obj.popitem()
                if isinstance(v, dict):
                    return Value(k, **v)
                else:
                    return Value(str(k) + ': ' + str(v))
            else:
                raise Exception('???')
        elif isinstance(obj, list):
            return [ Value.from_json(elem) for elem in obj ]

    @staticmethod
    def ensure(n, obj):
        if isinstance(obj, Value):
            return obj
        elif isinstance(obj, list):
            return [ Value.ensure(n, elem) for elem in obj ]
        else:
            return Value(obj)

    def __init__(self, datum, **tags):
        self.datum = datum
        self.tags = tags

    def __repr__(self):
        if not self.tags:
            return 'Value({0})'.format(repr(self.datum))
        else:
            return 'Value({0}, {1})'.format(
                repr(self.datum),
                ', '.join('{0}={1}'.format(k, repr(v))
                          for k, v in self.tags.items()))

    def __str__(self):
        if not self.tags:
            return str(self.datum)
        else:
            return '{0} ({1})'.format(
                str(self.datum),
                ', '.join('{0}: {1}'.format(k, str(v))
                          for k, v in self.tags.items()))

class Character(Data):
    FIELDS = [
        ('name', Value),
        ('class', Value),
        ('level', Value),
        ('background', Value),
        ('race', Value),
        ('alignment', Value),
        ('stats', StatBlock),
        ('speed', Value),
        ('size', Value),
        ('proficiency bonus', Value),
        ('hit die', Value),
        ('hit point maximum', Value),
        ('ac', Value),
        ('personality', Value),
        ('ideals', Value),
        ('bonds', Value),
        ('flaw', Value),
        ('saving throws', Value),
        ('languages', Value),
        ('proficiencies', Value),
        ('skills', Value),
        ('spell slots', Value),
        ('spells', Value),
        ('equipment', Value),
        ('money', Value),
        ('features & traits', Value),
    ]
