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

"""
from __future__ import division, absolute_import, print_function

__all__ = ['finfo', 'iinfo']

import warnings

from .machar import MachAr
from . import numeric
from . import numerictypes as ntypes
from .numeric import array, inf
from .umath import log10, exp2
from . import umath


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


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


_convert_to_float = {
    ntypes.csingle: ntypes.single,
    ntypes.complex_: ntypes.float_,
    ntypes.clongfloat: ntypes.longfloat
    }


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


class MachArLike(object):
    """ Object to simulate MachAr instance """

    def __init__(self,
                 ftype,
                 **kwargs):
        params = _MACHAR_PARAMS[ftype]
        float_conv = lambda v: array([v], ftype)
        float_to_float = lambda v : _fr1(float_conv(v))
        self._float_to_str = lambda v: (params['fmt'] %
                                        array(_fr0(v)[0], ftype))
        self.title = params['title']
        # Parameter types same as for discovered MachAr object.
        self.epsilon = self.eps = float_to_float(kwargs.pop('eps'))
        self.epsneg = float_to_float(kwargs.pop('epsneg'))
        self.xmax = self.huge = float_to_float(kwargs.pop('huge'))
        self.xmin = self.tiny = float_to_float(kwargs.pop('tiny'))
        self.ibeta = params['itype'](kwargs.pop('ibeta'))
        self.__dict__.update(kwargs)
        self.precision = int(-log10(self.eps))
        self.resolution = float_to_float(float_conv(10) ** (-self.precision))

    # Properties below to delay need for float_to_str, and thus avoid circular
    # imports during early numpy module loading.
    # See: https://github.com/numpy/numpy/pull/8983#discussion_r115838683

    @property
    def _str_eps(self):
        return self._float_to_str(self.eps)

    @property
    def _str_epsneg(self):
        return self._float_to_str(self.epsneg)

    @property
    def _str_xmin(self):
        return self._float_to_str(self.xmin)

    @property
    def _str_xmax(self):
        return self._float_to_str(self.xmax)

    @property
    def _str_resolution(self):
        return self._float_to_str(self.resolution)


# Known parameters for float16
# See docstring of MachAr class for description of parameters.
_f16 = ntypes.float16
_float16_ma = MachArLike(_f16,
                         machep=-10,
                         negep=-11,
                         minexp=-14,
                         maxexp=16,
                         it=10,
                         iexp=5,
                         ibeta=2,
                         irnd=5,
                         ngrd=0,
                         eps=exp2(_f16(-10)),
                         epsneg=exp2(_f16(-11)),
                         huge=_f16(65504),
                         tiny=_f16(2 ** -14))

# Known parameters for float32
_f32 = ntypes.float32
_float32_ma = MachArLike(_f32,
                         machep=-23,
                         negep=-24,
                         minexp=-126,
                         maxexp=128,
                         it=23,
                         iexp=8,
                         ibeta=2,
                         irnd=5,
                         ngrd=0,
                         eps=exp2(_f32(-23)),
                         epsneg=exp2(_f32(-24)),
                         huge=_f32((1 - 2 ** -24) * 2**128),
                         tiny=exp2(_f32(-126)))

# Known parameters for float64
_f64 = ntypes.float64
_epsneg_f64 = 2.0 ** -53.0
_tiny_f64 = 2.0 ** -1022.0
_float64_ma = MachArLike(_f64,
                         machep=-52,
                         negep=-53,
                         minexp=-1022,
                         maxexp=1024,
                         it=52,
                         iexp=11,
                         ibeta=2,
                         irnd=5,
                         ngrd=0,
                         eps=2.0 ** -52.0,
                         epsneg=_epsneg_f64,
                         huge=(1.0 - _epsneg_f64) / _tiny_f64 * _f64(4),
                         tiny=_tiny_f64)

# Known parameters for IEEE 754 128-bit binary float
_ld = ntypes.longdouble
_epsneg_f128 = exp2(_ld(-113))
_tiny_f128 = exp2(_ld(-16382))
# Ignore runtime error when this is not f128
with numeric.errstate(all='ignore'):
    _huge_f128 = (_ld(1) - _epsneg_f128) / _tiny_f128 * _ld(4)
_float128_ma = MachArLike(_ld,
                         machep=-112,
                         negep=-113,
                         minexp=-16382,
                         maxexp=16384,
                         it=112,
                         iexp=15,
                         ibeta=2,
                         irnd=5,
                         ngrd=0,
                         eps=exp2(_ld(-112)),
                         epsneg=_epsneg_f128,
                         huge=_huge_f128,
                         tiny=_tiny_f128)

# Known parameters for float80 (Intel 80-bit extended precision)
_epsneg_f80 = exp2(_ld(-64))
_tiny_f80 = exp2(_ld(-16382))
# Ignore runtime error when this is not f80
with numeric.errstate(all='ignore'):
    _huge_f80 = (_ld(1) - _epsneg_f80) / _tiny_f80 * _ld(4)
_float80_ma = MachArLike(_ld,
                         machep=-63,
                         negep=-64,
                         minexp=-16382,
                         maxexp=16384,
                         it=63,
                         iexp=15,
                         ibeta=2,
                         irnd=5,
                         ngrd=0,
                         eps=exp2(_ld(-63)),
                         epsneg=_epsneg_f80,
                         huge=_huge_f80,
                         tiny=_tiny_f80)

# Guessed / known parameters for double double; see:
# https://en.wikipedia.org/wiki/Quadruple-precision_floating-point_format#Double-double_arithmetic
# These numbers have the same exponent range as float64, but extended number of
# digits in the significand.
_huge_dd = (umath.nextafter(_ld(inf), _ld(0))
            if hasattr(umath, 'nextafter')  # Missing on some platforms?
            else _float64_ma.huge)
_float_dd_ma = MachArLike(_ld,
                          machep=-105,
                          negep=-106,
                          minexp=-1022,
                          maxexp=1024,
                          it=105,
                          iexp=11,
                          ibeta=2,
                          irnd=5,
                          ngrd=0,
                          eps=exp2(_ld(-105)),
                          epsneg= exp2(_ld(-106)),
                          huge=_huge_dd,
                          tiny=exp2(_ld(-1022)))


# Key to identify the floating point type.  Key is result of
# ftype('-0.1').newbyteorder('<').tobytes()
# See:
# https://perl5.git.perl.org/perl.git/blob/3118d7d684b56cbeb702af874f4326683c45f045:/Configure
_KNOWN_TYPES = {
    b'\x9a\x99\x99\x99\x99\x99\xb9\xbf' : _float64_ma,
    b'\xcd\xcc\xcc\xbd' : _float32_ma,
    b'f\xae' : _float16_ma,
    # float80, first 10 bytes containing actual storage
    b'\xcd\xcc\xcc\xcc\xcc\xcc\xcc\xcc\xfb\xbf' : _float80_ma,
    # double double; low, high order (e.g. PPC 64)
    b'\x9a\x99\x99\x99\x99\x99Y<\x9a\x99\x99\x99\x99\x99\xb9\xbf' :
    _float_dd_ma,
    # double double; high, low order (e.g. PPC 64 le)
    b'\x9a\x99\x99\x99\x99\x99\xb9\xbf\x9a\x99\x99\x99\x99\x99Y<' :
    _float_dd_ma,
    # IEEE 754 128-bit binary float
    b'\x9a\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\x99\xfb\xbf' :
    _float128_ma,
}


def _get_machar(ftype):
    """ Get MachAr instance or MachAr-like instance

    Get parameters for floating point type, by first trying signatures of
    various known floating point types, then, if none match, attempting to
    identify parameters by analysis.

    Parameters
    ----------
    ftype : class
        Numpy floating point type class (e.g. ``np.float64``)

    Returns
    -------
    ma_like : instance of :class:`MachAr` or :class:`MachArLike`
        Object giving floating point parameters for `ftype`.

    Warns
    -----
    UserWarning
        If the binary signature of the float type is not in the dictionary of
        known float types.
    """
    params = _MACHAR_PARAMS.get(ftype)
    if params is None:
        raise ValueError(repr(ftype))
    # Detect known / suspected types
    key = ftype('-0.1').newbyteorder('<').tobytes()
    ma_like = _KNOWN_TYPES.get(key)
    # Could be 80 bit == 10 byte extended precision, where last bytes can be
    # random garbage.  Try comparing first 10 bytes to pattern.
    if ma_like is None and ftype == ntypes.longdouble:
        ma_like = _KNOWN_TYPES.get(key[:10])
    if ma_like is not None:
        return ma_like
    # Fall back to parameter discovery
    warnings.warn(
        'Signature {} for {} does not match any known type: '
        'falling back to type probe function'.format(key, ftype),
        UserWarning, stacklevel=2)
    return _discovered_machar(ftype)


def _discovered_machar(ftype):
    """ Create MachAr instance with found information on float types
    """
    params = _MACHAR_PARAMS[ftype]
    return MachAr(lambda v: array([v], ftype),
                  lambda v:_fr0(v.astype(params['itype']))[0],
                  lambda v:array(_fr0(v)[0], ftype),
                  lambda v: params['fmt'] % array(_fr0(v)[0], ftype),
                  params['title'])


class finfo(object):
    """
    finfo(dtype)

    Machine limits for floating point types.

    Attributes
    ----------
    bits : int
        The number of bits occupied by the type.
    eps : float
        The smallest representable positive number such that
        ``1.0 + eps != 1.0``.  Type of `eps` is an appropriate floating
        point type.
    epsneg : floating point number of the appropriate type
        The smallest representable positive number such that
        ``1.0 - epsneg != 1.0``.
    iexp : int
        The number of bits in the exponent portion of the floating point
        representation.
    machar : MachAr
        The object which calculated these parameters and holds more
        detailed information.
    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.
    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.
    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 bits in the mantissa.
    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
        The smallest positive usable number.  Type of `tiny` is an
        appropriate floating point type.

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

    See Also
    --------
    MachAr : The implementation of the tests that produce this information.
    iinfo : The equivalent for integer data types.

    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.

    """

    _finfo_cache = {}

    def __new__(cls, dtype):
        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, None)
        if obj is not None:
            return obj
        dtypes = [dtype]
        newdtype = numeric.obj2sctype(dtype)
        if newdtype is not dtype:
            dtypes.append(newdtype)
            dtype = newdtype
        if not issubclass(dtype, numeric.inexact):
            raise ValueError("data type %r not inexact" % (dtype))
        obj = cls._finfo_cache.get(dtype, None)
        if obj is not None:
            return obj
        if not issubclass(dtype, numeric.floating):
            newdtype = _convert_to_float[dtype]
            if newdtype is not dtype:
                dtypes.append(newdtype)
                dtype = newdtype
        obj = cls._finfo_cache.get(dtype, None)
        if obj is not None:
            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)
        machar = _get_machar(dtype)

        for word in ['precision', 'iexp',
                     'maxexp', 'minexp', 'negep',
                     'machep']:
            setattr(self, word, getattr(machar, word))
        for word in ['tiny', 'resolution', 'epsneg']:
            setattr(self, word, getattr(machar, word).flat[0])
        self.bits = self.dtype.itemsize * 8
        self.max = machar.huge.flat[0]
        self.min = -self.max
        self.eps = machar.eps.flat[0]
        self.nexp = machar.iexp
        self.nmant = machar.it
        self.machar = machar
        self._str_tiny = machar._str_xmin.strip()
        self._str_max = machar._str_xmax.strip()
        self._str_epsneg = machar._str_epsneg.strip()
        self._str_eps = machar._str_eps.strip()
        self._str_resolution = machar._str_resolution.strip()
        return self

    def __str__(self):
        fmt = (
            'Machine parameters for %(dtype)s\n'
            '---------------------------------------------------------------\n'
            'precision = %(precision)3s   resolution = %(_str_resolution)s\n'
            'machep = %(machep)6s   eps =        %(_str_eps)s\n'
            'negep =  %(negep)6s   epsneg =     %(_str_epsneg)s\n'
            'minexp = %(minexp)6s   tiny =       %(_str_tiny)s\n'
            'maxexp = %(maxexp)6s   max =        %(_str_max)s\n'
            'nexp =   %(nexp)6s   min =        -max\n'
            '---------------------------------------------------------------\n'
            )
        return fmt % self.__dict__

    def __repr__(self):
        c = self.__class__.__name__
        d = self.__dict__.copy()
        d['klass'] = c
        return (("%(klass)s(resolution=%(resolution)s, min=-%(_str_max)s,"
                 " max=%(_str_max)s, dtype=%(dtype)s)") % d)


class iinfo(object):
    """
    iinfo(type)

    Machine limits for integer types.

    Attributes
    ----------
    bits : int
        The number of bits occupied by the type.
    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:

    >>> 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 = {}
    _max_vals = {}

    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 = "%s%d" % (self.kind, self.bits)
        if self.kind not in 'iu':
            raise ValueError("Invalid integer data type.")

    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

    min = property(min)

    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

    max = property(max)

    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):
        return "%s(min=%s, max=%s, dtype=%s)" % (self.__class__.__name__,
                                    self.min, self.max, self.dtype)

