from collections.abc import Callable, Iterable
from typing import TypeVar, overload

from .decorator import make_data_last

T = TypeVar('T')


@overload
def unique_with(iterable: Iterable[T], function: Callable[[T, T], bool], /) -> Iterable[T]: ...


@overload
def unique_with(function: Callable[[T, T], bool]) -> Callable[[Iterable[T]], Iterable[T]]: ...


@make_data_last
def unique_with(iterable: Iterable[T], function: Callable[[T, T], bool], /) -> Iterable[T]:
    """
    Yields unique elements from the iterable, given the iterable and a comparison function.

    Uniqueness is determined by the comparison function.

    Parameters
    ----------
    iterable: Iterable[T]
        Iterable (positional-only).
    function: Callable[[T, T], bool]
        Function to compare elements (positional-only).

    Returns
    -------
    Iterable[T]
        Unique elements from the iterable.

    Examples
    --------
    Data first:
    >>> list(
    ...     R.unique_with(
    ...         [{'a': 1}, {'a': 2}, {'a': 2}, {'a': 5}, {'a': 1}, {'a': 6}, {'a': 7}],
    ...         lambda x, y: x == y,
    ...     )
    ... )
    [{'a': 1}, {'a': 2}, {'a': 5}, {'a': 6}, {'a': 7}]
    >>> list(
    ...     R.unique_with(
    ...         [{'a': 1}, {'a': 2}, {'a': 2}, {'a': 5}, {'a': 1}, {'a': 6}, {'a': 7}],
    ...         lambda x, y: x['a'] % 2 == y['a'] % 2
    ...     )
    ... )
    [{'a': 1}, {'a': 2}]
    >>> list(
    ...     R.unique_with(
    ...         [{'a': 1}, {'a': 2}, {'a': 2}, {'a': 5}, {'a': 1}, {'a': 6}, {'a': 7}],
    ...         lambda x, y: x['a'] % 3 == y['a'] % 3
    ...     )
    ... )
    [{'a': 1}, {'a': 2}, {'a': 6}]

    Data last:
    >>> list(
    ...     R.unique_with(R.eq)(
    ...         [{'a': 1}, {'a': 2}, {'a': 2}, {'a': 5}, {'a': 1}, {'a': 6}, {'a': 7}]
    ...     )
    ... )
    [{'a': 1}, {'a': 2}, {'a': 5}, {'a': 6}, {'a': 7}]
    >>> R.pipe(
    ...     [{'a': 1}, {'a': 2}, {'a': 2}, {'a': 5}, {'a': 1}, {'a': 6}, {'a': 7}],
    ...     R.unique_with(R.eq),
    ...     R.take(3),
    ...     list,
    ... )
    [{'a': 1}, {'a': 2}, {'a': 5}]

    """
    returned: list[T] = []
    for x in iterable:
        if not any(function(x, y) for y in returned):
            yield x
            returned.append(x)
