import copy

__all__ = ['StaffChoices', 'OrderedChoices']


def parse_opt(key, val):
    """
    Normalize an option passed to choices
    """
    if isinstance(val, (tuple, list)):
        # OPT = val, desription
        assert len(val) == 2
        val, desc = val
    else:
        # OPT = val
        desc = val
    return Option(key, val, desc)


# Option = namedtuple('Option', 'name value desc')
# 2.5 =(

class Option:
    def __init__(self, name, value, desc):
        self.name = name
        self.value = value
        self.desc = desc


class ChoicesMeta(type):

    def __new__(mcs, cls_name, bases, declared_members):
        cls__dict__ = {}

        options = []
        for name, value in declared_members.items():
            # Ignore built-ins and in general anything that starts with '_'
            if name.startswith('_'):
                cls__dict__[name] = value
                continue

            opt = parse_opt(name, value)
            # Options dict
            options.append(opt)

            cls__dict__[name] = opt.value

        cls__dict__['_options'] = options

        cls = type.__new__(mcs, cls_name, bases, cls__dict__)
        return cls

    def choices(cls, include_empty=False, empty_label='--------', select=()):
        _choices = []
        if include_empty:
            _choices.append(('', empty_label))
        if select:
            options = [cls._get_opt(name) for name in select]
        else:
            options = cls._options
        _choices.extend((opt.value, opt.desc) for opt in options)
        return _choices

    def _get_opt(cls, name):
        """ Return full option object by name """
        return next(x for x in cls._options if x.name == name)

    def get_key(cls, value):
        """ Return value by description """
        try:
            return next(x.value for x in cls._options if x.desc == value)
        except StopIteration:
            raise KeyError(value)

    def get_name(cls, key):
        """ Return short name by value """
        try:
            return next(x.name for x in cls._options if x.value == key)
        except StopIteration:
            raise KeyError(key)

    def __getitem__(cls, value):
        """ Return description (aka long name) by value """
        try:
            return next(x.desc for x in cls._options if x.value == value)
        except StopIteration:
            raise KeyError(value)

    def __call__(cls,  **kwargs):
        return ChoicesMeta('Choices', (object,), kwargs)


class StaffChoices(metaclass=ChoicesMeta):
    """
    >>> # Declarative style
    >>> class STONE(StaffChoices):
    ...     LEFT = 'eye'
    ...     RIGHT = 'horse'
    ...     AHEAD = 'die', 'You die'
    >>> STONE.LEFT
    'eye'
    >>> STONE.AHEAD
    'die'
    >>> sorted(STONE.choices())
    [('die', 'You die'), ('eye', 'eye'), ('horse', 'horse')]
    >>> STONE.choices(select=['LEFT', 'AHEAD'])
    [('eye', 'eye'), ('die', 'You die')]
    >>> STONE.get_key('You die')
    'die'
    >>> STONE.get_key('horse')
    'horse'
    >>> STONE.get_name('eye')
    'LEFT'
    >>> STONE.get_name('die')
    'AHEAD'
    >>> # Functional style
    >>> MARITAL_STATUS = StaffChoices(
    ...     FREE=(1, u'Free'),
    ...     MARRIED=(2, u'Marred'),
    ... )
    >>> MARITAL_STATUS.FREE
    1
    >>> MARITAL_STATUS.MARRIED
    2
    >>> sorted(MARITAL_STATUS.choices())
    [(1, u'Free'), (2, u'Marred')]
    >>> MARITAL_STATUS[1]
    u'Free'
    >>> MARITAL_STATUS.get_key(u'Free')
    1
    >>> MARITAL_STATUS.get_key("Abyrvalg")
    Traceback (most recent call last):
    ...
    KeyError: 'Abyrvalg'
    >>> MARITAL_STATUS.get_name(1)
    'FREE'
    >>> MARITAL_STATUS.get_name(12345)
    Traceback (most recent call last):
    ...
    KeyError: 12345
    >>> CHOICES_WITH_SHORTCUTS = StaffChoices(
    ...     FREE='free',
    ...     MARRIED=['mar', 'married'],
    ... )
    >>> CHOICES_WITH_SHORTCUTS.FREE
    'free'
    >>> CHOICES_WITH_SHORTCUTS.MARRIED
    'mar'
    >>> CHOICES_WITH_SHORTCUTS['free']
    'free'
    >>> CHOICES_WITH_SHORTCUTS['mar']
    'married'
    >>> CHOICES_WITH_SHORTCUTS['MARRIED']
    Traceback (most recent call last):
    ...
    KeyError: 'MARRIED'
    >>> CHOICES_WITH_SHORTCUTS.choices()
    [('mar', 'married'), ('free', 'free')]
    """


class OrderedChoices:
    """
    Велосипедные упорядоченные Choices.

    Чем лучше конкурентов:

         - django_intranet_stuff.utils.choices.Choices:

             1. Не сортированный, а наш OrderedChoices сортированный.
               Сохраняет порядок пунктов в аргументах при итерировании.

             2. Использует __getattribute__, хотя хватило бы и __getattr__,
               да, к тому же, криво, из-за чего не дружит с deepcopy,
               уходя в бесконечную рекурсию.

        - model_utils:

            1. Не дружит с deepcopy, уходя в бесконечную рекурсию.

    В этой реализации вышеперечисленные недостатки отсутствуют.

    Как пользоваться:
    >>> _ = lambda x: x  # gettext-заглушка
    >>> MARITAL_STATUS = OrderedChoices(
    ...     ('FREE', 1, _('Free')),
    ...     ('MARRIED', 2, _('Marred')),
    ...     ('FREE_BUT_NOT', 4, _('Free_but_married')),
    ...     ('MARRIED_BUT_IT_MEANS_NOTHING', 3, _('Marred_but_free')),
    ... )
    >>> MARITAL_STATUS.FREE
    1
    >>> MARITAL_STATUS.MARRIED
    2
    >>> MARITAL_STATUS.MARRIED_BUT_IT_MEANS_NOTHING
    3
    >>> MARITAL_STATUS.FREE_BUT_NOT
    4
    >>> sorted(MARITAL_STATUS)
    [(1, u'Free'), (2, u'Marred'), (3, u'Marred_but_free'), (4, u'Free_but_married')]
    >>> list(MARITAL_STATUS)
    [(1, u'Free'), (2, u'Marred'), (4, u'Free_but_married'), (3, u'Marred_but_free')]
    >>> MARITAL_STATUS[1]
    u'Free'
    >>> MARITAL_STATUS.as_dict()
    {'FREE': 1, 'MARRIED': 2, 'FREE_BUT_NOT': 4, 'MARRIED_BUT_IT_MEANS_NOTHING': 3}
    >>> CHOICES_WITH_SHORTCUTS = OrderedChoices(
    ...     ('FREE', 'free'),
    ...     ('MARRIED', 'mar', 'married'),
    ...     ('FREE_BUT_NOT', 'free_but_not'),
    ...     ('MARRIED_BUT_IT_MEANS_NOTHING', 'mnothing', 'married_but_free'),
    ...     'misanthrope',
    ... )
    >>> CHOICES_WITH_SHORTCUTS.FREE
    'free'
    >>> CHOICES_WITH_SHORTCUTS.MARRIED
    'mar'
    >>> list(CHOICES_WITH_SHORTCUTS)
    [
        ('free', 'free'),
        ('mar', 'married'),
        ('free_but_not', 'free_but_not'),
        ('mnothing', 'married_but_free'),
        ('misanthrope', 'misanthrope'),
    ]
    """
    def __init__(self, *choices):
        """

        :rtype:
        """
        self._choices = choices
        self._db_values = {}
        self._text_values = {}
        self._pairs = []

        for attr, db_value, text_value in self._equalize(choices):
            self._db_values[attr] = db_value
            self._text_values[db_value] = text_value
            self._text_values[attr] = text_value
            self._pairs.append((db_value, text_value))

    def __getattr__(self, item):
        if item in self._db_values:
            return self._db_values[item]
        else:
            raise AttributeError(item)

    def __getitem__(self, item):
        return self._text_values[item]

    def __iter__(self):
        return iter(self._pairs)

    def __repr__(self):
        return ("{name}({choices})"
                .format(name=self.__class__.__name__,
                        choices=', '.join(str(c) for c in self._choices)))

    def __deepcopy__(self, memo):
        return self.__class__(*copy.deepcopy(self._choices, memo))

    def __contains__(self, item):
        return item in self._db_values.values()

    def _equalize(self, choices):
        for items in choices:
            if isinstance(items, (list, tuple)):
                assert len(items) in range(1, 4), 'RTFM!'
                if len(items) == 1:
                    attr = db = text = items[0]
                elif len(items) == 2:
                    attr, db, text = items[0], items[1], items[1]
                elif len(items) == 3:
                    attr, db, text = items
            else:
                attr = db = text = items

            yield attr, db, text

    def _select_by_name(self, names):
        return [(self._db_values[name], self._text_values[name])
                for name in names]

    def choices(self, include_empty=False, empty_label='--------', select=()):
        _choices = []
        if include_empty:
            _choices.append(('', empty_label))
        if select:
            options = self._select_by_name(select)
        else:
            options = list(self)

        return _choices + options

    def get_key(self, value):
        try:
            return next(x[0] for x in self._pairs if x[1] == value)
        except StopIteration:
            raise KeyError(value)

    def get_name(self, key):
        db_value = key
        try:
            return next(
                attr for attr, _db_value
                in self._db_values.items() if _db_value == db_value
            )
        except StopIteration:
            raise KeyError(db_value)

    def as_dict(self) -> dict:
        return {
            x[0]: x[1]
            for x
            in self._equalize(self._choices)
        }


if __name__ == "__main__":
    import doctest
    doctest.testmod()
