# Copyright (C) 2020-2022 HERE Global B.V. and its affiliate(s).
# All rights reserved.
#
# This software and other materials contain proprietary information
# controlled by HERE and are protected by applicable copyright legislation.
# Any use and utilization of this software and other materials and
# disclosure to any third parties is conditional upon having a separate
# agreement with HERE for the access, use, utilization or disclosure of this
# software. In the absence of such agreement, the use of the software is not
# allowed.

"""
This module contains generic utility functions for python collections and iterables.
"""

import collections.abc
from itertools import zip_longest
from typing import Any, Collection, Iterable, Iterator, Tuple, TypeVar

T = TypeVar("T")


def grouper(size: int, iterable: Iterable[T], fill_value=None) -> Iterator[T]:
    """
    Create groups of `size` each from given iterable.

    :param size: An int representing size of each group.
    :param iterable: An iterable.
    :param fill_value: Value to put for the last group.
    :return: A Generator.
    """
    args = [iter(iterable)] * size
    return zip_longest(fillvalue=fill_value, *args)


def iter_tuples(iterable):
    """
    Return an iterator of key-value tuples from a variety of collections types.

    This is used to convert mapping to their ``items``, but also supporting collections
    or iterators that are already in the form of key-value tuples.

    Supported types:
    - mappings (dict), yielding key-value tuples
    - every collection that, when iterated, yields tuples
      in this case the tuples are not modified and may contain
      more than two elements

    :param iterable: a collection containing keys and values
    :return: an iterator over the keys and values of the collection, as tuples

    Usage::

        >>> dict_data = { 1: 10, 2: 20, 3: 30, 4: 40 }
        >>> list(iter_tuples(dict_data))
        [(1, 10), (2, 20), (3, 30), (4, 40)]

        >>> list_data = [(1, 10), (2, 20), (3, 30), (4, 40)]
        >>> list(iter_tuples(list_data))
        [(1, 10), (2, 20), (3, 30), (4, 40)]

        >>> map_data = map(lambda x: (x, x * 10), range(1, 5))
        >>> list(iter_tuples(map_data))
        [(1, 10), (2, 20), (3, 30), (4, 40)]
    """
    return (
        iter(iterable.items()) if isinstance(iterable, collections.abc.Mapping) else iter(iterable)
    )


def flatten_iterator(
    iterator: Iterator[Tuple[Any, Any]],
    max_level: int = -1,
    prefix: str = "",
    sep: str = ".",
    exclude_keys: Collection = [],
) -> Iterator[Tuple[str, Any]]:
    """
    Scan a tuple iterator and flatten keys and values, in case values contain nested dictionaries.

    Keys are converted to string and composed in hierarchical paths, separated by ``sep``.
    An initial prefix, if present, is prepended and separated by ``sep`` to the keys
    of the passed and nested dictionaries. Order of elements is preserved.

    Among other uses, the output of the function can be passed directly to
    the constructor of ``dict`` to compose a new, flat dictionary without overhead.

    :param iterator: the iterator to scan recursively
    :param max_level: the maximum level of the recursion. 0 performs no flattening,
        1 flattens only the first nested dictionaries, 2 only the first and second nested
        dictionaries, and so on. A negative number represents no limit.
    :param prefix: what to prepend, separate using ``sep``, to all the returned keys
    :param sep: the separator to use when concatenating keys
    :param exclude_keys: full path of keys that should not be flattened
    :yield: tuples of concatenated keys and unchanged values

    Usage::

        >>> data = { 1: 10, 2: 20, 3: { "a": "AAA", "b": "BBB" }, 4: 40 }
        >>> list(flatten_iterator(iter(data.items())))
        [('1', 10), ('2', 20), ('3.a', 'AAA'), ('3.b', 'BBB'), ('4', 40)]

        >>> dict(flatten_iterator(iter(data.items())))
        {'1': 10, '2': 20, '3.a': 'AAA', '3.b': 'BBB', '4': 40}

        >>> dict(flatten_iterator(iter(data.items()), prefix="x"))
        {'x.1': 10, 'x.2': 20, 'x.3.a': 'AAA', 'x.3.b': 'BBB', 'x.4': 40}
    """
    for k, v in iterator:
        prefix_k = str(k) if not prefix else prefix + sep + str(k)
        if isinstance(v, dict) and max_level != 0 and prefix_k not in exclude_keys:
            yield from flatten_iterator(
                iter(v.items()),
                max_level=max_level - 1,
                prefix=prefix_k,
                sep=sep,
                exclude_keys=exclude_keys,
            )
        else:
            yield prefix_k, v
