import array
import ctypes
import struct
import sys
import unittest
from itertools import combinations
from math import copysign, isnan
from operator import truth
from ctypes import (byref, sizeof, alignment,
                    c_char, c_byte, c_ubyte, c_short, c_ushort, c_int, c_uint,
                    c_long, c_ulong, c_longlong, c_ulonglong,
                    c_float, c_double, c_longdouble, c_bool)


def valid_ranges(*types):
    # given a sequence of numeric types, collect their _type_
    # attribute, which is a single format character compatible with
    # the struct module, use the struct module to calculate the
    # minimum and maximum value allowed for this format.
    # Returns a list of (min, max) values.
    result = []
    for t in types:
        fmt = t._type_
        size = struct.calcsize(fmt)
        a = struct.unpack(fmt, (b"\x00"*32)[:size])[0]
        b = struct.unpack(fmt, (b"\xFF"*32)[:size])[0]
        c = struct.unpack(fmt, (b"\x7F"+b"\x00"*32)[:size])[0]
        d = struct.unpack(fmt, (b"\x80"+b"\xFF"*32)[:size])[0]
        result.append((min(a, b, c, d), max(a, b, c, d)))
    return result


ArgType = type(byref(c_int(0)))

unsigned_types = [c_ubyte, c_ushort, c_uint, c_ulong, c_ulonglong]
signed_types = [c_byte, c_short, c_int, c_long, c_longlong]
bool_types = [c_bool]
float_types = [c_double, c_float]

unsigned_ranges = valid_ranges(*unsigned_types)
signed_ranges = valid_ranges(*signed_types)
bool_values = [True, False, 0, 1, -1, 5000, 'test', [], [1]]

class IntLike:
    def __int__(self):
        return 2

class IndexLike:
    def __index__(self):
        return 2

class FloatLike:
    def __float__(self):
        return 2.0

class ComplexLike:
    def __complex__(self):
        return 1+1j


INF = float("inf")
NAN = float("nan")


class NumberTestCase(unittest.TestCase):
    # from Lib/test/test_complex.py
    def assertFloatsAreIdentical(self, x, y):
        """assert that floats x and y are identical, in the sense that:
        (1) both x and y are nans, or
        (2) both x and y are infinities, with the same sign, or
        (3) both x and y are zeros, with the same sign, or
        (4) x and y are both finite and nonzero, and x == y

        """
        msg = 'floats {!r} and {!r} are not identical'

        if isnan(x) or isnan(y):
            if isnan(x) and isnan(y):
                return
        elif x == y:
            if x != 0.0:
                return
            # both zero; check that signs match
            elif copysign(1.0, x) == copysign(1.0, y):
                return
            else:
                msg += ': zeros have different signs'
        self.fail(msg.format(x, y))

    def assertComplexesAreIdentical(self, x, y):
        self.assertFloatsAreIdentical(x.real, y.real)
        self.assertFloatsAreIdentical(x.imag, y.imag)

    def test_default_init(self):
        # default values are set to zero
        for t in signed_types + unsigned_types + float_types:
            self.assertEqual(t().value, 0)

    def test_unsigned_values(self):
        # the value given to the constructor is available
        # as the 'value' attribute
        for t, (l, h) in zip(unsigned_types, unsigned_ranges):
            self.assertEqual(t(l).value, l)
            self.assertEqual(t(h).value, h)

    def test_signed_values(self):
        # see above
        for t, (l, h) in zip(signed_types, signed_ranges):
            self.assertEqual(t(l).value, l)
            self.assertEqual(t(h).value, h)

    def test_bool_values(self):
        for t, v in zip(bool_types, bool_values):
            self.assertEqual(t(v).value, truth(v))

    def test_typeerror(self):
        # Only numbers are allowed in the constructor,
        # otherwise TypeError is raised
        for t in signed_types + unsigned_types + float_types:
            self.assertRaises(TypeError, t, "")
            self.assertRaises(TypeError, t, None)

    def test_from_param(self):
        # the from_param class method attribute always
        # returns PyCArgObject instances
        for t in signed_types + unsigned_types + float_types:
            self.assertEqual(ArgType, type(t.from_param(0)))

    def test_byref(self):
        # calling byref returns also a PyCArgObject instance
        for t in signed_types + unsigned_types + float_types + bool_types:
            parm = byref(t())
            self.assertEqual(ArgType, type(parm))


    def test_floats(self):
        # c_float and c_double can be created from
        # Python int and float
        f = FloatLike()
        for t in float_types:
            self.assertEqual(t(2.0).value, 2.0)
            self.assertEqual(t(2).value, 2.0)
            self.assertEqual(t(2).value, 2.0)
            self.assertEqual(t(f).value, 2.0)

    @unittest.skipUnless(hasattr(ctypes, "c_double_complex"),
                         "requires C11 complex type")
    def test_complex(self):
        for t in [ctypes.c_double_complex, ctypes.c_float_complex,
                  ctypes.c_longdouble_complex]:
            self.assertEqual(t(1).value, 1+0j)
            self.assertEqual(t(1.0).value, 1+0j)
            self.assertEqual(t(1+0.125j).value, 1+0.125j)
            self.assertEqual(t(IndexLike()).value, 2+0j)
            self.assertEqual(t(FloatLike()).value, 2+0j)
            self.assertEqual(t(ComplexLike()).value, 1+1j)

    @unittest.skipUnless(hasattr(ctypes, "c_double_complex"),
                         "requires C11 complex type")
    def test_complex_round_trip(self):
        # Ensure complexes transformed exactly.  The CMPLX macro should
        # preserve special components (like inf/nan or signed zero).
        values = [complex(*_) for _ in combinations([1, -1, 0.0, -0.0, 2,
                                                     -3, INF, -INF, NAN], 2)]
        for z in values:
            for t in [ctypes.c_double_complex, ctypes.c_float_complex,
                      ctypes.c_longdouble_complex]:
                with self.subTest(z=z, type=t):
                    self.assertComplexesAreIdentical(z, t(z).value)

    def test_integers(self):
        f = FloatLike()
        d = IntLike()
        i = IndexLike()
        # integers cannot be constructed from floats,
        # but from integer-like objects
        for t in signed_types + unsigned_types:
            self.assertRaises(TypeError, t, 3.14)
            self.assertRaises(TypeError, t, f)
            self.assertRaises(TypeError, t, d)
            self.assertEqual(t(i).value, 2)

    def test_sizes(self):
        for t in signed_types + unsigned_types + float_types + bool_types:
            try:
                size = struct.calcsize(t._type_)
            except struct.error:
                continue
            # sizeof of the type...
            self.assertEqual(sizeof(t), size)
            # and sizeof of an instance
            self.assertEqual(sizeof(t()), size)

    def test_alignments(self):
        for t in signed_types + unsigned_types + float_types:
            code = t._type_ # the typecode
            align = struct.calcsize("c%c" % code) - struct.calcsize(code)

            # alignment of the type...
            self.assertEqual((code, alignment(t)),
                                 (code, align))
            # and alignment of an instance
            self.assertEqual((code, alignment(t())),
                                 (code, align))

    def test_int_from_address(self):
        for t in signed_types + unsigned_types:
            # the array module doesn't support all format codes
            # (no 'q' or 'Q')
            try:
                array.array(t._type_)
            except ValueError:
                continue
            a = array.array(t._type_, [100])

            # v now is an integer at an 'external' memory location
            v = t.from_address(a.buffer_info()[0])
            self.assertEqual(v.value, a[0])
            self.assertEqual(type(v), t)

            # changing the value at the memory location changes v's value also
            a[0] = 42
            self.assertEqual(v.value, a[0])


    def test_float_from_address(self):
        for t in float_types:
            a = array.array(t._type_, [3.14])
            v = t.from_address(a.buffer_info()[0])
            self.assertEqual(v.value, a[0])
            self.assertIs(type(v), t)
            a[0] = 2.3456e17
            self.assertEqual(v.value, a[0])
            self.assertIs(type(v), t)

    def test_char_from_address(self):
        a = array.array('b', [0])
        a[0] = ord('x')
        v = c_char.from_address(a.buffer_info()[0])
        self.assertEqual(v.value, b'x')
        self.assertIs(type(v), c_char)

        a[0] = ord('?')
        self.assertEqual(v.value, b'?')

    def test_init(self):
        # c_int() can be initialized from Python's int, and c_int.
        # Not from c_long or so, which seems strange, abc should
        # probably be changed:
        self.assertRaises(TypeError, c_int, c_long(42))

    def test_float_overflow(self):
        big_int = int(sys.float_info.max) * 2
        for t in float_types + [c_longdouble]:
            self.assertRaises(OverflowError, t, big_int)
            if (hasattr(t, "__ctype_be__")):
                self.assertRaises(OverflowError, t.__ctype_be__, big_int)
            if (hasattr(t, "__ctype_le__")):
                self.assertRaises(OverflowError, t.__ctype_le__, big_int)


if __name__ == '__main__':
    unittest.main()
