__all__ = ["chunked", "unique_everseen", "unique_list", "find_first", "merge_dicts"]

from collections.abc import Iterable, Mapping
from itertools import chain, filterfalse, islice


def chunked(qs, size, startindex=0):
    """
    Divide an interable into chunks of ``size``

    >>> list(chunked("hahahaha", 2))
    ['ha', 'ha', 'ha', 'ha']
    >>> list(chunked([1,2,3,4,5,6,7], 3))
    [[1, 2, 3], [4, 5, 6], [7]]
    """
    while True:
        chunk = qs[startindex : startindex + size]
        chunklen = len(chunk)
        if chunklen:
            yield chunk
        if chunklen < size:
            break
        startindex += size


# from https://docs.python.org/3/library/itertools.html#itertools-recipes


def ichunked(qs, size):
    """
    Does the same as chunked, but works with arbitrary generators

    >>> list(map(lambda x: "".join(x), ichunked("hahahaha", 2)))
    ['ha', 'ha', 'ha', 'ha']
    >>> list(map(list, ichunked([1,2,3,4,5,6,7], 3)))
    [[1, 2, 3], [4, 5, 6], [7]]
    """
    iterator = iter(qs)
    for first in iterator:
        yield chain([first], islice(iterator, size - 1))


def unique_everseen(iterable, key=None):
    """List unique elements, preserving order. Remember all elements ever seen.

    >>> list(unique_everseen('AAAABBBCCDAABBB'))
    ['A', 'B', 'C', 'D']
    >>> list(unique_everseen('ABBCcAD', str.lower))
    ['A', 'B', 'C', 'D']
    """
    seen = set()
    seen_add = seen.add
    if key is None:
        for element in filterfalse(seen.__contains__, iterable):
            seen_add(element)
            yield element
    else:
        for element in iterable:
            k = key(element)
            if k not in seen:
                seen_add(k)
                yield element


def unique_list(iterable, key=None):
    """Return a list without duplicates, preserving order.

    >>> unique_list('AAAABBBCCDAABBB')
    ['A', 'B', 'C', 'D']
    >>> unique_list('ABBCcAD', str.lower)
    ['A', 'B', 'C', 'D']
    >>> unique_list([1, 3, 5, 3, 1, 7, 20])
    [1, 3, 5, 7, 20]
    """
    unique_iterable = unique_everseen(iterable, key=key)
    return list(unique_iterable)


def find_first(function, iterable):
    """
    Find first item in iterable that fulfills function

    >>> is_henk_1 = lambda x: x.get('henk') == 1
    >>> data = [{'kak': 0, 'henk': 8}, {'koe': 5, 'henk': 1}]
    >>> find_first(is_henk_1, data)
    {'koe': 5, 'henk': 1}
    >>> find_first(is_henk_1, [])
    Traceback (most recent call last):
      ...
    StopIteration
    """
    return next(filter(function, iterable))


def merge_dicts(
    target, updates, overwrite=False, multivalue=False, deduplicate_iterables=False
):
    """
    Merge two dicts

    >>> first = {'a': 1, 'b': [1], 'c': {'d': 1, 'e': [2]}}
    >>> second = {'a': 3, 'b': 'hanny', 'c': {'d': 9, 'e': [7], 'f': [99]}, 'g': 8}
    >>> merge_dicts(first, second)
    {'a': 1, 'b': [1, 'hanny'], 'c': {'d': 1, 'e': [2, 7], 'f': [99]}, 'g': 8}

    >>> first = {'a': 1, 'b': [1], 'c': {'d': 1, 'e': [2]}}
    >>> merge_dicts(first, second, multivalue=True)
    {'a': [1, 3], 'b': [1, 'hanny'], 'c': {'d': 1, 'e': [2, 7], 'f': [99]}, 'g': 8}

    >>> first = {'a': 1, 'b': [1], 'c': {'d': 1, 'e': [2]}}
    >>> merge_dicts(first, second, overwrite=True)
    {'a': 3, 'b': [1, 'hanny'], 'c': {'d': 9, 'e': [2, 7], 'f': [99]}, 'g': 8}

    >>> first = {'a': 1, 'b': [1]}
    >>> second = {'b': [1]}
    >>> merge_dicts(first, second)
    {'a': 1, 'b': [1, 1]}

    >>> first = {'a': 1, 'b': [1]}
    >>> second = {'b': [1]}
    >>> merge_dicts(first, second, deduplicate_iterables=True)
    {'a': 1, 'b': [1]}

    >>> first = {'a': ["klaas", "piet"], 'b': [3, 1]}
    >>> second = {'a': "piet", 'b': 1}
    >>> merge_dicts(first, second, multivalue=True, deduplicate_iterables=True)
    {'a': ['klaas', 'piet'], 'b': [3, 1]}

    >>> first = {'a': ["klaas", "piet"], 'b': [3, 1]}
    >>> second = {'a': "piet", 'b': 1}
    >>> merge_dicts(first, second, multivalue=True, deduplicate_iterables=False)
    {'a': ['klaas', 'piet', 'piet'], 'b': [3, 1, 1]}
    """
    for key, value in updates.items():
        if key in target:
            item = target[key]
            if isinstance(item, dict):
                if isinstance(value, Mapping):
                    target[key] = merge_dicts(item, value, overwrite)
                elif overwrite:
                    target[key] = value
                else:
                    raise Exception(  # pylint:disable=broad-exception-raised
                        "can not merge nub"
                    )
            elif isinstance(item, list):
                if isinstance(value, str):
                    if deduplicate_iterables and value in item:
                        continue
                    else:
                        item.append(value)
                elif isinstance(value, Iterable):
                    item = item + list(value)
                    if deduplicate_iterables:
                        item = list(unique_everseen(item))
                else:
                    if deduplicate_iterables and value in item:
                        continue
                    else:
                        item.append(value)

                target[key] = item
            elif multivalue and value != item:  # gather multiple values into list
                target[key] = [item, value]
            elif overwrite:
                target[key] = value
        else:
            target[key] = value

    return target
