import distutils.ccompiler
import distutils.log
import distutils.sysconfig
import os
import platform
import re
import shutil
import subprocess
import tempfile
import textwrap
import types

import cffi


def local_build_flags(projdir, target):
    """Construct build flags for building against a checkout.

    :param projdir: The root directory of the deltachat-core-rust project.
    :param target: The rust build target, `debug` or `release`.
    """
    flags = types.SimpleNamespace()
    if platform.system() == 'Darwin':
        flags.libs = ['resolv', 'dl']
        flags.extra_link_args = [
                '-framework', 'CoreFoundation',
                '-framework', 'CoreServices',
                '-framework', 'Security',
        ]
    elif platform.system() == 'Linux':
        flags.libs = ['rt', 'dl', 'm']
        flags.extra_link_args = []
    else:
        raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
    target_dir = os.environ.get("CARGO_TARGET_DIR")
    if target_dir is None:
        target_dir = os.path.join(projdir, 'target')
    flags.objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
    assert os.path.exists(flags.objs[0]), flags.objs
    flags.incs = [os.path.join(projdir, 'deltachat-ffi')]
    return flags


def system_build_flags():
    """Construct build flags for building against an installed libdeltachat."""
    flags = types.SimpleNamespace()
    flags.libs = ['deltachat']
    flags.objs = []
    flags.incs = []
    flags.extra_link_args = []
    return flags


def extract_functions(flags):
    """Extract the function definitions from deltachat.h.

    This creates a .h file with a single `#include <deltachat.h>` line
    in it.  It then runs the C preprocessor to create an output file
    which contains all function definitions found in `deltachat.h`.
    """
    distutils.log.set_verbosity(distutils.log.INFO)
    cc = distutils.ccompiler.new_compiler(force=True)
    distutils.sysconfig.customize_compiler(cc)
    tmpdir = tempfile.mkdtemp()
    try:
        src_name = os.path.join(tmpdir, "include.h")
        dst_name = os.path.join(tmpdir, "expanded.h")
        with open(src_name, "w") as src_fp:
            src_fp.write('#include <deltachat.h>')
        cc.preprocess(source=src_name,
                      output_file=dst_name,
                      include_dirs=flags.incs,
                      macros=[('PY_CFFI', '1')])
        with open(dst_name, "r") as dst_fp:
            return dst_fp.read()
    finally:
        shutil.rmtree(tmpdir)


def find_header(flags):
    """Use the compiler to find the deltachat.h header location.

    This uses a small utility in deltachat.h to find the location of
    the header file location.
    """
    distutils.log.set_verbosity(distutils.log.INFO)
    cc = distutils.ccompiler.new_compiler(force=True)
    distutils.sysconfig.customize_compiler(cc)
    tmpdir = tempfile.mkdtemp()
    try:
        src_name = os.path.join(tmpdir, "where.c")
        obj_name = os.path.join(tmpdir, "where.o")
        dst_name = os.path.join(tmpdir, "where")
        with open(src_name, "w") as src_fp:
            src_fp.write(textwrap.dedent("""
                #include <stdio.h>
                #include <deltachat.h>

                int main(void) {
                    printf("%s", _dc_header_file_location());
                    return 0;
                }
            """))
        cwd = os.getcwd()
        try:
            os.chdir(tmpdir)
            cc.compile(sources=["where.c"],
                       include_dirs=flags.incs,
                       macros=[("PY_CFFI_INC", "1")])
        finally:
            os.chdir(cwd)
        cc.link_executable(objects=[obj_name],
                           output_progname="where",
                           output_dir=tmpdir)
        return subprocess.check_output(dst_name)
    finally:
        shutil.rmtree(tmpdir)


def extract_defines(flags):
    """Extract the required #DEFINEs from deltachat.h.

    Since #DEFINEs are interpreted by the C preprocessor we can not
    use the compiler to extract these and need to parse the header
    file ourselves.

    The defines are returned in a string that can be passed to CFFIs
    cdef() method.
    """
    header = find_header(flags)
    defines_re = re.compile(r"""
        \#define\s+   # The start of a define.
        (             # Begin capturing group which captures the define name.
            (?:       # A nested group which is not captured, this allows us
                      #    to build the list of prefixes to extract without
                      #    creation another capture group.
                DC_EVENT
                | DC_QR
                | DC_MSG
                | DC_LP
                | DC_EMPTY
                | DC_CERTCK
                | DC_STATE
                | DC_STR
                | DC_CONTACT_ID
                | DC_GCL
                | DC_GCM
                | DC_SOCKET
                | DC_CHAT
                | DC_PROVIDER
                | DC_KEY_GEN
                | DC_IMEX
                | DC_CONNECTIVITY
            )         # End of prefix matching
            _[\w_]+   # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
        )             # Close the capturing group, this contains
                      # the entire name e.g. DC_MSG_TEXT.
        \s+\S+        # Ensure there is whitespace followed by a value.
    """, re.VERBOSE)
    defines = []
    with open(header) as fp:
        for line in fp:
            match = defines_re.match(line)
            if match:
                defines.append(match.group(1))
    return '\n'.join('#define {} ...'.format(d) for d in defines)


def ffibuilder():
    projdir = os.environ.get('DCC_RS_DEV')
    if projdir:
        target = os.environ.get('DCC_RS_TARGET', 'release')
        flags = local_build_flags(projdir, target)
    else:
        flags = system_build_flags()
    builder = cffi.FFI()
    builder.set_source(
        'deltachat.capi',
        """
            #include <deltachat.h>
            int dc_event_has_string_data(int e)
            {
                return DC_EVENT_DATA2_IS_STRING(e);
            }
        """,
        include_dirs=flags.incs,
        libraries=flags.libs,
        extra_objects=flags.objs,
        extra_link_args=flags.extra_link_args,
    )
    builder.cdef("""
        typedef int... time_t;
        void free(void *ptr);
        extern int dc_event_has_string_data(int);
    """)
    function_defs = extract_functions(flags)
    defines = extract_defines(flags)
    builder.cdef(function_defs)
    builder.cdef(defines)
    return builder


if __name__ == '__main__':
    import os.path
    pkgdir = os.path.join(os.path.dirname(__file__), '..')
    builder = ffibuilder()
    builder.compile(tmpdir=pkgdir, verbose=True)
