import os
import inflector
from distutils.version import StrictVersion
from waflib.ConfigSet import ConfigSet
from waflib import Utils

__all__ = [
    "check_pkg_config", "check_pkg_config_mixed", "check_pkg_config_mixed_all",
    "check_pkg_config_cflags", "check_cc", "check_statement", "check_libs",
    "check_headers", "compose_checks", "any_check", "check_true", "any_version",
    "load_fragment", "check_stub", "check_ctx_vars", "check_program",
    "check_pkg_config_datadir", "check_macos_sdk", "check_preprocessor"]

any_version = None

def even(n):
    return n % 2 == 0

def __define_options__(dependency_identifier):
    return inflector.define_dict(dependency_identifier)

def __merge_options__(dependency_identifier, *args):
    options_accu = inflector.storage_dict(dependency_identifier)
    options_accu['mandatory'] = False
    [options_accu.update(arg) for arg in args if arg]
    return options_accu

def _filter_cc_arguments(ctx, opts):
    if ctx.env.DEST_OS != Utils.unversioned_sys_platform():
        # cross compiling, remove execute=True if present
        if opts.get('execute'):
            opts['execute'] = False
    return opts

def check_program(name, var):
    def fn(ctx, dependency_identifier):
        return ctx.find_program(name, var=var, mandatory=False)
    return fn

def check_libs(libs, function):
    libs = [None] + libs
    def fn(ctx, dependency_identifier):
        for lib in libs:
            kwargs = lib and {'lib': lib} or {}
            if function(ctx, dependency_identifier, **kwargs):
                return True
        return False
    return fn

def check_preprocessor(header, expression, **kw_ext):
    def fn(ctx, dependency_identifier, **kw):
        headers = header
        if not isinstance(headers, list):
            headers = [header]
        hs = "\n".join(["#include <{0}>".format(h) for h in headers])
        fragment = ("{0}\n"
                    "#if !({1})\n#error\n#endif\n"
                    "int main(int argc, char **argv)\n"
                    "{{ return 0; }}").format(hs, expression)
        opts = __merge_options__(dependency_identifier,
                                 {'fragment':fragment},
                                 __define_options__(dependency_identifier),
                                 kw_ext, kw)
        return ctx.check_cc(**_filter_cc_arguments(ctx, opts))
    return fn

def check_statement(header, statement, **kw_ext):
    def fn(ctx, dependency_identifier, **kw):
        headers = header
        if not isinstance(headers, list):
            headers = [header]
        hs = "\n".join(["#include <{0}>".format(h) for h in headers])
        fragment = ("{0}\n"
                    "int main(int argc, char **argv)\n"
                    "{{ {1}; return 0; }}").format(hs, statement)
        opts = __merge_options__(dependency_identifier,
                                 {'fragment':fragment},
                                 __define_options__(dependency_identifier),
                                 kw_ext, kw)
        return ctx.check_cc(**_filter_cc_arguments(ctx, opts))
    return fn

def check_cc(**kw_ext):
    def fn(ctx, dependency_identifier, **kw):
        options = __merge_options__(dependency_identifier,
                                    __define_options__(dependency_identifier),
                                    kw_ext, kw)
        return ctx.check_cc(**_filter_cc_arguments(ctx, options))
    return fn

def check_pkg_config(*args, **kw_ext):
    return _check_pkg_config([], ["--libs", "--cflags"], *args, **kw_ext)

def check_pkg_config_mixed(_dyn_libs, *args, **kw_ext):
    return _check_pkg_config([_dyn_libs], ["--libs", "--cflags"], *args, **kw_ext)

def check_pkg_config_mixed_all(*all_args, **kw_ext):
    args = [all_args[i] for i in [n for n in range(0, len(all_args)) if n % 3]]
    return _check_pkg_config(all_args[::3], ["--libs", "--cflags"], *args, **kw_ext)

def check_pkg_config_cflags(*args, **kw_ext):
    return _check_pkg_config([], ["--cflags"], *args, **kw_ext)

def check_pkg_config_datadir(*args, **kw_ext):
    return _check_pkg_config([], ["--variable=pkgdatadir"], *args, **kw_ext)

def _check_pkg_config(_dyn_libs, _pkgc_args, *args, **kw_ext):
    def fn(ctx, dependency_identifier, **kw):
        argsl     = list(args)
        packages  = args[::2]
        verchecks = args[1::2]
        sargs     = []
        pkgc_args = _pkgc_args
        dyn_libs  = {}
        for i in range(0, len(packages)):
            if i < len(verchecks):
                sargs.append(packages[i] + ' ' + verchecks[i])
            else:
                sargs.append(packages[i])
            if _dyn_libs and _dyn_libs[i]:
                dyn_libs[packages[i]] = _dyn_libs[i]
        if ctx.dependency_satisfied('static-build') and not dyn_libs:
            pkgc_args += ["--static"]

        defaults = {
            'path':    ctx.env.PKG_CONFIG,
            'package': " ".join(packages),
            'args':    sargs + pkgc_args }
        opts = __merge_options__(dependency_identifier, defaults, kw_ext, kw)

        # Warning! Megahack incoming: when parsing flags in `parse_flags` waf
        # uses append_unique. This appends the flags only if they aren't
        # already present in the list. This causes breakage if one checks for
        # multiple pkg-config packages in a single call as stuff like -lm is
        # added only at its first occurrence.
        original_append_unique  = ConfigSet.append_unique
        ConfigSet.append_unique = ConfigSet.append_value
        result = ctx.check_cfg(**opts)
        ConfigSet.append_unique = original_append_unique

        defkey = inflector.define_key(dependency_identifier)
        if result:
            ctx.define(defkey, 1)
            for x in dyn_libs.keys():
                ctx.env['LIB_'+x] += dyn_libs[x]
        else:
            ctx.add_optional_message(dependency_identifier,
                                     "'{0}' not found".format(" ".join(sargs)))
            ctx.undefine(defkey)
        return result
    return fn

def check_headers(*headers, **kw_ext):
    def undef_others(ctx, headers, found):
        not_found_hs = set(headers) - {found}
        for not_found_h in not_found_hs:
            ctx.undefine(inflector.define_key(not_found_h))

    def fn(ctx, dependency_identifier):
        for header in headers:
            defaults = {'header_name': header, 'features': 'c cprogram'}
            options  = __merge_options__(dependency_identifier, defaults, kw_ext)
            if ctx.check(**options):
                undef_others(ctx, headers, header)
                ctx.define(inflector.define_key(dependency_identifier), 1)
                return True
        undef_others(ctx, headers, None)
        return False
    return fn

def check_true(ctx, dependency_identifier):
    ctx.define(inflector.define_key(dependency_identifier), 1)
    return True

def check_ctx_vars(*variables):
    def fn(ctx, dependency_identifier):
        missing = []
        for variable in variables:
            if variable not in ctx.env:
                missing.append(variable)

        if any(missing):
            ctx.add_optional_message(dependency_identifier,
                'missing {0}'.format(', '.join(missing)))
            return False
        else:
            return True

    return fn

def check_stub(ctx, dependency_identifier):
    ctx.undefine(inflector.define_key(dependency_identifier))
    return False

def compose_checks(*checks):
    def fn(ctx, dependency_identifier):
        return all([check(ctx, dependency_identifier) for check in checks])
    return fn

def any_check(*checks):
    def fn(ctx, dependency_identifier):
        return any(check(ctx, dependency_identifier) for check in checks)
    return fn

def load_fragment(fragment):
    file_path = os.path.join(os.path.dirname(__file__), '..', 'fragments',
                             fragment)
    fp = open(file_path,"r")
    fragment_code = fp.read()
    fp.close()
    return fragment_code

def check_macos_sdk(version):
    def fn(ctx, dependency_identifier):
        if ctx.env.MACOS_SDK_VERSION:
            if StrictVersion(ctx.env.MACOS_SDK_VERSION) >= StrictVersion(version):
                ctx.define(inflector.define_key(dependency_identifier), 1)
                return True
        return False

    return fn
