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

import remedapy as R

TNum = TypeVar('TNum', int, float)
T = TypeVar('T')


@overload
def clamp(value: int, /, min: int, max: int) -> int: ...
@overload
def clamp(value: int, /, *, max: int) -> int: ...
@overload
def clamp(value: int, /, *, min: int) -> int: ...
@overload
def clamp(min: int, max: int, /) -> Callable[[TNum], TNum]: ...
@overload
def clamp(*, min: int) -> Callable[[TNum], TNum]: ...
@overload
def clamp(*, max: int) -> Callable[[TNum], TNum]: ...


@overload
def clamp(value: int | float, /, min: int | float, max: int | float) -> int | float: ...
@overload
def clamp(value: int | float, /, *, max: int | float) -> int | float: ...
@overload
def clamp(value: int | float, /, *, min: int | float) -> int | float: ...
@overload
def clamp(min: int | float, max: int | float, /) -> Callable[[int | float], int | float]: ...
@overload
def clamp(*, min: int | float) -> Callable[[int | float], int | float]: ...
@overload
def clamp(*, max: int | float) -> Callable[[int | float], int | float]: ...


@overload
def clamp(value: T, /, min: T, max: T) -> T: ...
@overload
def clamp(value: T, /, *, max: T) -> T: ...
@overload
def clamp(value: T, /, *, min: T) -> T: ...
@overload
def clamp(min: T, max: T, /) -> Callable[[T], T]: ...
@overload
def clamp(*, min: T) -> Callable[[T], T]: ...
@overload
def clamp(*, max: T) -> Callable[[T], T]: ...


def clamp(*args: int | float, **kwargs: int | float) -> int | float | Callable[[int | float], int | float]:  # pyright: ignore[reportInconsistentOverload]  # noqa: D417
    """
    Returns a number if it is within the range specified by the arguments, otherwise the closer boundary.

    Works like min and max at the same time.

    If you want to specify only one boundary you can do this, but only as a keyword.

    Parameters
    ----------
    value: int | float
        Value to clamp.
    min: int | float
        Minimal boundary.
    max: int | float
        Maximal boundary.

    Returns
    -------
    result: int | float
        Clamped value.

    Examples
    --------
    Data first:
    >>> R.clamp(10, 5, 15)
    10
    >>> R.clamp(20, 5, 15)
    15
    >>> R.clamp(2, 5, 15)
    5
    >>> R.clamp(10, min=5)
    10
    >>> R.clamp(2, min=5)
    5
    >>> R.clamp(2, max=15)
    2
    >>> R.clamp(20, max=15)
    15

    Data last:
    >>> R.clamp(5, 15)(10)
    10
    >>> R.clamp(5, 15)(20)
    15
    >>> R.clamp(5, 15)(2)
    5
    >>> R.clamp(min=5)(10)
    10
    >>> R.clamp(min=5)(2)
    5
    >>> R.clamp(max=15)(2)
    2
    >>> R.clamp(max=15)(20)
    15

    """
    if len(args) == 2 and not kwargs:
        return lambda x: clamp(x, *args)
    if not args:
        return lambda x: clamp(x, **kwargs)
    value = args[0]
    min_val = kwargs.get('min') or R.get(args, 1)
    max_val = kwargs.get('max') or R.get(args, 2)  # pyright: ignore
    if min_val is not None and value < min_val:
        return min_val  # pyright: ignore
    if max_val is not None and value > max_val:
        return max_val  # pyright: ignore
    return value  # pyright: ignore
