import os
import shutil
import sys
import tempfile
from collections import defaultdict
from distutils import ccompiler
from importlib import import_module
from subprocess import check_output

from setuptools import Extension

USE_RELATIVE_PATHS = False

COMPILER_DIRECTIVES = {'language_level': 3,
                       'cdivision': True,
                       'wraparound': False,
                       'boundscheck': False,
                       'initializedcheck': False,
                       }

DEVICES = {
    "airspy": {"lib": "airspy", "test_function": "open"},
    "bladerf": {"lib": "bladeRF", "test_function": "bladerf_open",
                "api_header_include": "libbladeRF.h",
                "api_version_check_code":
                    """
                    #include<stdio.h>
                    #include<libbladeRF.h>
                    
                    int main(void) {
                    struct bladerf_version result; bladerf_version(&result);
                    printf("%f", result.major + result.minor/10.0 + result.patch/100.0);
                    return 0;}
                    """},
    "hackrf": {"lib": "hackrf", "test_function": "hackrf_init",
               "extras": {"HACKRF_MULTI_DEVICE_SUPPORT": "hackrf_open_by_serial"}},
    "limesdr": {"lib": "LimeSuite", "test_function": "LMS_GetDeviceList"},
    "plutosdr": {"lib": "iio", "test_function": "iio_create_default_context"},
    "rtlsdr": {"lib": "rtlsdr", "test_function": "rtlsdr_get_device_name",
               "extras": {"RTLSDR_BANDWIDTH_SUPPORT": "rtlsdr_set_tuner_bandwidth"}},
    # Use C only for USRP to avoid boost dependency
    "usrp": {"lib": "uhd", "test_function": "uhd_usrp_find", "language": "c"},
    "sdrplay": {"lib": "mir_sdr_api" if sys.platform == "win32" else "mirsdrapi-rsp",
                "test_function": "mir_sdr_ApiVersion"}
}


def compiler_has_function(compiler, function_name, libraries, library_dirs, include_dirs) -> bool:
    tmp_dir = tempfile.mkdtemp(prefix='urh-')
    devnull = old_stderr = None
    try:
        try:
            file_name = os.path.join(tmp_dir, '{}.c'.format(function_name))
            with open(file_name, 'w') as f:
                # declare function in order to prevent Clang 12 error (https://github.com/jopohl/urh/issues/811)
                f.write('void %s();\n' % function_name)
                f.write('int main(void) {\n')
                f.write('    %s();\n' % function_name)
                f.write('}\n')

            # Redirect stderr to /dev/null to hide any error messages from the compiler.
            devnull = open(os.devnull, 'w')
            old_stderr = os.dup(sys.stderr.fileno())
            os.dup2(devnull.fileno(), sys.stderr.fileno())
            objects = compiler.compile([file_name], include_dirs=include_dirs)
            compiler.link_executable(objects, os.path.join(tmp_dir, "a.out"), library_dirs=library_dirs,
                                     libraries=libraries)
        except Exception as e:
            return False
        return True
    finally:
        if old_stderr is not None:
            os.dup2(old_stderr, sys.stderr.fileno())
        if devnull is not None:
            devnull.close()
        shutil.rmtree(tmp_dir)


def check_api_version(compiler, api_version_code, libraries, library_dirs, include_dirs) -> float:
    tmp_dir = tempfile.mkdtemp(prefix='urh-')
    try:
        try:
            file_name = os.path.join(tmp_dir, 'get_api_version.c')
            with open(file_name, 'w') as f:
                f.write(api_version_code)

            objects = compiler.compile([file_name], include_dirs=include_dirs)
            check_api_program = os.path.join(tmp_dir, "check_api")
            compiler.link_executable(objects, check_api_program, library_dirs=library_dirs, libraries=libraries)

            env = os.environ.copy()
            env["PATH"] = os.pathsep.join(library_dirs) + os.pathsep + os.environ.get("PATH", "")

            result = float(check_output(check_api_program, env=env))
            print("    Automatic API version check succeeded.")
            return result
        except Exception as e:
            print("    API version check failed: {}".format(e))
            return 0.0
    finally:
        shutil.rmtree(tmp_dir)


def get_device_extensions_and_extras(library_dirs=None, include_dirs=None):
    library_dirs = [] if library_dirs is None else library_dirs

    cur_dir = os.path.dirname(os.path.realpath(__file__))
    include_dirs = [] if include_dirs is None else include_dirs

    device_extras = dict()

    if os.path.isdir(os.path.join(cur_dir, "lib/shared")):
        # Device libs are packaged, so we are in release mode
        include_dirs.insert(0, os.path.realpath(os.path.join(cur_dir, "lib/shared/include")))
        library_dirs.insert(0, os.path.realpath(os.path.join(cur_dir, "lib/shared")))

    if sys.platform == "darwin":
        for prefix in ["/usr/local", "/opt/homebrew"]:
            include_dirs.append(prefix + "/include")
            library_dirs.append(prefix + "/lib")

    result = []

    # None = automatic (depending on lib is installed)
    # 1 = install extension always
    # 0 = Do not install extension
    build_device_extensions = defaultdict(lambda: None)

    for dev_name in DEVICES:
        with_option = "--with-" + dev_name
        without_option = "--without-" + dev_name

        if with_option in sys.argv and without_option in sys.argv:
            print("ambiguous options for " + dev_name)
            sys.exit(1)
        elif without_option in sys.argv:
            build_device_extensions[dev_name] = 0
            sys.argv.remove(without_option)
        elif with_option in sys.argv:
            build_device_extensions[dev_name] = 1
            sys.argv.remove(with_option)

    sys.path.append(os.path.realpath(os.path.join(cur_dir, "lib")))

    compiler = ccompiler.new_compiler()
    for dev_name, params in DEVICES.items():
        if build_device_extensions[dev_name] == 0:
            print("Skipping native {0} support".format(dev_name))
            continue

        if build_device_extensions[dev_name] == 1:
            print("Enforcing native {0} support".format(dev_name))
        elif compiler_has_function(compiler, params["test_function"], (params["lib"],), library_dirs, include_dirs):
            print("Found {0} lib. Will compile with native {1} support".format(params["lib"], dev_name))
        else:
            print("Skipping native support for {0}".format(dev_name))
            continue

        device_extras.update(get_device_extras(compiler, dev_name, [params["lib"]], library_dirs, include_dirs))
        if "api_version_check_code" in params:
            env_name = dev_name.upper() + "_API_VERSION"
            ver = os.getenv(env_name)
            if ver is not None:
                try:
                    ver = float(ver)
                except Exception as e:
                    print("    Could not convert content of {} to float: {}".format(env_name, e))
                    print("    Will now try to automatically detect API version.")
                    ver = None
            else:
                print("    Environment variable {} is unset, try to automatically detect API version".format(env_name))

            if ver is None:
                ver = check_api_version(compiler, params["api_version_check_code"], (params["lib"],),
                                        library_dirs, include_dirs)
            device_extras[env_name] = ver
            print("    Using {}={}".format(env_name, ver))

        extension = get_device_extension(dev_name, [params["lib"]], library_dirs, include_dirs)
        result.append(extension)

    return result, device_extras


def get_device_extras(compiler, dev_name, libraries, library_dirs, include_dirs):
    try:
        extras = DEVICES[dev_name]["extras"]
    except KeyError:
        extras = dict()

    result = dict()

    for extra, func_name in extras.items():
        if compiler_has_function(compiler, func_name, libraries, library_dirs, include_dirs):
            result[extra] = 1
        else:
            print("Skipping {} as installed driver does not support it".format(extra))
            result[extra] = 0

    return result


def get_device_extension(dev_name: str, libraries: list, library_dirs: list, include_dirs: list):
    try:
        language = DEVICES[dev_name]["language"]
    except KeyError:
        language = "c++"

    cur_dir = os.path.dirname(os.path.realpath(__file__))
    if USE_RELATIVE_PATHS:
        # We need relative paths on windows
        cpp_file_path = "src/urh/dev/native/lib/{0}.pyx".format(dev_name)
    else:
        cpp_file_path = os.path.join(cur_dir, "lib", "{0}.pyx".format(dev_name))

    return Extension("urh.dev.native.lib." + dev_name,
                     [cpp_file_path],
                     libraries=libraries, library_dirs=library_dirs,
                     include_dirs=include_dirs, language=language)


def perform_health_check() -> str:
    result = []
    for device in sorted(DEVICES.keys()):
        try:
            _ = import_module("urh.dev.native.lib." + device)
            result.append(device + " -- OK")
        except ImportError as e:
            result.append(device + " -- ERROR: " + str(e))

    return "\n".join(result)


if __name__ == "__main__":
    from setuptools import setup

    if "-L" in sys.argv:
        library_directories = sys.argv[sys.argv.index("-L") + 1].split(":")
    else:
        library_directories = None

    if "-I" in sys.argv:
        include_directories = sys.argv[sys.argv.index("-I") + 1].split(":")
    else:
        include_directories = []

    import numpy as np

    include_directories.append(np.get_include())

    cur_dir = os.path.dirname(os.path.realpath(__file__))
    os.chdir(os.path.join(cur_dir, "..", "..", ".."))

    try:
        from Cython.Build import cythonize
    except ImportError:
        print("You need Cython to rebuild URH's device extensions. "
              "You can get it e.g. with python3 -m pip install cython.",
              file=sys.stderr)
        sys.exit(1)

    dev_extensions, dev_extras = get_device_extensions_and_extras(library_dirs=library_directories,
                                                                  include_dirs=include_directories)
    setup(
        name="urh",
        ext_modules=cythonize(dev_extensions, force=True,
                              compile_time_env=dev_extras, compiler_directives=COMPILER_DIRECTIVES,
                              )
    )
