import py
import pytest
from cffi import FFI, CDefError
import math, os, sys
import ctypes.util
from cffi.backend_ctypes import CTypesBackend
from testing.udir import udir
from testing.support import FdWriteCapture
from .backend_tests import needs_dlopen_none

try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO


lib_m = 'm'
if sys.platform == 'win32':
    #there is a small chance this fails on Mingw via environ $CC
    import distutils.ccompiler
    if distutils.ccompiler.get_default_compiler() == 'msvc':
        lib_m = 'msvcrt'

class TestFunction(object):
    Backend = CTypesBackend

    def test_sin(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            double sin(double x);
        """)
        m = ffi.dlopen(lib_m)
        x = m.sin(1.23)
        assert x == math.sin(1.23)

    def test_sinf(self):
        if sys.platform == 'win32':
            py.test.skip("no sinf found in the Windows stdlib")
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            float sinf(float x);
        """)
        m = ffi.dlopen(lib_m)
        x = m.sinf(1.23)
        assert type(x) is float
        assert x != math.sin(1.23)    # rounding effects
        assert abs(x - math.sin(1.23)) < 1E-6

    def test_getenv_no_return_value(self):
        # check that 'void'-returning functions work too
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            void getenv(char *);
        """)
        needs_dlopen_none()
        m = ffi.dlopen(None)
        x = m.getenv(b"FOO")
        assert x is None

    def test_dlopen_filename(self):
        path = ctypes.util.find_library(lib_m)
        if not path:
            py.test.skip("%s not found" % lib_m)
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            double cos(double x);
        """)
        m = ffi.dlopen(path)
        x = m.cos(1.23)
        assert x == math.cos(1.23)

        m = ffi.dlopen(os.path.basename(path))
        x = m.cos(1.23)
        assert x == math.cos(1.23)

    def test_dlopen_flags(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            double cos(double x);
        """)
        m = ffi.dlopen(lib_m, ffi.RTLD_LAZY | ffi.RTLD_LOCAL)
        x = m.cos(1.23)
        assert x == math.cos(1.23)

    def test_dlopen_constant(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            #define FOOBAR 42
            static const float baz = 42.5;   /* not visible */
            double sin(double x);
        """)
        m = ffi.dlopen(lib_m)
        assert m.FOOBAR == 42
        with pytest.raises(NotImplementedError):
            m.baz

    def test_tlsalloc(self):
        if sys.platform != 'win32':
            py.test.skip("win32 only")
        if self.Backend is CTypesBackend:
            py.test.skip("ctypes complains on wrong calling conv")
        ffi = FFI(backend=self.Backend())
        ffi.cdef("long TlsAlloc(void); int TlsFree(long);")
        lib = ffi.dlopen('KERNEL32.DLL')
        x = lib.TlsAlloc()
        assert x != 0
        y = lib.TlsFree(x)
        assert y != 0

    def test_fputs(self):
        if not sys.platform.startswith('linux'):
            py.test.skip("probably no symbol 'stderr' in the lib")
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            int fputs(const char *, void *);
            extern void *stderr;
        """)
        needs_dlopen_none()
        ffi.C = ffi.dlopen(None)
        ffi.C.fputs   # fetch before capturing, for easier debugging
        with FdWriteCapture() as fd:
            ffi.C.fputs(b"hello\n", ffi.C.stderr)
            ffi.C.fputs(b"  world\n", ffi.C.stderr)
        res = fd.getvalue()
        assert res == b'hello\n  world\n'

    def test_fputs_without_const(self):
        if not sys.platform.startswith('linux'):
            py.test.skip("probably no symbol 'stderr' in the lib")
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            int fputs(char *, void *);
            extern void *stderr;
        """)
        needs_dlopen_none()
        ffi.C = ffi.dlopen(None)
        ffi.C.fputs   # fetch before capturing, for easier debugging
        with FdWriteCapture() as fd:
            ffi.C.fputs(b"hello\n", ffi.C.stderr)
            ffi.C.fputs(b"  world\n", ffi.C.stderr)
        res = fd.getvalue()
        assert res == b'hello\n  world\n'

    def test_vararg(self):
        if not sys.platform.startswith('linux'):
            py.test.skip("probably no symbol 'stderr' in the lib")
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
           int fprintf(void *, const char *format, ...);
           extern void *stderr;
        """)
        needs_dlopen_none()
        ffi.C = ffi.dlopen(None)
        with FdWriteCapture() as fd:
            ffi.C.fprintf(ffi.C.stderr, b"hello with no arguments\n")
            ffi.C.fprintf(ffi.C.stderr,
                          b"hello, %s!\n", ffi.new("char[]", b"world"))
            ffi.C.fprintf(ffi.C.stderr,
                          ffi.new("char[]", b"hello, %s!\n"),
                          ffi.new("char[]", b"world2"))
            ffi.C.fprintf(ffi.C.stderr,
                          b"hello int %d long %ld long long %lld\n",
                          ffi.cast("int", 42),
                          ffi.cast("long", 84),
                          ffi.cast("long long", 168))
            ffi.C.fprintf(ffi.C.stderr, b"hello %p\n", ffi.NULL)
        res = fd.getvalue()
        assert res == (b"hello with no arguments\n"
                       b"hello, world!\n"
                       b"hello, world2!\n"
                       b"hello int 42 long 84 long long 168\n"
                       b"hello (nil)\n")

    def test_must_specify_type_of_vararg(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
           int printf(const char *format, ...);
        """)
        needs_dlopen_none()
        ffi.C = ffi.dlopen(None)
        e = py.test.raises(TypeError, ffi.C.printf, b"hello %d\n", 42)
        assert str(e.value) == ("argument 2 passed in the variadic part "
                                "needs to be a cdata object (got int)")

    def test_function_has_a_c_type(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            int puts(const char *);
        """)
        needs_dlopen_none()
        ffi.C = ffi.dlopen(None)
        fptr = ffi.C.puts
        assert ffi.typeof(fptr) == ffi.typeof("int(*)(const char*)")
        if self.Backend is CTypesBackend:
            assert repr(fptr).startswith("<cdata 'int puts(char *)' 0x")

    def test_function_pointer(self):
        ffi = FFI(backend=self.Backend())
        def cb(charp):
            assert repr(charp).startswith("<cdata 'char *' 0x")
            return 42
        fptr = ffi.callback("int(*)(const char *txt)", cb)
        assert fptr != ffi.callback("int(*)(const char *)", cb)
        assert repr(fptr) == "<cdata 'int(*)(char *)' calling %r>" % (cb,)
        res = fptr(b"Hello")
        assert res == 42
        #
        if not sys.platform.startswith('linux'):
            py.test.skip("probably no symbol 'stderr' in the lib")
        ffi.cdef("""
            int fputs(const char *, void *);
            extern void *stderr;
        """)
        needs_dlopen_none()
        ffi.C = ffi.dlopen(None)
        fptr = ffi.cast("int(*)(const char *txt, void *)", ffi.C.fputs)
        assert fptr == ffi.C.fputs
        assert repr(fptr).startswith("<cdata 'int(*)(char *, void *)' 0x")
        with FdWriteCapture() as fd:
            fptr(b"world\n", ffi.C.stderr)
        res = fd.getvalue()
        assert res == b'world\n'

    def test_callback_returning_void(self):
        ffi = FFI(backend=self.Backend())
        for returnvalue in [None, 42]:
            def cb():
                return returnvalue
            fptr = ffi.callback("void(*)(void)", cb)
            old_stderr = sys.stderr
            try:
                sys.stderr = StringIO()
                returned = fptr()
                printed = sys.stderr.getvalue()
            finally:
                sys.stderr = old_stderr
            assert returned is None
            if returnvalue is None:
                assert printed == ''
            else:
                assert "None" in printed

    def test_passing_array(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            int strlen(char[]);
        """)
        needs_dlopen_none()
        ffi.C = ffi.dlopen(None)
        p = ffi.new("char[]", b"hello")
        res = ffi.C.strlen(p)
        assert res == 5

    def test_write_variable(self):
        if not sys.platform.startswith('linux'):
            py.test.skip("probably no symbol 'stdout' in the lib")
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            extern void *stdout;
        """)
        needs_dlopen_none()
        C = ffi.dlopen(None)
        pout = C.stdout
        C.stdout = ffi.NULL
        assert C.stdout == ffi.NULL
        C.stdout = pout
        assert C.stdout == pout

    def test_strchr(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            char *strchr(const char *s, int c);
        """)
        needs_dlopen_none()
        ffi.C = ffi.dlopen(None)
        p = ffi.new("char[]", b"hello world!")
        q = ffi.C.strchr(p, ord('w'))
        assert ffi.string(q) == b"world!"

    def test_function_with_struct_argument(self):
        if sys.platform == 'win32':
            py.test.skip("no 'inet_ntoa'")
        if (self.Backend is CTypesBackend and
            '__pypy__' in sys.builtin_module_names):
            py.test.skip("ctypes limitation on pypy")
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            struct in_addr { unsigned int s_addr; };
            char *inet_ntoa(struct in_addr in);
        """)
        needs_dlopen_none()
        ffi.C = ffi.dlopen(None)
        ina = ffi.new("struct in_addr *", [0x04040404])
        a = ffi.C.inet_ntoa(ina[0])
        assert ffi.string(a) == b'4.4.4.4'

    def test_function_typedef(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            typedef double func_t(double);
            func_t sin;
        """)
        m = ffi.dlopen(lib_m)
        x = m.sin(1.23)
        assert x == math.sin(1.23)

    def test_fputs_custom_FILE(self):
        if self.Backend is CTypesBackend:
            py.test.skip("FILE not supported with the ctypes backend")
        filename = str(udir.join('fputs_custom_FILE'))
        ffi = FFI(backend=self.Backend())
        ffi.cdef("int fputs(const char *, FILE *);")
        needs_dlopen_none()
        C = ffi.dlopen(None)
        with open(filename, 'wb') as f:
            f.write(b'[')
            C.fputs(b"hello from custom file", f)
            f.write(b'][')
            C.fputs(b"some more output", f)
            f.write(b']')
        with open(filename, 'rb') as f:
            res = f.read()
        assert res == b'[hello from custom file][some more output]'

    def test_constants_on_lib(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""enum foo_e { AA, BB, CC=5, DD };
                    typedef enum { EE=-5, FF } some_enum_t;""")
        needs_dlopen_none()
        lib = ffi.dlopen(None)
        assert lib.AA == 0
        assert lib.BB == 1
        assert lib.CC == 5
        assert lib.DD == 6
        assert lib.EE == -5
        assert lib.FF == -4

    def test_void_star_accepts_string(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""int strlen(const void *);""")
        needs_dlopen_none()
        lib = ffi.dlopen(None)
        res = lib.strlen(b"hello")
        assert res == 5

    def test_signed_char_star_accepts_string(self):
        if self.Backend is CTypesBackend:
            py.test.skip("not supported by the ctypes backend")
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""int strlen(signed char *);""")
        needs_dlopen_none()
        lib = ffi.dlopen(None)
        res = lib.strlen(b"hello")
        assert res == 5

    def test_unsigned_char_star_accepts_string(self):
        if self.Backend is CTypesBackend:
            py.test.skip("not supported by the ctypes backend")
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""int strlen(unsigned char *);""")
        needs_dlopen_none()
        lib = ffi.dlopen(None)
        res = lib.strlen(b"hello")
        assert res == 5

    def test_missing_function(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            int nonexistent();
        """)
        m = ffi.dlopen(lib_m)
        assert not hasattr(m, 'nonexistent')

    def test_wraps_from_stdlib(self):
        import functools
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            double sin(double x);
        """)
        def my_decorator(f):
            @functools.wraps(f)
            def wrapper(*args):
                return f(*args) + 100
            return wrapper
        m = ffi.dlopen(lib_m)
        sin100 = my_decorator(m.sin)
        x = sin100(1.23)
        assert x == math.sin(1.23) + 100

    def test_free_callback_cycle(self):
        if self.Backend is CTypesBackend:
            py.test.skip("seems to fail with the ctypes backend on windows")
        import weakref
        def make_callback(data):
            container = [data]
            callback = ffi.callback('int()', lambda: len(container))
            container.append(callback)
            # Ref cycle: callback -> lambda (closure) -> container -> callback
            return callback

        class Data(object):
            pass
        ffi = FFI(backend=self.Backend())
        data = Data()
        callback = make_callback(data)
        wr = weakref.ref(data)
        del callback, data
        for i in range(3):
            if wr() is not None:
                import gc; gc.collect()
        assert wr() is None    # 'data' does not leak

    def test_windows_stdcall(self):
        if sys.platform != 'win32':
            py.test.skip("Windows-only test")
        if self.Backend is CTypesBackend:
            py.test.skip("not with the ctypes backend")
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            BOOL QueryPerformanceFrequency(LONGLONG *lpFrequency);
        """)
        m = ffi.dlopen("Kernel32.dll")
        p_freq = ffi.new("LONGLONG *")
        res = m.QueryPerformanceFrequency(p_freq)
        assert res != 0
        assert p_freq[0] != 0

    def test_explicit_cdecl_stdcall(self):
        if sys.platform != 'win32':
            py.test.skip("Windows-only test")
        if self.Backend is CTypesBackend:
            py.test.skip("not with the ctypes backend")
        win64 = (sys.maxsize > 2**32)
        #
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            BOOL QueryPerformanceFrequency(LONGLONG *lpFrequency);
        """)
        m = ffi.dlopen("Kernel32.dll")
        tp = ffi.typeof(m.QueryPerformanceFrequency)
        assert str(tp) == "<ctype 'int(*)(long long *)'>"
        #
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            BOOL __cdecl QueryPerformanceFrequency(LONGLONG *lpFrequency);
        """)
        m = ffi.dlopen("Kernel32.dll")
        tpc = ffi.typeof(m.QueryPerformanceFrequency)
        assert tpc is tp
        #
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            BOOL WINAPI QueryPerformanceFrequency(LONGLONG *lpFrequency);
        """)
        m = ffi.dlopen("Kernel32.dll")
        tps = ffi.typeof(m.QueryPerformanceFrequency)
        if win64:
            assert tps is tpc
        else:
            assert tps is not tpc
            assert str(tps) == "<ctype 'int(__stdcall *)(long long *)'>"
        #
        ffi = FFI(backend=self.Backend())
        ffi.cdef("typedef int (__cdecl *fnc_t)(int);")
        ffi.cdef("typedef int (__stdcall *fns_t)(int);")
        tpc = ffi.typeof("fnc_t")
        tps = ffi.typeof("fns_t")
        assert str(tpc) == "<ctype 'int(*)(int)'>"
        if win64:
            assert tps is tpc
        else:
            assert str(tps) == "<ctype 'int(__stdcall *)(int)'>"
        #
        fnc = ffi.cast("fnc_t", 0)
        fns = ffi.cast("fns_t", 0)
        ffi.new("fnc_t[]", [fnc])
        if not win64:
            py.test.raises(TypeError, ffi.new, "fnc_t[]", [fns])
            py.test.raises(TypeError, ffi.new, "fns_t[]", [fnc])
        ffi.new("fns_t[]", [fns])

    def test_stdcall_only_on_windows(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("double __stdcall sin(double x);")     # stdcall ignored
        m = ffi.dlopen(lib_m)
        if (sys.platform == 'win32' and sys.maxsize < 2**32 and
                self.Backend is not CTypesBackend):
            assert "double(__stdcall *)(double)" in str(ffi.typeof(m.sin))
        else:
            assert "double(*)(double)" in str(ffi.typeof(m.sin))
        x = m.sin(1.23)
        assert x == math.sin(1.23)

    def test_dir_on_dlopen_lib(self):
        ffi = FFI(backend=self.Backend())
        ffi.cdef("""
            typedef enum { MYE1, MYE2 } myenum_t;
            double myfunc(double);
            extern double myvar;
            const double myconst;
            #define MYFOO 42
        """)
        m = ffi.dlopen(lib_m)
        assert dir(m) == ['MYE1', 'MYE2', 'MYFOO', 'myconst', 'myfunc', 'myvar']

    def test_dlclose(self):
        if self.Backend is CTypesBackend:
            py.test.skip("not with the ctypes backend")
        ffi = FFI(backend=self.Backend())
        ffi.cdef("int foobar(void); extern int foobaz;")
        lib = ffi.dlopen(lib_m)
        ffi.dlclose(lib)
        e = py.test.raises(ValueError, getattr, lib, 'foobar')
        assert str(e.value).startswith("library '")
        assert str(e.value).endswith("' has already been closed")
        e = py.test.raises(ValueError, getattr, lib, 'foobaz')
        assert str(e.value).startswith("library '")
        assert str(e.value).endswith("' has already been closed")
        e = py.test.raises(ValueError, setattr, lib, 'foobaz', 42)
        assert str(e.value).startswith("library '")
        assert str(e.value).endswith("' has already been closed")
        ffi.dlclose(lib)    # does not raise
