from ctypes import *
import unittest
import struct

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]
signed_types = [c_byte, c_short, c_int, c_long, c_longlong]

bool_types = []

float_types = [c_double, c_float]

try:
    c_ulonglong
    c_longlong
except NameError:
    pass
else:
    unsigned_types.append(c_ulonglong)
    signed_types.append(c_longlong)

try:
    c_bool
except NameError:
    pass
else:
    bool_types.append(c_bool)

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

################################################################

class NumberTestCase(unittest.TestCase):

    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):
        from operator import truth
        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)

    @unittest.skip('test disabled')
    def test_valid_ranges(self):
        # invalid values of the correct type
        # raise ValueError (not OverflowError)
        for t, (l, h) in zip(unsigned_types, unsigned_ranges):
            self.assertRaises(ValueError, t, l-1)
            self.assertRaises(ValueError, t, h+1)

    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
        class FloatLike(object):
            def __float__(self):
                return 2.0
        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)

    def test_integers(self):
        class FloatLike(object):
            def __float__(self):
                return 2.0
        f = FloatLike()
        class IntLike(object):
            def __int__(self):
                return 2
        d = IntLike()
        class IndexLike(object):
            def __index__(self):
                return 2
        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):
        from array import array
        for t in signed_types + unsigned_types:
            # the array module doesn't support all format codes
            # (no 'q' or 'Q')
            try:
                array(t._type_)
            except ValueError:
                continue
            a = 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):
        from array import array
        for t in float_types:
            a = 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):
        from ctypes import c_char
        from array import array

        a = 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'?')

    # array does not support c_bool / 't'
    @unittest.skip('test disabled')
    def test_bool_from_address(self):
        from ctypes import c_bool
        from array import array
        a = array(c_bool._type_, [True])
        v = t.from_address(a.buffer_info()[0])
        self.assertEqual(v.value, a[0])
        self.assertEqual(type(v) is t)
        a[0] = False
        self.assertEqual(v.value, a[0])
        self.assertEqual(type(v) is t)

    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):
        import sys
        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)

    @unittest.skip('test disabled')
    def test_perf(self):
        check_perf()

from ctypes import _SimpleCData
class c_int_S(_SimpleCData):
    _type_ = "i"
    __slots__ = []

def run_test(rep, msg, func, arg=None):
##    items = [None] * rep
    items = range(rep)
    from time import perf_counter as clock
    if arg is not None:
        start = clock()
        for i in items:
            func(arg); func(arg); func(arg); func(arg); func(arg)
        stop = clock()
    else:
        start = clock()
        for i in items:
            func(); func(); func(); func(); func()
        stop = clock()
    print("%15s: %.2f us" % (msg, ((stop-start)*1e6/5/rep)))

def check_perf():
    # Construct 5 objects
    from ctypes import c_int

    REP = 200000

    run_test(REP, "int()", int)
    run_test(REP, "int(999)", int)
    run_test(REP, "c_int()", c_int)
    run_test(REP, "c_int(999)", c_int)
    run_test(REP, "c_int_S()", c_int_S)
    run_test(REP, "c_int_S(999)", c_int_S)

# Python 2.3 -OO, win2k, P4 700 MHz:
#
#          int(): 0.87 us
#       int(999): 0.87 us
#        c_int(): 3.35 us
#     c_int(999): 3.34 us
#      c_int_S(): 3.23 us
#   c_int_S(999): 3.24 us

# Python 2.2 -OO, win2k, P4 700 MHz:
#
#          int(): 0.89 us
#       int(999): 0.89 us
#        c_int(): 9.99 us
#     c_int(999): 10.02 us
#      c_int_S(): 9.87 us
#   c_int_S(999): 9.85 us

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