from __future__ import absolute_import

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

from cpython.object cimport PyObject_Str

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

cdef object _marker = object()

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


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


cdef _eq(self, other):
    cdef int is_left_base, is_right_base
    cdef Py_ssize_t i, l
    cdef list lft_items, rgt_items
    cdef _Pair lft, rgt

    is_left_base = isinstance(self, _Base)
    is_right_base = isinstance(other, _Base)

    if is_left_base and is_right_base:
        lft_items = (<_Base>self)._impl._items
        rgt_items = (<_Base>other)._impl._items
        l = len(lft_items)
        if l != len(rgt_items):
            return False
        for i in range(l):
            lft = <_Pair>(lft_items[i])
            rgt = <_Pair>(rgt_items[i])
            if lft._hash != rgt._hash:
                return False
            if lft._identity != rgt._identity:
                return False
            if lft._value != rgt._value:
                return False
        return True
    elif is_left_base and isinstance(other, abc.Mapping):
        return (<_Base>self)._eq_to_mapping(other)
    elif is_right_base and isinstance(self, abc.Mapping):
        return (<_Base>other)._eq_to_mapping(self)
    else:
        return NotImplemented


cdef class _Pair:
    cdef str _identity
    cdef Py_hash_t _hash
    cdef str _key
    cdef object _value

    def __cinit__(self, identity, key, value):
        self._hash = hash(identity)
        self._identity = <str>identity
        self._key = <str>key
        self._value = value


cdef unsigned long long _version


cdef class _Impl:
    cdef list _items
    cdef unsigned long long _version

    def __cinit__(self):
        self._items = []
        self.incr_version()

    cdef void incr_version(self):
        global _version
        _version += 1
        self._version = _version


cdef class _Base:

    cdef _Impl _impl

    cdef str _title(self, s):
        typ = type(s)
        if typ is str:
            return <str>s
        elif typ is _istr:
            return PyObject_Str(s)
        else:
            return str(s)

    def getall(self, key, default=_marker):
        """Return a list of all values matching the key."""
        return self._getall(self._title(key), key, default)

    cdef _getall(self, str identity, key, default):
        cdef list res
        cdef _Pair item
        cdef Py_hash_t h = hash(identity)
        res = []
        for i in self._impl._items:
            item = <_Pair>i
            if item._hash != h:
                continue
            if item._identity == identity:
                res.append(item._value)
        if res:
            return res
        elif default is not _marker:
            return default
        else:
            raise KeyError('Key not found: %r' % key)

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

    cdef _getone(self, str identity, key, default):
        cdef _Pair item
        cdef Py_hash_t h = hash(identity)
        for i in self._impl._items:
            item = <_Pair>i
            if item._hash != h:
                continue
            if item._identity == identity:
                return item._value
        if default is not _marker:
            return default
        raise KeyError('Key not found: %r' % key)

    # Mapping interface #

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

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

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

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

    cdef _contains(self, str identity):
        cdef _Pair item
        cdef Py_hash_t h = hash(identity)
        for i in self._impl._items:
            item = <_Pair>i
            if item._hash != h:
                continue
            if item._identity == identity:
                return True
        return False

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

    def __len__(self):
        return len(self._impl._items)

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

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

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

    def __repr__(self):
        cdef _Pair item
        lst = []
        for i in self._impl._items:
            item = <_Pair>i
            lst.append("'{}': {!r}".format(item._key, item._value))
        body = ', '.join(lst)
        return '<{}({})>'.format(self.__class__.__name__, body)

    cdef _eq_to_mapping(self, other):
        cdef _Pair item
        if len(self._impl._items) != len(other):
            return False
        for i in self._impl._items:
            item = <_Pair>i
            for k, v in other.items():
                if self._title(k) != item._identity:
                    continue
                if v == item._value:
                    break
            else:
                return False
        return True

    def __richcmp__(self, other, op):
        if op == 2:  # ==
            return _eq(self, other)
        elif op == 3:  # !=
            ret = _eq(self, other)
            if ret is NotImplemented:
                return ret
            else:
                return not ret
        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

    cdef str _title(self, s):
        typ = type(s)
        if typ is str:
            return <str>(s.title())
        elif type(s) is _istr:
            return PyObject_Str(s)
        return s.title()


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 = _Impl()
        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 _Pair item
        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):
                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 do_add:
                    self._append_items_seq(arg, name)
                else:
                    self._update_items_seq(arg, name)

        for key, value in kwargs.items():
            if do_add:
                self._add(key, value)
            else:
                self._replace(key, value)

    cdef object _update_items(self, _Impl impl):
        cdef _Pair item, item2
        cdef object i
        cdef dict used_keys = {}
        cdef Py_ssize_t start
        cdef Py_ssize_t post
        cdef Py_ssize_t size = len(self._impl._items)
        cdef Py_hash_t h

        for i in impl._items:
            item = <_Pair>i

            start = used_keys.get(item._identity, 0)
            for pos in range(start, size):
                item2 = <_Pair>(self._impl._items[pos])
                if item2._hash != item._hash:
                    continue
                if item2._identity == item._identity:
                    used_keys[item._identity] = pos + 1
                    item2._key = item._key
                    item2._value = item._value
                    break
            else:
                self._impl._items.append(_Pair.__new__(
                    _Pair, item._identity, item._key, item._value))
                size += 1
                used_keys[item._identity] = size

        self._post_update(used_keys)

    cdef object _update_items_seq(self, object arg, object name):
        cdef _Pair item
        cdef object i
        cdef object identity
        cdef object key
        cdef object value
        cdef dict used_keys = {}
        cdef Py_ssize_t start
        cdef Py_ssize_t post
        cdef Py_ssize_t size = len(self._impl._items)
        cdef Py_hash_t h
        for i in arg:
            if not len(i) == 2:
                raise TypeError(
                    "{} takes either dict or list of (key, value) "
                    "tuples".format(name))
            key = _str(i[0])
            value = i[1]
            identity = self._title(key)
            h = hash(identity)

            start = used_keys.get(identity, 0)
            for pos in range(start, size):
                item = <_Pair>(self._impl._items[pos])
                if item._hash != h:
                    continue
                if item._identity == identity:
                    used_keys[identity] = pos + 1
                    item._key = key
                    item._value = value
                    break
            else:
                self._impl._items.append(_Pair.__new__(
                    _Pair, identity, key, value))
                size += 1
                used_keys[identity] = size

        self._post_update(used_keys)

    cdef object _post_update(self, dict used_keys):
        cdef Py_ssize_t i = 0
        cdef _Pair item
        while i < len(self._impl._items):
            item = <_Pair>self._impl._items[i]
            pos = used_keys.get(item._identity)
            if pos is None:
                i += 1
                continue
            if i >= pos:
                del self._impl._items[i]
            else:
                i += 1

        self._impl.incr_version()

    cdef object _append_items(self, _Impl impl):
        cdef _Pair item
        cdef object i
        cdef str key
        cdef object value
        for i in impl._items:
            item = <_Pair>i
            key = item._key
            value = item._value
            self._impl._items.append(_Pair.__new__(
                _Pair, self._title(key), key, value))
        self._impl.incr_version()

    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._impl._items.append(_Pair.__new__(
                _Pair, self._title(key), _str(key), value))
        self._impl.incr_version()

    cdef _add(self, key, value):
        self._impl._items.append(_Pair.__new__(
            _Pair, self._title(key), _str(key), value))
        self._impl.incr_version()

    cdef _replace(self, key, value):
        cdef str identity = self._title(key)
        cdef str k = _str(key)
        cdef Py_hash_t h = hash(identity)
        cdef Py_ssize_t i, rgt
        cdef _Pair item
        cdef list items = self._impl._items

        for i in range(len(items)-1, -1, -1):
            item = <_Pair>items[i]
            if h != item._hash:
                continue
            if item._identity == identity:
                item._key = k
                item._value = value
                # i points to last found item
                rgt = i
                self._impl.incr_version()
                break
        else:
            self._impl._items.append(_Pair.__new__(_Pair, identity, k, value))
            self._impl.incr_version()
            return

        # remove all precending items
        i = 0
        while i < rgt:
            item = <_Pair>items[i]
            if h == item._hash and item._identity == identity:
                del items[i]
                rgt -= 1
            else:
                i += 1

    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"""
        self._impl._items.clear()
        self._impl.incr_version()

    # MutableMapping interface #

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

    def __delitem__(self, key):
        self._remove(key)

    cdef _remove(self, key):
        cdef _Pair item
        cdef bint found = False
        cdef str identity = self._title(key)
        cdef Py_hash_t h = hash(identity)
        cdef list items = self._impl._items
        for i in range(len(items) - 1, -1, -1):
            item = <_Pair>items[i]
            if item._hash != h:
                continue
            if item._identity == identity:
                del items[i]
                found = True
        if not found:
            raise KeyError(key)
        else:
            self._impl.incr_version()

    def setdefault(self, key, default=None):
        """Return value for key, set value to default if key is not present."""
        cdef _Pair item
        cdef str identity = self._title(key)
        cdef Py_hash_t h = hash(identity)
        cdef list items = self._impl._items
        for i in items:
            item = <_Pair>i
            if item._hash != h:
                continue
            if item._identity == identity:
                return item._value
        self._add(key, default)
        return 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.

        """
        cdef object value = None
        cdef str identity = self._title(key)
        cdef Py_hash_t h = hash(identity)
        cdef _Pair item
        cdef list items = self._impl._items
        for i in range(len(items)):
            item = <_Pair>items[i]
            if item._hash != h:
                continue
            if item._identity == identity:
                value = item._value
                del items[i]
                self._impl.incr_version()
                return value
        if default is _marker:
            raise KeyError(key)
        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.

        """
        cdef bint found = False
        cdef str identity = self._title(key)
        cdef Py_hash_t h = hash(identity)
        cdef _Pair item
        cdef list items = self._impl._items
        cdef list ret = []
        for i in range(len(items)-1, -1, -1):
            item = <_Pair>items[i]
            if item._hash != h:
                continue
            if item._identity == identity:
                ret.append(item._value)
                del items[i]
                self._impl.incr_version()
                found = True
        if not found:
            if default is _marker:
                raise KeyError(key)
            else:
                return default
        else:
            ret.reverse()
            return ret

    def popitem(self):
        """Remove and return an arbitrary (key, value) pair."""
        cdef _Pair item
        cdef list items = self._impl._items
        if items:
            item = <_Pair>items.pop(0)
            self._impl.incr_version()
            return (item._key, item._value)
        else:
            raise KeyError("empty multidict")

    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 = _Impl()
        self._extend(args, kwargs, 'CIMultiDict', True)

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

    cdef str _title(self, s):
        typ = type(s)
        if typ is str:
            return <str>(s.title())
        elif type(s) is _istr:
            return PyObject_Str(s)
        return s.title()

    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 _Impl _impl

    def __cinit__(self, _Impl impl):
        self._impl = impl

    def __len__(self):
        return len(self._impl._items)


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 _ItemsIter:
    cdef _Impl _impl
    cdef int _current
    cdef int _len
    cdef unsigned long long _version

    def __cinit__(self, _Impl impl):
        self._impl = impl
        self._current = 0
        self._version = impl._version
        self._len = len(impl._items)

    def __iter__(self):
        return self

    def __next__(self):
        if self._version != self._impl._version:
            raise RuntimeError("Dictionary changed during iteration")
        if self._current == self._len:
            raise StopIteration
        item = <_Pair>self._impl._items[self._current]
        self._current += 1
        return (item._key, item._value)


cdef class _ItemsView(_ViewBaseSet):

    def isdisjoint(self, other):
        'Return True if two sets have a null intersection.'
        cdef _Pair item
        for i in self._impl._items:
            item = <_Pair>i
            t = (item._key, item._value)
            if t in other:
                return False
        return True

    def __contains__(self, i):
        cdef _Pair item
        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 item in self._impl._items:
            if key == item._key and value == item._value:
                return True
        return False

    def __iter__(self):
        return _ItemsIter.__new__(_ItemsIter, self._impl)

    def __repr__(self):
        cdef _Pair item
        lst = []
        for i in self._impl._items:
            item = <_Pair>i
            lst.append("{!r}: {!r}".format(item._key, item._value))
        body = ', '.join(lst)
        return '{}({})'.format(self.__class__.__name__, body)


abc.ItemsView.register(_ItemsView)


cdef class _ValuesIter:
    cdef _Impl _impl
    cdef int _current
    cdef int _len
    cdef unsigned long long _version

    def __cinit__(self, _Impl impl):
        self._impl = impl
        self._current = 0
        self._len = len(impl._items)
        self._version = impl._version

    def __iter__(self):
        return self

    def __next__(self):
        if self._version != self._impl._version:
            raise RuntimeError("Dictionary changed during iteration")
        if self._current == self._len:
            raise StopIteration
        item = <_Pair>self._impl._items[self._current]
        self._current += 1
        return item._value


cdef class _ValuesView(_ViewBase):

    def __contains__(self, value):
        cdef _Pair item
        for i in self._impl._items:
            item = <_Pair>i
            if item._value == value:
                return True
        return False

    def __iter__(self):
        return _ValuesIter.__new__(_ValuesIter, self._impl)

    def __repr__(self):
        cdef _Pair item
        lst = []
        for i in self._impl._items:
            item = <_Pair>i
            lst.append("{!r}".format(item._value))
        body = ', '.join(lst)
        return '{}({})'.format(self.__class__.__name__, body)


abc.ValuesView.register(_ValuesView)


cdef class _KeysIter:
    cdef _Impl _impl
    cdef int _current
    cdef int _len
    cdef unsigned long long _version

    def __cinit__(self, _Impl impl):
        self._impl = impl
        self._current = 0
        self._len = len(self._impl._items)
        self._version = impl._version

    def __iter__(self):
        return self

    def __next__(self):
        if self._version != self._impl._version:
            raise RuntimeError("Dictionary changed during iteration")
        if self._current == self._len:
            raise StopIteration
        item = <_Pair>self._impl._items[self._current]
        self._current += 1
        return item._key


cdef class _KeysView(_ViewBaseSet):

    def isdisjoint(self, other):
        'Return True if two sets have a null intersection.'
        cdef _Pair item
        for i in self._impl._items:
            item = <_Pair>i
            if item._key in other:
                return False
        return True

    def __contains__(self, value):
        cdef _Pair item
        for i in self._impl._items:
            item = <_Pair>i
            if item._key == value:
                return True
        return False

    def __iter__(self):
        return _KeysIter.__new__(_KeysIter, self._impl)

    def __repr__(self):
        cdef _Pair item
        lst = []
        for i in self._impl._items:
            item = <_Pair>i
            lst.append("{!r}".format(item._key))
        body = ', '.join(lst)
        return '{}({})'.format(self.__class__.__name__, body)


abc.KeysView.register(_KeysView)
