"""
Adds some strong-types for different int / uint sizes
"""

from functools import total_ordering
from typing import Generic, TypeVar, Union

_U = TypeVar("_U", bound=Union[int, float])

_T = TypeVar("_T", "UInt8", "UInt16", "UInt32", "Int8", "Int16", int, float)


def clip(value: _T, min_value: _T, max_value: _T) -> _T:
    return min(max(min_value, value), max_value)


@total_ordering
class BoundedNumber(Generic[_U]):
    """Generic number type which is clipped to be within a certain range.

    Incorporates total ordering so that members of this type can be compared directly.
    """

    def __init__(self, v: _U, min_value: _U, max_value: _U, raise_if_out_of_bounds: bool):
        if raise_if_out_of_bounds and (v < min_value or v > max_value):
            raise ValueError(f"{type(self)} must be within [{min_value}, {max_value}]. Got [{v}]")

        self._v = clip(v, min_value, max_value)

    def __eq__(self, other: object) -> bool:
        if isinstance(other, float) or isinstance(other, int):
            return self._v == other
        elif isinstance(other, self.__class__):
            return self._v == other._v
        return NotImplemented

    def __lt__(self, other: object) -> bool:
        if isinstance(other, float) or isinstance(other, int):
            return self._v < other
        elif isinstance(other, self.__class__):
            return self._v < other._v
        return NotImplemented

    @property
    def v(self) -> _U:
        return self._v


class Hex(BoundedNumber[int]):
    def __init__(self, v: float, raise_if_out_of_bounds: bool = False):
        super().__init__(int(v), 0, 15, raise_if_out_of_bounds)


class UInt8(BoundedNumber[int]):
    def __init__(self, v: float, raise_if_out_of_bounds: bool = False):
        super().__init__(int(v), 0, 255, raise_if_out_of_bounds)

    @staticmethod
    def from_hex_str(hex_str: str, raise_if_out_of_bounds: bool = False) -> "UInt8":
        return UInt8(int(hex_str, base=16), raise_if_out_of_bounds)


class Int8(BoundedNumber[int]):
    def __init__(self, v: float, raise_if_out_of_bounds: bool = False):
        super().__init__(int(v), -127, 128, raise_if_out_of_bounds)


class UInt16(BoundedNumber[int]):
    def __init__(self, v: float, raise_if_out_of_bounds: bool = False):
        super().__init__(int(v), 0, 65535, raise_if_out_of_bounds)


class Int16(BoundedNumber[int]):
    def __init__(self, v: float, raise_if_out_of_bounds: bool = False):
        super().__init__(int(v), -32768, 32767, raise_if_out_of_bounds)


class UInt32(BoundedNumber[int]):
    def __init__(self, v: float, raise_if_out_of_bounds: bool = False):
        super().__init__(int(v), 0, 2**32 - 1, raise_if_out_of_bounds)
