# Copyright (c) 2006-2018  Andrey Golovigin
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


"""Miscellaneous small utils."""
from __future__ import print_function, unicode_literals

import itertools
from collections import (
    MutableMapping, MutableSet, OrderedDict, Sequence, deque
)
from functools import wraps
from types import GeneratorType

from six.moves import zip_longest

from .py3compat import fix_unicode_literals_in_doctest


def deprecated(since, reason=None):
    def decorator(f):
        @wraps(f)
        def new_f(*args, **kwargs):
            import warnings
            message = u'{0}() is deprecated since {1}'.format(f.__name__, since)
            if reason:
                message += ': {0}'.format(reason)
            warnings.warn(message, DeprecationWarning, stacklevel=2)
            return f(*args, **kwargs)
        return new_f
    return decorator


def memoize(f, capacity=1024):
    memory = {}
    history = deque()

    @wraps(f)
    def new_f(*args):
        if args not in memory:
            if len(history) >= capacity:
                del memory[history.popleft()]
            memory[args] = f(*args)
            history.append(args)
        return memory[args]
    return new_f


def collect_iterable(f):
    @wraps(f)
    def new_f(*args, **kwargs):
        return list(f(*args, **kwargs))
    return new_f


def pairwise(iterable):
    a, b = itertools.tee(iterable)
    next(b, None)
    return zip_longest(a, b)


@fix_unicode_literals_in_doctest
class CaseInsensitiveDict(MutableMapping):
    """A dict with case-insensitive lookup.

    >>> d = CaseInsensitiveDict(TesT='passed')
    >>> d
    CaseInsensitiveDict({'TesT': u'passed'})
    >>> d.lower()
    CaseInsensitiveDict({'test': u'passed'})
    >>> print(d['TesT'])
    passed
    >>> print(d['test'])
    passed
    >>> print(d['Test'])
    passed
    >>> print(d.get('test'))
    passed
    >>> print(d.get('Test'))
    passed

    >>> d['Test'] = 'passed again'
    >>> print(d['test'])
    passed again
    >>> 'test' in d
    True
    >>> 'Test' in d
    True
    >>> list(d.keys())
    [u'Test']
    >>> for key in d:
    ...     print(key)
    Test
    >>> for key, value in d.items():
    ...     print(key, value)
    Test passed again
    >>> bool(d)
    True
    >>> len(d)
    1

    >>> del d['test']
    >>> len(d)
    0
    >>> bool(d)
    False
    >>> 'test' in d
    False
    >>> 'Test' in d
    False
    >>> print(d.get('test'))
    None
    >>> print(d.get('Test'))
    None
    >>> print(d.get('Test', 'failed'))
    failed

    >>> CaseInsensitiveDict(
    ...     (key, value) for key, value in [('a', 'b')]
    ... )
    CaseInsensitiveDict({u'a': u'b'})

    """

    def __init__(self, *args, **kwargs):
        initial = dict(*args, **kwargs)
        self._dict = dict((key.lower(), value) for key, value in initial.items())
        self._keys = dict((key.lower(), key) for key in initial)

    def __len__(self):
        return len(self._dict)

    def __iter__(self):
        return iter(self._keys.values())

    def __setitem__(self, key, value):
        """To implement lowercase keys."""
        key_lower = key.lower()
        self._dict[key_lower] = value
        self._keys[key_lower] = key

    def __getitem__(self, key):
        return self._dict[key.lower()]

    def __delitem__(self, key):
        key_lower = key.lower()
        del self._dict[key_lower]
        del self._keys[key_lower]

    def __contains__(self, key):
        return key.lower() in self._dict

    def __repr__(self):
        """A caselessDict version of __repr__ """
        dct = dict((key, self[key]) for key in self)
        return '{0}({1})'.format(
            type(self).__name__, repr(dct),
        )

    def items_lower(self):
        return ((key.lower(), value) for key, value in self.items())

    def lower(self):
        return type(self)(self.items_lower())


class CaseInsensitiveDefaultDict(CaseInsensitiveDict):
    """CaseInseisitiveDict with default factory, like collections.defaultdict

    >>> d = CaseInsensitiveDefaultDict(int)
    >>> d['a']
    0
    >>> d['a'] += 1
    >>> d['a']
    1
    >>> d['A']
    1
    >>> d['a'] = 3
    >>> d['a']
    3
    >>> d['B'] += 10
    >>> d['b']
    10

    """
    def __init__(self, default_factory):
        super(CaseInsensitiveDefaultDict, self).__init__()
        self.default_factory = default_factory

    def __getitem__(self, key):
        try:
            return super(CaseInsensitiveDefaultDict, self).__getitem__(key)
        except KeyError:
            return self.default_factory()


@fix_unicode_literals_in_doctest
class OrderedCaseInsensitiveDict(CaseInsensitiveDict):
    """ An (incomplete) ordered case-insensitive dict.

    >>> d = OrderedCaseInsensitiveDict([
    ...     ('Uno', 1),
    ...     ('Dos', 2),
    ...     ('Tres', 3),
    ... ])
    >>> d
    OrderedCaseInsensitiveDict([(u'Uno', 1), (u'Dos', 2), (u'Tres', 3)])
    >>> d.lower()
    OrderedCaseInsensitiveDict([(u'uno', 1), (u'dos', 2), (u'tres', 3)])
    >>> d.keys()
    [u'Uno', u'Dos', u'Tres']
    >>> d.items()
    [(u'Uno', 1), (u'Dos', 2), (u'Tres', 3)]
    >>> d.values()
    [1, 2, 3]
    >>> d['Cuatro'] = 4
    >>> d.keys()
    [u'Uno', u'Dos', u'Tres', u'Cuatro']
    >>> d.items()
    [(u'Uno', 1), (u'Dos', 2), (u'Tres', 3), (u'Cuatro', 4)]
    >>> d.values()
    [1, 2, 3, 4]
    >>> list(d)
    [u'Uno', u'Dos', u'Tres', u'Cuatro']
    >>> list(d.iterkeys()) == d.keys()
    True
    >>> list(d.itervalues()) == d.values()
    True
    >>> list(d.items()) == d.items()
    True
    >>> 'Uno' in d
    True
    >>> 'uno' in d
    True
    >>> d['Uno']
    1
    >>> d['uno']
    1
    >>> d['UNO']
    1
    >>> 'Cuatro' in d
    True
    >>> 'CUATRO' in d
    True
    >>> d['Cuatro']
    4
    >>> d['cuatro']
    4
    >>> d['UNO'] = 'one'
    >>> d['uno']
    u'one'
    >>> d['Uno']
    u'one'
    >>> d.keys()
    [u'Uno', u'Dos', u'Tres', u'Cuatro']
    >>> d['cuatro'] = 'four'
    >>> d['Cuatro']
    u'four'
    >>> d['cuatro']
    u'four'
    >>> d.keys()
    [u'Uno', u'Dos', u'Tres', u'Cuatro']

    """

    def __init__(self, data=()):
        if isinstance(data, GeneratorType):
            data = list(data)
        if isinstance(data, Sequence):
            self.order = [key for key, value in data]
        else:
            self.order = list(data.keys())
        super(OrderedCaseInsensitiveDict, self).__init__(data)

    def __setitem__(self, key, value):
        if key not in self:
            self.order.append(key)
        super(OrderedCaseInsensitiveDict, self).__setitem__(key, value)

    def __delitem__(self, key):
        raise NotImplementedError

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

    def __eq__(self, other):
        """
        >>> OrderedCaseInsensitiveDict([('a', 1), ('b', 2)]) == OrderedCaseInsensitiveDict([('a', 1), ('b', 2)])
        True
        >>> OrderedCaseInsensitiveDict([('a', 1), ('b', 2)]) == OrderedCaseInsensitiveDict([('b', 2), ('a', 1)])
        False
        >>> OrderedCaseInsensitiveDict([('a', 1), ('b', 2)]) == dict([('b', 2), ('a', 1)])
        True
        >>> OrderedCaseInsensitiveDict([('a', 1), ('B', 2)]) == OrderedCaseInsensitiveDict([('A', 1), ('b', 2)])
        False
        """
        if isinstance(other, (OrderedCaseInsensitiveDict, OrderedDict)):
            return list(self.items()) == list(other.items())
        else:
            return super(OrderedCaseInsensitiveDict, self).__eq__(other)

    def iterkeys(self):
        return iter(self.order)

    def keys(self):
        return self.order

    def itervalues(self):
        for key in self.order:
            yield self[key]

    def values(self):
        return [self[key] for key in self.order]

    def items(self):
        for key in self.order:
            yield key, self[key]

    def items(self):
        return [(key, self[key]) for key in self.order]

    def __repr__(self):
        return '{0}({1})'.format(
            type(self).__name__, repr(self.items())
        )


@fix_unicode_literals_in_doctest
class CaseInsensitiveSet(MutableSet):
    """A very basic case-insensitive set.

    >>> s = CaseInsensitiveSet()
    >>> len(s)
    0
    >>> 'a' in s
    False
    >>> list(CaseInsensitiveSet(['aaa', 'Aaa', 'AAA']))
    [u'aaa']
    >>> s = CaseInsensitiveSet(['Aaa', 'Bbb'])
    >>> s
    CaseInsensitiveSet([u'Aaa', u'Bbb'])
    >>> s.lower()
    CaseInsensitiveSet([u'aaa', u'bbb'])
    >>> len(s)
    2
    >>> 'aaa' in s
    True
    >>> 'Aaa' in s
    True
    >>> 'AAA' in s
    True
    >>> 'bbb' in s
    True
    >>> 'Bbb' in s
    True
    >>> 'abc' in s
    False
    >>> s.add('ccc')
    >>> len(s)
    3
    >>> 'aaa' in s
    True
    >>> 'ccc' in s
    True
    >>> s.remove('AAA')
    >>> len(s)
    2
    >>> 'aaa' in s
    False

    >>> bool(CaseInsensitiveSet(['a']))
    True
    >>> bool(CaseInsensitiveSet([]))
    False
    >>> bool(CaseInsensitiveSet())
    False

    """

    def __init__(self, iterable=()):
        self._set = set()
        self._keys = dict()
        for item in iterable:
            self.add(item)

    def __contains__(self, key):
        return key.lower() in self._set

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

    def __len__(self):
        return len(self._set)

    def __repr__(self):
        """A caselessDict version of __repr__ """
        return '{0}({1})'.format(
            type(self).__name__, repr(sorted(self._keys.values()))
        )

    def add(self, key):
        key_lower = key.lower()
        self._set.add(key_lower)
        self._keys[key_lower] = key

    def discard(self, key):
        key_lower = key.lower()
        self._set.discard(key_lower)
        self._keys.pop(key_lower, None)

    def get_canonical_key(self, key):
        return self._keys[key.lower()]

    def lower(self):
        return type(self)(self._set)
