from collections.abc import Callable, Hashable, Iterable, Sequence
from functools import reduce
from typing import TypeVar, overload

__all__ = (
    "dfilter",
    "dmap",
    "endofilter",
    "endomap",
    "fold_dictionaries",
    "identity",
    "kfilter",
    "kmap",
    "kvfilter",
    "kvmap",
    "lfilter",
    "lmap",
    # "negate",
    "sfilter",
    "smap",
    "split_head",
    "tail",
    "tfilter",
    "tmap",
    "vfilter",
    "vmap",
)

T = TypeVar("T")
Tup = TypeVar("Tup", bound=tuple[object, ...])
Seq = TypeVar("Seq", bound=list | tuple)
KPre = TypeVar("KPre")
VPre = TypeVar("VPre")
KPost = TypeVar("KPost")
VPost = TypeVar("VPost")
TPost = TypeVar("TPost")
TPre = TypeVar("TPre")
K = TypeVar("K", bound=Hashable)
V = TypeVar("V")
type Filterer[T] = Callable[[T], bool]
type DictLike[K, V] = dict[K, V] | Iterable[tuple[K, V]]


def negate[T: object](f: Callable[[T], object]) -> Callable[[T], bool]:
    def inner(ob: T) -> bool:
        return not f(ob)
    
    return inner


@overload
def endomap(
    callable_: Callable[[TPre], TPost], sequence: list[TPre]
) -> list[TPost]: ...  # pragma: no cover
@overload
def endomap(
    callable_: Callable[[TPre], TPost], sequence: set[TPre]
) -> set[TPost]: ...  # pragma: no cover
@overload
def endomap(
    callable_: Callable[[TPre], TPost], sequence: tuple[TPre, ...]
) -> tuple[TPost, ...]: ...  # pragma: no cover
def endomap(callable_, sequence):
    return type(sequence)(map(callable_, sequence))


@overload
def endofilter(
    callable_: Callable[[T], bool], sequence: list[T]
) -> list[T]: ...  # pragma: no cover
@overload
def endofilter(callable_: Callable[[T], bool], sequence: set[T]) -> set[T]: ...  # pragma: no cover
@overload
def endofilter(
    callable_: Callable[[T], bool], sequence: tuple[T, ...]
) -> tuple[T, ...]: ...  # pragma: no cover
def endofilter(callable_, sequence):
    return type(sequence)(filter(callable_, sequence))


def _handle_dictlike[K, V](d: DictLike[K, V]) -> Iterable[tuple[K, V]]:
    if isinstance(d, dict):
        tmp = tuple(d.items())
        return tmp  # type: ignore
    return d


def lmap(callable_: Callable[[TPre], TPost], iterable: Iterable[TPre]) -> list[TPost]:
    return list(map(callable_, iterable))


def smap(callable_: Callable[[TPre], TPost], iterable: Iterable[TPre]) -> set[TPost]:
    return set(map(callable_, iterable))


def tmap(callable_: Callable[[TPre], TPost], iterable: Iterable[TPre]) -> tuple[TPost, ...]:
    return tuple(map(callable_, iterable))


def vmap(callable_: Callable[[TPre], TPost], dictionary: DictLike[K, TPre]) -> dict[K, TPost]:
    return {k: callable_(v) for k, v in _handle_dictlike(dictionary)}


def kmap(callable_: Callable[[TPre], TPost], dictionary: DictLike[TPre, V]) -> dict[TPost, V]:
    return {callable_(k): v for k, v in _handle_dictlike(dictionary)}


def dmap(
    callable_: Callable[[TPre], TPost], dictionary: DictLike[TPre, TPre]
) -> dict[TPost, TPost]:
    return {callable_(k): callable_(v) for k, v in _handle_dictlike(dictionary)}


def kvmap(
    callable_: Callable[[tuple[KPre, VPre]], tuple[KPost, VPost]], d: DictLike[KPre, VPre]
) -> dict[KPost, VPost]:
    return dict(map(callable_, _handle_dictlike(d)))


def lfilter(filterer: Filterer[T], iterable: Iterable[T]) -> list[T]:
    return list(filter(filterer, iterable))


def sfilter(filterer: Filterer[T], iterable: Iterable[T]) -> set[T]:
    return set(filter(filterer, iterable))


def tfilter(filterer: Filterer[T], iterable: Iterable[T]) -> tuple[T, ...]:
    return tuple(filter(filterer, iterable))


def vfilter(filterer: Filterer[V], dictionary: dict[K, V]) -> dict[K, V]:
    return {k: v for k, v in dictionary.items() if filterer(v)}


def kfilter(filterer: Filterer[K], dictionary: dict[K, V]) -> dict[K, V]:
    return {k: v for k, v in dictionary.items() if filterer(k)}


def dfilter(filterer: Filterer[T], dictionary: DictLike[T, T]) -> dict[T, T]:
    return {k: v for k, v in _handle_dictlike(dictionary) if filterer(k) and filterer(v)}


def kvfilter(callable_: Callable[[tuple[K, V]], bool], d: DictLike[K, V]) -> dict[K, V]:
    return dict(filter(callable_, _handle_dictlike(d)))


def identity[T](x: T) -> T:
    return x


def fold_dictionaries[K, V](dicts: Iterable[dict[K, V]]) -> dict[K, V]:
    def _or(dict1: dict[K, V], dict2: dict[K, V]) -> dict[K, V]:
        return dict1 | dict2

    return reduce(_or, dicts)


def split_head[T](seq: Sequence[T]) -> tuple[T, Sequence[T]]:
    if not seq:
        raise ValueError(f"List {seq} is invalid because it cannot be properly split.")
    return seq[0], seq[1:]


def tail[T](seq: Sequence[T]) -> Sequence[T]:
    return seq[1:]
