import sys, os, imp, math, shutil
import py
from cffi import FFI, FFIError
from cffi.verifier import Verifier, _locate_engine_class, _get_so_suffixes
from cffi.ffiplatform import maybe_relative_path
from testing.udir import udir


class DistUtilsTest(object):
    def setup_class(self):
        self.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':
                self.lib_m = 'msvcrt'

    def teardown_class(self):
        if udir.isdir():
            udir.remove(ignore_errors=True)
        udir.ensure(dir=1)

    def test_locate_engine_class(self):
        cls = _locate_engine_class(FFI(), self.generic)
        if self.generic:
            # asked for the generic engine, which must not generate a
            # CPython extension module
            assert not cls._gen_python_module
        else:
            # asked for the CPython engine: check that we got it, unless
            # we are running on top of PyPy, where the generic engine is
            # always better
            if '__pypy__' not in sys.builtin_module_names:
                assert cls._gen_python_module

    def test_write_source(self):
        ffi = FFI()
        ffi.cdef("double sin(double x);")
        csrc = '/*hi there %s!*/\n#include <math.h>\n' % self
        v = Verifier(ffi, csrc, force_generic_engine=self.generic,
                     libraries=[self.lib_m])
        v.write_source()
        with open(v.sourcefilename, 'r') as f:
            data = f.read()
        assert csrc in data

    def test_write_source_explicit_filename(self):
        ffi = FFI()
        ffi.cdef("double sin(double x);")
        csrc = '/*hi there %s!*/\n#include <math.h>\n' % self
        v = Verifier(ffi, csrc, force_generic_engine=self.generic,
                     libraries=[self.lib_m])
        v.sourcefilename = filename = str(udir.join('write_source.c'))
        v.write_source()
        assert filename == v.sourcefilename
        with open(filename, 'r') as f:
            data = f.read()
        assert csrc in data

    def test_write_source_to_file_obj(self):
        ffi = FFI()
        ffi.cdef("double sin(double x);")
        csrc = '/*hi there %s!*/\n#include <math.h>\n' % self
        v = Verifier(ffi, csrc, force_generic_engine=self.generic,
                     libraries=[self.lib_m])
        try:
            from StringIO import StringIO
        except ImportError:
            from io import StringIO
        f = StringIO()
        v.write_source(file=f)
        assert csrc in f.getvalue()

    def test_compile_module(self):
        ffi = FFI()
        ffi.cdef("double sin(double x);")
        csrc = '/*hi there %s!*/\n#include <math.h>\n' % self
        v = Verifier(ffi, csrc, force_generic_engine=self.generic,
                     libraries=[self.lib_m])
        v.compile_module()
        assert v.get_module_name().startswith('_cffi_')
        if v.generates_python_module():
            mod = imp.load_dynamic(v.get_module_name(), v.modulefilename)
            assert hasattr(mod, '_cffi_setup')

    def test_compile_module_explicit_filename(self):
        ffi = FFI()
        ffi.cdef("double sin(double x);")
        csrc = '/*hi there %s!2*/\n#include <math.h>\n' % self
        v = Verifier(ffi, csrc, force_generic_engine=self.generic,
                     libraries=[self.lib_m])
        basename = self.__class__.__name__ + 'test_compile_module'
        v.modulefilename = filename = str(udir.join(basename + '.so'))
        v.compile_module()
        assert filename == v.modulefilename
        assert v.get_module_name() == basename
        if v.generates_python_module():
            mod = imp.load_dynamic(v.get_module_name(), v.modulefilename)
            assert hasattr(mod, '_cffi_setup')

    def test_name_from_checksum_of_cdef(self):
        names = []
        for csrc in ['double', 'double', 'float']:
            ffi = FFI()
            ffi.cdef("%s sin(double x);" % csrc)
            v = Verifier(ffi, "#include <math.h>",
                         force_generic_engine=self.generic,
                         libraries=[self.lib_m])
            names.append(v.get_module_name())
        assert names[0] == names[1] != names[2]

    def test_name_from_checksum_of_csrc(self):
        names = []
        for csrc in ['123', '123', '1234']:
            ffi = FFI()
            ffi.cdef("double sin(double x);")
            v = Verifier(ffi, csrc, force_generic_engine=self.generic)
            names.append(v.get_module_name())
        assert names[0] == names[1] != names[2]

    def test_load_library(self):
        ffi = FFI()
        ffi.cdef("double sin(double x);")
        csrc = '/*hi there %s!3*/\n#include <math.h>\n' % self
        v = Verifier(ffi, csrc, force_generic_engine=self.generic,
                     libraries=[self.lib_m])
        library = v.load_library()
        assert library.sin(12.3) == math.sin(12.3)

    def test_verifier_args(self):
        ffi = FFI()
        ffi.cdef("double sin(double x);")
        csrc = '/*hi there %s!4*/#include "test_verifier_args.h"\n' % self
        udir.join('test_verifier_args.h').write('#include <math.h>\n')
        v = Verifier(ffi, csrc, include_dirs=[str(udir)],
                     force_generic_engine=self.generic,
                     libraries=[self.lib_m])
        library = v.load_library()
        assert library.sin(12.3) == math.sin(12.3)

    def test_verifier_object_from_ffi(self):
        ffi = FFI()
        ffi.cdef("double sin(double x);")
        csrc = "/*6%s*/\n#include <math.h>" % self
        lib = ffi.verify(csrc, force_generic_engine=self.generic,
                         libraries=[self.lib_m])
        assert lib.sin(12.3) == math.sin(12.3)
        assert isinstance(ffi.verifier, Verifier)
        with open(ffi.verifier.sourcefilename, 'r') as f:
            data = f.read()
        assert csrc in data

    def test_extension_object(self):
        ffi = FFI()
        ffi.cdef("double sin(double x);")
        csrc = '/*7%s*/' % self + '''
    #include <math.h>
    #ifndef TEST_EXTENSION_OBJECT
    # error "define_macros missing"
    #endif
    '''
        lib = ffi.verify(csrc, define_macros=[('TEST_EXTENSION_OBJECT', '1')],
                         force_generic_engine=self.generic,
                         libraries=[self.lib_m])
        assert lib.sin(12.3) == math.sin(12.3)
        v = ffi.verifier
        ext = v.get_extension()
        assert 'distutils.extension.Extension' in str(ext.__class__) or \
               'setuptools.extension.Extension' in str(ext.__class__)
        assert ext.sources == [maybe_relative_path(v.sourcefilename)]
        assert ext.name == v.get_module_name()
        assert ext.define_macros == [('TEST_EXTENSION_OBJECT', '1')]

    def test_extension_forces_write_source(self):
        ffi = FFI()
        ffi.cdef("double sin(double x);")
        csrc = '/*hi there9!%s*/\n#include <math.h>\n' % self
        v = Verifier(ffi, csrc, force_generic_engine=self.generic,
                     libraries=[self.lib_m])
        assert not os.path.exists(v.sourcefilename)
        v.get_extension()
        assert os.path.exists(v.sourcefilename)

    def test_extension_object_extra_sources(self):
        ffi = FFI()
        ffi.cdef("double test1eoes(double x);")
        extra_source = str(udir.join('extension_extra_sources.c'))
        with open(extra_source, 'w') as f:
            f.write('double test1eoes(double x) { return x * 6.0; }\n')
        csrc = '/*9%s*/' % self + '''
        double test1eoes(double x);   /* or #include "extra_sources.h" */
        '''
        lib = ffi.verify(csrc, sources=[extra_source],
                         force_generic_engine=self.generic)
        assert lib.test1eoes(7.0) == 42.0
        v = ffi.verifier
        ext = v.get_extension()
        assert 'distutils.extension.Extension' in str(ext.__class__) or \
               'setuptools.extension.Extension' in str(ext.__class__)
        assert ext.sources == [maybe_relative_path(v.sourcefilename),
                               extra_source]
        assert ext.name == v.get_module_name()

    def test_install_and_reload_module(self, targetpackage='', ext_package=''):
        KEY = repr(self)
        if not hasattr(os, 'fork'):
            py.test.skip("test requires os.fork()")

        if targetpackage:
            udir.ensure(targetpackage, dir=1).ensure('__init__.py')
        sys.path.insert(0, str(udir))

        def make_ffi(**verifier_args):
            ffi = FFI()
            ffi.cdef("/* %s, %s, %s */" % (KEY, targetpackage, ext_package))
            ffi.cdef("double test1iarm(double x);")
            csrc = "double test1iarm(double x) { return x * 42.0; }"
            lib = ffi.verify(csrc, force_generic_engine=self.generic,
                             ext_package=ext_package,
                             **verifier_args)
            return ffi, lib

        childpid = os.fork()
        if childpid == 0:
            # in the child
            ffi, lib = make_ffi()
            assert lib.test1iarm(1.5) == 63.0
            # "install" the module by moving it into udir (/targetpackage)
            if targetpackage:
                target = udir.join(targetpackage)
            else:
                target = udir
            shutil.move(ffi.verifier.modulefilename, str(target))
            os._exit(0)
        # in the parent
        _, status = os.waitpid(childpid, 0)
        if not (os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0):
            raise AssertionError   # see error above in subprocess

        from cffi import ffiplatform
        prev_compile = ffiplatform.compile
        try:
            if targetpackage == ext_package:
                ffiplatform.compile = lambda *args: dont_call_me_any_more
            # won't find it in tmpdir, but should find it correctly
            # installed in udir
            ffi, lib = make_ffi()
            assert lib.test1iarm(0.5) == 21.0
        finally:
            ffiplatform.compile = prev_compile

    def test_install_and_reload_module_package(self):
        self.test_install_and_reload_module(targetpackage='foo_iarmp',
                                            ext_package='foo_iarmp')

    def test_install_and_reload_module_ext_package_not_found(self):
        self.test_install_and_reload_module(targetpackage='foo_epnf',
                                            ext_package='not_found')

    def test_tag(self):
        ffi = FFI()
        ffi.cdef("/* %s test_tag */ double test1tag(double x);" % self)
        csrc = "double test1tag(double x) { return x - 42.0; }"
        lib = ffi.verify(csrc, force_generic_engine=self.generic,
                         tag='xxtest_tagxx')
        assert lib.test1tag(143) == 101.0
        assert '_cffi_xxtest_tagxx_' in ffi.verifier.modulefilename

    def test_modulename(self):
        ffi = FFI()
        ffi.cdef("/* %s test_modulename */ double test1foo(double x);" % self)
        csrc = "double test1foo(double x) { return x - 63.0; }"
        modname = 'xxtest_modulenamexx%d' % (self.generic,)
        lib = ffi.verify(csrc, force_generic_engine=self.generic,
                         modulename=modname)
        assert lib.test1foo(143) == 80.0
        suffix = _get_so_suffixes()[0]
        fn1 = os.path.join(ffi.verifier.tmpdir, modname + '.c')
        fn2 = os.path.join(ffi.verifier.tmpdir, modname + suffix)
        assert ffi.verifier.sourcefilename == fn1
        assert ffi.verifier.modulefilename == fn2


class TestDistUtilsCPython(DistUtilsTest):
    generic = False

class TestDistUtilsGeneric(DistUtilsTest):
    generic = True
