"""Composition and predicate combinators: compose, all_of, any_of, none_of."""

from __future__ import annotations

from functools import reduce
from operator import not_
from typing import TYPE_CHECKING, Any, TypeVar

import attrs

if TYPE_CHECKING:
    from collections.abc import Callable, Sequence

    Composable = Callable[[Any], Any]
    Filterable = Callable[[Any], bool]

T = TypeVar("T")


@attrs.frozen
class _Compose:
    """
    Callable that applies a chain of functions.
    """

    fns: Sequence[Composable]

    def __call__(self, initial: Any) -> Any:
        """Apply the composed functions to *initial* left-to-right."""
        return reduce(_apply, self.fns, initial)


def _apply(value: Any, fn: Composable) -> Any:
    """
    Apply *fn* to *value*.
    """
    return fn(value)


def compose(*fns: Composable) -> Composable:
    """
    Return a callable composing *fns* left-to-right.
    """
    return _Compose(fns)


@attrs.frozen
class _AllOf:
    """
    Callable that returns *True* when all
    predicates pass.
    """

    fns: Sequence[Filterable]

    def __call__(self, item: Any) -> bool:
        """Return True if all predicates pass for *item*."""
        return all(fn(item) for fn in self.fns)


def all_of(*fns: Filterable) -> Filterable:
    """
    Return a predicate that is *True* when
    all *fns* return *True*.
    """
    return _AllOf(fns)


@attrs.frozen
class _AnyOf:
    """
    Callable that returns *True* when any
    predicate passes.
    """

    fns: Sequence[Filterable]

    def __call__(self, item: Any) -> bool:
        """Return True if any predicate passes for *item*."""
        return any(fn(item) for fn in self.fns)


def any_of(*fns: Filterable) -> Filterable:
    """
    Return a predicate that is *True* when
    any of *fns* returns *True*.
    """
    return _AnyOf(fns)


def none_of(*fns: Filterable) -> Filterable:
    """
    Return a predicate that is *True* when
    none of *fns* returns *True*.
    """
    return compose(_AnyOf(fns), not_)


def identity(x: T) -> T:
    """
    Return *x* unchanged.
    """
    return x


def invert(func: Filterable) -> Filterable:
    """
    Return the logical negation of *func*.
    """
    return compose(func, not_)
