"""Machine limits for Float32 and Float64 and (long double) if available...

"""
__all__ = ['finfo', 'iinfo']

import math
import types
from functools import cached_property

from numpy._utils import set_module

from . import numeric, numerictypes as ntypes
from ._multiarray_umath import _finfo_get_realdtype, _populate_finfo_constants


def _fr0(a):
    """fix rank-0 --> rank-1"""
    if a.ndim == 0:
        a = a.reshape((1,))
    return a


def _fr1(a):
    """fix rank > 0 --> rank-0"""
    if a.size == 1:
        a = a.reshape(())
    return a


# Parameters for creating MachAr / MachAr-like objects
_title_fmt = 'numpy {} precision floating point number'
_MACHAR_PARAMS = {
    ntypes.double: {
        'itype': ntypes.int64,
        'fmt': '%24.16e',
        'title': _title_fmt.format('double')},
    ntypes.single: {
        'itype': ntypes.int32,
        'fmt': '%15.7e',
        'title': _title_fmt.format('single')},
    ntypes.longdouble: {
        'itype': ntypes.longlong,
        'fmt': '%s',
        'title': _title_fmt.format('long double')},
    ntypes.half: {
        'itype': ntypes.int16,
        'fmt': '%12.5e',
        'title': _title_fmt.format('half')}}


@set_module('numpy')
class finfo:
    """
    finfo(dtype)

    Machine limits for floating point types.

    Attributes
    ----------
    bits : int
        The number of bits occupied by the type.
    dtype : dtype
        Returns the dtype for which `finfo` returns information. For complex
        input, the returned dtype is the associated ``float*`` dtype for its
        real and complex components.
    eps : float
        The difference between 1.0 and the next smallest representable float
        larger than 1.0. For example, for 64-bit binary floats in the IEEE-754
        standard, ``eps = 2**-52``, approximately 2.22e-16.
    epsneg : float
        The difference between 1.0 and the next smallest representable float
        less than 1.0. For example, for 64-bit binary floats in the IEEE-754
        standard, ``epsneg = 2**-53``, approximately 1.11e-16.
    iexp : int
        The number of bits in the exponent portion of the floating point
        representation.
    machep : int
        The exponent that yields `eps`.
    max : floating point number of the appropriate type
        The largest representable number.
    maxexp : int
        The smallest positive power of the base (2) that causes overflow.
        Corresponds to the C standard MAX_EXP.
    min : floating point number of the appropriate type
        The smallest representable number, typically ``-max``.
    minexp : int
        The most negative power of the base (2) consistent with there
        being no leading 0's in the mantissa. Corresponds to the C
        standard MIN_EXP - 1.
    negep : int
        The exponent that yields `epsneg`.
    nexp : int
        The number of bits in the exponent including its sign and bias.
    nmant : int
        The number of explicit bits in the mantissa (excluding the implicit
        leading bit for normalized numbers).
    precision : int
        The approximate number of decimal digits to which this kind of
        float is precise.
    resolution : floating point number of the appropriate type
        The approximate decimal resolution of this type, i.e.,
        ``10**-precision``.
    tiny : float
        An alias for `smallest_normal`, kept for backwards compatibility.
    smallest_normal : float
        The smallest positive floating point number with 1 as leading bit in
        the mantissa following IEEE-754 (see Notes).
    smallest_subnormal : float
        The smallest positive floating point number with 0 as leading bit in
        the mantissa following IEEE-754.

    Parameters
    ----------
    dtype : float, dtype, or instance
        Kind of floating point or complex floating point
        data-type about which to get information.

    See Also
    --------
    iinfo : The equivalent for integer data types.
    spacing : The distance between a value and the nearest adjacent number
    nextafter : The next floating point value after x1 towards x2

    Notes
    -----
    For developers of NumPy: do not instantiate this at the module level.
    The initial calculation of these parameters is expensive and negatively
    impacts import times.  These objects are cached, so calling ``finfo()``
    repeatedly inside your functions is not a problem.

    Note that ``smallest_normal`` is not actually the smallest positive
    representable value in a NumPy floating point type. As in the IEEE-754
    standard [1]_, NumPy floating point types make use of subnormal numbers to
    fill the gap between 0 and ``smallest_normal``. However, subnormal numbers
    may have significantly reduced precision [2]_.

    For ``longdouble``, the representation varies across platforms. On most
    platforms it is IEEE 754 binary128 (quad precision) or binary64-extended
    (80-bit extended precision). On PowerPC systems, it may use the IBM
    double-double format (a pair of float64 values), which has special
    characteristics for precision and range.

    This function can also be used for complex data types as well. If used,
    the output will be the same as the corresponding real float type
    (e.g. numpy.finfo(numpy.csingle) is the same as numpy.finfo(numpy.single)).
    However, the output is true for the real and imaginary components.

    References
    ----------
    .. [1] IEEE Standard for Floating-Point Arithmetic, IEEE Std 754-2008,
           pp.1-70, 2008, https://doi.org/10.1109/IEEESTD.2008.4610935
    .. [2] Wikipedia, "Denormal Numbers",
           https://en.wikipedia.org/wiki/Denormal_number

    Examples
    --------
    >>> import numpy as np
    >>> np.finfo(np.float64).dtype
    dtype('float64')
    >>> np.finfo(np.complex64).dtype
    dtype('float32')

    """

    _finfo_cache = {}  # noqa: RUF012

    __class_getitem__ = classmethod(types.GenericAlias)

    def __new__(cls, dtype):
        if dtype is None:
            raise TypeError("dtype must not be None")

        try:
            obj = cls._finfo_cache.get(dtype)  # most common path
            if obj is not None:
                return obj
        except TypeError:
            pass

        try:
            dtype = numeric.dtype(dtype)
        except TypeError:
            # In case a float instance was given
            dtype = numeric.dtype(type(dtype))

        obj = cls._finfo_cache.get(dtype)
        if obj is not None:
            return obj
        dtypes = [dtype]
        # Call result_type to normalize to e.g. native byte-order:
        newdtype = numeric.result_type(dtype)
        if newdtype is not dtype:
            dtypes.append(newdtype)
            dtype = newdtype

        obj = cls._finfo_cache.get(dtype)
        if obj is not None:
            return obj

        sctype = newdtype.type
        if sctype is not None and not issubclass(sctype, numeric.floating):
            newdtype = _finfo_get_realdtype(dtype)
            if newdtype is not dtype:
                # dtype changed, for example from complex128 to float64
                dtypes.append(newdtype)
                dtype = newdtype

                obj = cls._finfo_cache.get(dtype, None)
                if obj is not None:
                    # the original dtype was not in the cache, but the new
                    # dtype is in the cache. we add the original dtypes to
                    # the cache and return the result
                    for dt in dtypes:
                        cls._finfo_cache[dt] = obj
                    return obj
        obj = object.__new__(cls)._init(dtype)
        for dt in dtypes:
            cls._finfo_cache[dt] = obj
        return obj

    def _init(self, dtype):
        self.dtype = numeric.dtype(dtype)
        self.bits = self.dtype.itemsize * 8
        self._fmt = None
        self._repr = None
        _populate_finfo_constants(self, self.dtype)
        return self

    @cached_property
    def epsneg(self):
        # Assume typical floating point logic.  Could also use nextafter.
        return self.eps / self._radix

    @cached_property
    def resolution(self):
        return self.dtype.type(10)**-self.precision

    @cached_property
    def machep(self):
        return int(math.log2(self.eps))

    @cached_property
    def negep(self):
        return int(math.log2(self.epsneg))

    @cached_property
    def nexp(self):
        # considering all ones (inf/nan) and all zeros (subnormal/zero)
        return math.ceil(math.log2(self.maxexp - self.minexp + 2))

    @cached_property
    def iexp(self):
        # Calculate exponent bits from it's range:
        return math.ceil(math.log2(self.maxexp - self.minexp))

    def __str__(self):
        if (fmt := getattr(self, "_fmt", None)) is not None:
            return fmt

        def get_str(name, pad=None):
            if (val := getattr(self, name, None)) is None:
                return "<undefined>"
            if pad is not None:
                s = str(val).ljust(pad)
            return str(val)

        precision = get_str("precision", 3)
        machep = get_str("machep", 6)
        negep = get_str("negep", 6)
        minexp = get_str("minexp", 6)
        maxexp = get_str("maxexp", 6)
        resolution = get_str("resolution")
        eps = get_str("eps")
        epsneg = get_str("epsneg")
        tiny = get_str("tiny")
        smallest_normal = get_str("smallest_normal")
        smallest_subnormal = get_str("smallest_subnormal")
        nexp = get_str("nexp", 6)
        max_ = get_str("max")
        if hasattr(self, "min") and hasattr(self, "max") and -self.min == self.max:
            min_ = "-max"
        else:
            min_ = get_str("min")

        fmt = (
            f'Machine parameters for {self.dtype}\n'
            f'---------------------------------------------------------------\n'
            f'precision = {precision}   resolution = {resolution}\n'
            f'machep = {machep}   eps =        {eps}\n'
            f'negep =  {negep}   epsneg =     {epsneg}\n'
            f'minexp = {minexp}   tiny =       {tiny}\n'
            f'maxexp = {maxexp}   max =        {max_}\n'
            f'nexp =   {nexp}   min =        {min_}\n'
            f'smallest_normal = {smallest_normal}   '
            f'smallest_subnormal = {smallest_subnormal}\n'
            f'---------------------------------------------------------------\n'
        )
        self._fmt = fmt
        return fmt

    def __repr__(self):
        if (repr_str := getattr(self, "_repr", None)) is not None:
            return repr_str

        c = self.__class__.__name__

        # Use precision+1 digits in exponential notation
        fmt_str = _MACHAR_PARAMS.get(self.dtype.type, {}).get('fmt', '%s')
        if fmt_str != '%s' and hasattr(self, 'max') and hasattr(self, 'min'):
            max_str = (fmt_str % self.max).strip()
            min_str = (fmt_str % self.min).strip()
        else:
            max_str = str(self.max)
            min_str = str(self.min)

        resolution_str = str(self.resolution)

        repr_str = (f"{c}(resolution={resolution_str}, min={min_str},"
                    f" max={max_str}, dtype={self.dtype})")
        self._repr = repr_str
        return repr_str

    @cached_property
    def tiny(self):
        """Return the value for tiny, alias of smallest_normal.

        Returns
        -------
        tiny : float
            Value for the smallest normal, alias of smallest_normal.

        Warns
        -----
        UserWarning
            If the calculated value for the smallest normal is requested for
            double-double.
        """
        return self.smallest_normal


@set_module('numpy')
class iinfo:
    """
    iinfo(type)

    Machine limits for integer types.

    Attributes
    ----------
    bits : int
        The number of bits occupied by the type.
    dtype : dtype
        Returns the dtype for which `iinfo` returns information.
    min : int
        The smallest integer expressible by the type.
    max : int
        The largest integer expressible by the type.

    Parameters
    ----------
    int_type : integer type, dtype, or instance
        The kind of integer data type to get information about.

    See Also
    --------
    finfo : The equivalent for floating point data types.

    Examples
    --------
    With types:

    >>> import numpy as np
    >>> ii16 = np.iinfo(np.int16)
    >>> ii16.min
    -32768
    >>> ii16.max
    32767
    >>> ii32 = np.iinfo(np.int32)
    >>> ii32.min
    -2147483648
    >>> ii32.max
    2147483647

    With instances:

    >>> ii32 = np.iinfo(np.int32(10))
    >>> ii32.min
    -2147483648
    >>> ii32.max
    2147483647

    """

    _min_vals = {}  # noqa: RUF012
    _max_vals = {}  # noqa: RUF012

    __class_getitem__ = classmethod(types.GenericAlias)

    def __init__(self, int_type):
        try:
            self.dtype = numeric.dtype(int_type)
        except TypeError:
            self.dtype = numeric.dtype(type(int_type))
        self.kind = self.dtype.kind
        self.bits = self.dtype.itemsize * 8
        self.key = f"{self.kind}{self.bits}"
        if self.kind not in 'iu':
            raise ValueError(f"Invalid integer data type {self.kind!r}.")

    @property
    def min(self):
        """Minimum value of given dtype."""
        if self.kind == 'u':
            return 0
        else:
            try:
                val = iinfo._min_vals[self.key]
            except KeyError:
                val = int(-(1 << (self.bits - 1)))
                iinfo._min_vals[self.key] = val
            return val

    @property
    def max(self):
        """Maximum value of given dtype."""
        try:
            val = iinfo._max_vals[self.key]
        except KeyError:
            if self.kind == 'u':
                val = int((1 << self.bits) - 1)
            else:
                val = int((1 << (self.bits - 1)) - 1)
            iinfo._max_vals[self.key] = val
        return val

    def __str__(self):
        """String representation."""
        fmt = (
            'Machine parameters for %(dtype)s\n'
            '---------------------------------------------------------------\n'
            'min = %(min)s\n'
            'max = %(max)s\n'
            '---------------------------------------------------------------\n'
            )
        return fmt % {'dtype': self.dtype, 'min': self.min, 'max': self.max}

    def __repr__(self):
        name = self.__class__.__name__
        return f'{name}(min={self.min}, max={self.max}, dtype={self.dtype})'
