from __future__ import absolute_import

import sys
from collections import abc
from collections.abc import Iterable, Set

from cpython.object cimport PyObject_Str, Py_NE, PyObject_RichCompare

from ._abc import MultiMapping, MutableMultiMapping
from ._istr import istr

from ._multidict_iter cimport *
from ._pair_list cimport *

cdef object _marker = object()

upstr = istr  # for relaxing backward compatibility problems
cdef object _istr = istr

pair_list_init(istr)
multidict_iter_init()


def getversion(_Base md):
    return pair_list_version(md._impl)


cdef class _Base:

    cdef object _impl

    def getall(self, key, default=_marker):
        """Return a list of all values matching the key."""
        try:
            return pair_list_get_all(self._impl, key)
        except KeyError:
            if default is not _marker:
                return default
            else:
                raise

    def getone(self, key, default=_marker):
        """Get first value matching the key."""
        return self._getone(key, default)

    cdef _getone(self, key, default):
        try:
            return pair_list_get_one(self._impl, key)
        except KeyError:
            if default is not _marker:
                return default
            else:
                raise

    # Mapping interface #

    def __getitem__(self, key):
        return self._getone(key, _marker)

    def get(self, key, default=None):
        """Get first value matching the key.

        The method is alias for .getone().
        """
        return self._getone(key, default)

    def __contains__(self, key):
        return self._contains(key)

    cdef _contains(self, key):
        return pair_list_contains(self._impl, key)

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

    def __len__(self):
        return pair_list_len(self._impl)

    cpdef keys(self):
        """Return a new view of the dictionary's keys."""
        return _KeysView.__new__(_KeysView, self)

    def items(self):
        """Return a new view of the dictionary's items *(key, value) pairs)."""
        return _ItemsView.__new__(_ItemsView, self)

    def values(self):
        """Return a new view of the dictionary's values."""
        return _ValuesView.__new__(_ValuesView, self)

    def __repr__(self):
        lst = []
        for k, v in self.items():
            lst.append("'{}': {!r}".format(k, v))
        body = ', '.join(lst)
        return '<{}({})>'.format(self.__class__.__name__, body)

    def __eq__(self, arg):
        cdef Py_ssize_t pos1
        cdef PyObject *identity1
        cdef PyObject *value1
        cdef Py_hash_t h1

        cdef Py_ssize_t pos2
        cdef PyObject *identity2
        cdef PyObject *value2
        cdef Py_hash_t h2

        cdef _Base other

        if isinstance(arg, _Base):
            other = <_Base>arg
            if pair_list_len(self._impl) != pair_list_len(other._impl):
                return False
            pos1 = pos2 = 0
            while (_pair_list_next(self._impl, &pos1, &identity1,
                                   NULL, &value1, &h1) and
                   _pair_list_next(other._impl, &pos2, &identity2,
                                   NULL, &value2, &h2)):
                if h1 != h2:
                    return False
                if PyObject_RichCompare(<object>identity1, <object>identity2, Py_NE):
                    return False
                if PyObject_RichCompare(<object>value1, <object>value2, Py_NE):
                    return False
            return True
        elif isinstance(arg, abc.Mapping):
            return bool(pair_list_eq_to_mapping(self._impl, arg))
        else:
            return NotImplemented


cdef class MultiDictProxy(_Base):
    _proxy_classes = (MultiDict, MultiDictProxy)
    _base_class = MultiDict

    def __init__(self, arg):
        cdef _Base base
        if not isinstance(arg, self._proxy_classes):
            raise TypeError(
                'ctor requires {} instance'
                ', not {}'.format(
                    ' or '.join(self._proxy_classes),
                    type(arg)))

        base = arg
        self._impl = base._impl

    def __reduce__(self):
        raise TypeError("can't pickle {} objects"
                        .format(self.__class__.__name__))

    def copy(self):
        """Return a copy of itself."""
        return self._base_class(self)

MultiMapping.register(MultiDictProxy)


cdef class CIMultiDictProxy(MultiDictProxy):
    _proxy_classes = (CIMultiDict, CIMultiDictProxy)
    _base_class = CIMultiDict


MultiMapping.register(CIMultiDictProxy)


cdef str _str(key):
    typ = type(key)
    if typ is str:
        return <str>key
    if typ is _istr:
        return PyObject_Str(key)
    elif issubclass(typ, str):
        return str(key)
    else:
        raise TypeError("MultiDict keys should be either str "
                        "or subclasses of str")


cdef class MultiDict(_Base):
    """An ordered dictionary that can have multiple values for each key."""

    def __init__(self, *args, **kwargs):
        self._impl = pair_list_new()
        self._extend(args, kwargs, 'MultiDict', True)

    def __reduce__(self):
        return (
            self.__class__,
            (list(self.items()),)
        )

    cdef _extend(self, tuple args, dict kwargs, name, bint do_add):
        cdef object key
        cdef object value
        cdef object arg
        cdef object i

        if len(args) > 1:
            raise TypeError("{} takes at most 1 positional argument"
                            " ({} given)".format(name, len(args)))

        if args:
            arg = args[0]
            if isinstance(arg, _Base) and not kwargs:
                if do_add:
                    self._append_items((<_Base>arg)._impl)
                else:
                    self._update_items((<_Base>arg)._impl)
            else:
                if hasattr(arg, 'items'):
                    arg = arg.items()
                if kwargs:
                    arg = list(arg)
                    arg.extend(list(kwargs.items()))
                if do_add:
                    self._append_items_seq(arg, name)
                else:
                    pair_list_update_from_seq(self._impl, arg)
        else:
            arg = list(kwargs.items())
            if do_add:
                self._append_items_seq(arg, name)
            else:
                pair_list_update_from_seq(self._impl, arg)

    cdef object _update_items(self, object impl):
        pair_list_update(self._impl, impl)

    cdef object _append_items(self, object impl):
        cdef PyObject *key
        cdef PyObject *val
        cdef Py_ssize_t pos
        pos = 0
        while _pair_list_next(impl, &pos, NULL, &key, &val, NULL):
            self._add(<object>key, <object>val)

    cdef object _append_items_seq(self, object arg, object name):
        cdef object i
        cdef object key
        cdef object value
        for i in arg:
            if not len(i) == 2:
                raise TypeError(
                    "{} takes either dict or list of (key, value) "
                    "tuples".format(name))
            key = i[0]
            value = i[1]
            self._add(key, value)

    cdef _add(self, key, value):
        pair_list_add(self._impl, key, value);

    cdef _replace(self, key, value):
        pair_list_replace(self._impl, key, value)

    def add(self, key, value):
        """Add the key and value, not overwriting any previous value."""
        self._add(key, value)

    def copy(self):
        """Return a copy of itself."""
        ret = MultiDict()
        ret._extend((list(self.items()),), {}, 'copy', True)
        return ret

    def extend(self, *args, **kwargs):
        """Extend current MultiDict with more values.

        This method must be used instead of update.
        """
        self._extend(args, kwargs, "extend", True)

    def clear(self):
        """Remove all items from MultiDict"""
        pair_list_clear(self._impl)

    # MutableMapping interface #

    def __setitem__(self, key, value):
        self._replace(key, value)

    def __delitem__(self, key):
        pair_list_del(self._impl, key)

    def setdefault(self, key, default=None):
        """Return value for key, set value to default if key is not present."""
        return pair_list_set_default(self._impl, key, default)

    def popone(self, key, default=_marker):
        """Remove the last occurrence of key and return the corresponding
        value.

        If key is not found, default is returned if given, otherwise
        KeyError is raised.

        """
        try:
            return pair_list_pop_one(self._impl, key)
        except KeyError:
            if default is _marker:
                raise
            else:
                return default

    pop = popone

    def popall(self, key, default=_marker):
        """Remove all occurrences of key and return the list of corresponding
        values.

        If key is not found, default is returned if given, otherwise
        KeyError is raised.

        """
        try:
            return pair_list_pop_all(self._impl, key)
        except KeyError:
            if default is _marker:
                raise
            else:
                return default

    def popitem(self):
        """Remove and return an arbitrary (key, value) pair."""
        return pair_list_pop_item(self._impl)

    def update(self, *args, **kwargs):
        """Update the dictionary from *other*, overwriting existing keys."""
        self._extend(args, kwargs, "update", False)


MutableMultiMapping.register(MultiDict)


cdef class CIMultiDict(MultiDict):
    """An ordered dictionary that can have multiple values for each key."""

    def __init__(self, *args, **kwargs):
        self._impl = ci_pair_list_new()
        self._extend(args, kwargs, 'CIMultiDict', True)

    def __reduce__(self):
        return (
            self.__class__,
            (list(self.items()),),
        )

    def copy(self):
        """Return a copy of itself."""
        ret = CIMultiDict()
        ret._extend((list(self.items()),), {}, 'copy', True)
        return ret



MutableMultiMapping.register(CIMultiDict)


cdef class _ViewBase:

    cdef _Base _md

    def __cinit__(self, _Base md):
        self._md = md

    def __len__(self):
        return pair_list_len(self._md._impl)


cdef class _ViewBaseSet(_ViewBase):

    def __richcmp__(self, other, op):
        if op == 0:  # <
            if not isinstance(other, Set):
                return NotImplemented
            return len(self) < len(other) and self <= other
        elif op == 1:  # <=
            if not isinstance(other, Set):
                return NotImplemented
            if len(self) > len(other):
                return False
            for elem in self:
                if elem not in other:
                    return False
            return True
        elif op == 2:  # ==
            if not isinstance(other, Set):
                return NotImplemented
            return len(self) == len(other) and self <= other
        elif op == 3:  # !=
            return not self == other
        elif op == 4:  #  >
            if not isinstance(other, Set):
                return NotImplemented
            return len(self) > len(other) and self >= other
        elif op == 5:  # >=
            if not isinstance(other, Set):
                return NotImplemented
            if len(self) < len(other):
                return False
            for elem in other:
                if elem not in self:
                    return False
            return True

    def __and__(self, other):
        if not isinstance(other, Iterable):
            return NotImplemented
        if isinstance(self, _ViewBaseSet):
            self = set(iter(self))
        if isinstance(other, _ViewBaseSet):
            other = set(iter(other))
        if not isinstance(other, Set):
            other = set(iter(other))
        return self & other

    def __or__(self, other):
        if not isinstance(other, Iterable):
            return NotImplemented
        if isinstance(self, _ViewBaseSet):
            self = set(iter(self))
        if isinstance(other, _ViewBaseSet):
            other = set(iter(other))
        if not isinstance(other, Set):
            other = set(iter(other))
        return self | other

    def __sub__(self, other):
        if not isinstance(other, Iterable):
            return NotImplemented
        if isinstance(self, _ViewBaseSet):
            self = set(iter(self))
        if isinstance(other, _ViewBaseSet):
            other = set(iter(other))
        if not isinstance(other, Set):
            other = set(iter(other))
        return self - other

    def __xor__(self, other):
        if not isinstance(other, Iterable):
            return NotImplemented
        if isinstance(self, _ViewBaseSet):
            self = set(iter(self))
        if isinstance(other, _ViewBaseSet):
            other = set(iter(other))
        if not isinstance(other, Set):
            other = set(iter(other))
        return self ^ other


cdef class _ItemsView(_ViewBaseSet):

    def isdisjoint(self, other):
        'Return True if two sets have a null intersection.'
        for v in other:
            if v in self:
                return False
        return True

    def __contains__(self, i):
        cdef str key
        cdef object value
        assert isinstance(i, tuple) or isinstance(i, list)
        assert len(i) == 2
        key = i[0]
        value = i[1]
        for k, v in self:
            if key == k and value == v:
                return True
        return False

    def __iter__(self):
        return multidict_items_iter_new(self._md._impl)

    def __repr__(self):
        lst = []
        for k ,v in self:
            lst.append("{!r}: {!r}".format(k, v))
        body = ', '.join(lst)
        return '{}({})'.format(self.__class__.__name__, body)


abc.ItemsView.register(_ItemsView)


cdef class _ValuesView(_ViewBase):

    def __contains__(self, value):
        for v in self:
            if v == value:
                return True
        return False

    def __iter__(self):
        return multidict_values_iter_new(self._md._impl)

    def __repr__(self):
        lst = []
        for v in self:
            lst.append("{!r}".format(v))
        body = ', '.join(lst)
        return '{}({})'.format(self.__class__.__name__, body)


abc.ValuesView.register(_ValuesView)


cdef class _KeysView(_ViewBaseSet):

    def isdisjoint(self, other):
        'Return True if two sets have a null intersection.'
        for k in other:
            print('isdisjoint.1', k)
            if k in self:
                print('isdisjoint.2', k)
                return False
        return True

    def __contains__(self, value):
        return self._md._contains(value)

    def __iter__(self):
        return multidict_keys_iter_new(self._md._impl)

    def __repr__(self):
        lst = []
        for k in self:
            lst.append("{!r}".format(k))
        body = ', '.join(lst)
        return '{}({})'.format(self.__class__.__name__, body)


abc.KeysView.register(_KeysView)
