# SPDX-License-Identifier: Apache-2.0
# Copyright 2013-2019 The Meson development team

# This file contains the detection logic for miscellaneous external dependencies.
from __future__ import annotations

import functools
import re
import typing as T

from .. import mesonlib
from .. import mlog
from .base import DependencyException, DependencyMethods
from .base import BuiltinDependency, SystemDependency
from .cmake import CMakeDependency, CMakeDependencyFactory
from .configtool import ConfigToolDependency
from .detect import packages
from .factory import DependencyFactory, factory_methods
from .pkgconfig import PkgConfigDependency

if T.TYPE_CHECKING:
    from ..environment import Environment
    from .factory import DependencyGenerator


@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CMAKE})
def netcdf_factory(env: 'Environment',
                   for_machine: 'mesonlib.MachineChoice',
                   kwargs: T.Dict[str, T.Any],
                   methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
    language = kwargs.get('language', 'c')
    if language not in ('c', 'cpp', 'fortran'):
        raise DependencyException(f'Language {language} is not supported with NetCDF.')

    candidates: T.List['DependencyGenerator'] = []

    if DependencyMethods.PKGCONFIG in methods:
        if language == 'fortran':
            pkg = 'netcdf-fortran'
        else:
            pkg = 'netcdf'

        candidates.append(functools.partial(PkgConfigDependency, pkg, env, kwargs, language=language))

    if DependencyMethods.CMAKE in methods:
        candidates.append(functools.partial(CMakeDependency, 'NetCDF', env, kwargs, language=language))

    return candidates

packages['netcdf'] = netcdf_factory


class DlBuiltinDependency(BuiltinDependency):
    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, env, kwargs)
        self.feature_since = ('0.62.0', "consider checking for `dlopen` with and without `find_library('dl')`")

        if self.clib_compiler.has_function('dlopen', '#include <dlfcn.h>', env)[0]:
            self.is_found = True


class DlSystemDependency(SystemDependency):
    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, env, kwargs)
        self.feature_since = ('0.62.0', "consider checking for `dlopen` with and without `find_library('dl')`")

        h = self.clib_compiler.has_header('dlfcn.h', '', env)
        self.link_args = self.clib_compiler.find_library('dl', env, [], self.libtype)

        if h[0] and self.link_args:
            self.is_found = True


class OpenMPDependency(SystemDependency):
    # Map date of specification release (which is the macro value) to a version.
    VERSIONS = {
        '202111': '5.2',
        '202011': '5.1',
        '201811': '5.0',
        '201611': '5.0-revision1',  # This is supported by ICC 19.x
        '201511': '4.5',
        '201307': '4.0',
        '201107': '3.1',
        '200805': '3.0',
        '200505': '2.5',
        '200203': '2.0',
        '199810': '1.0',
    }

    def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
        language = kwargs.get('language')
        super().__init__('openmp', environment, kwargs, language=language)
        self.is_found = False
        if self.clib_compiler.get_id() == 'nagfor':
            # No macro defined for OpenMP, but OpenMP 3.1 is supported.
            self.version = '3.1'
            self.is_found = True
            self.compile_args = self.link_args = self.clib_compiler.openmp_flags(environment)
            return
        if self.clib_compiler.get_id() == 'pgi':
            # through at least PGI 19.4, there is no macro defined for OpenMP, but OpenMP 3.1 is supported.
            self.version = '3.1'
            self.is_found = True
            self.compile_args = self.link_args = self.clib_compiler.openmp_flags(environment)
            return

        # Set these now so they're available for the following compiler checks
        try:
            self.compile_args.extend(self.clib_compiler.openmp_flags(environment))
            self.link_args.extend(self.clib_compiler.openmp_link_flags(environment))
        except mesonlib.MesonException as e:
            mlog.warning('OpenMP support not available because:', str(e), fatal=False)
            return

        try:
            openmp_date = self.clib_compiler.get_define(
                '_OPENMP', '', self.env, [], [self], disable_cache=True)[0]
        except mesonlib.EnvironmentException as e:
            mlog.debug('OpenMP support not available in the compiler')
            mlog.debug(e)
            return

        try:
            self.version = self.VERSIONS[openmp_date]
        except KeyError:
            mlog.debug(f'Could not find an OpenMP version matching {openmp_date}')
            if openmp_date == '_OPENMP':
                mlog.debug('This can be caused by flags such as gcc\'s `-fdirectives-only`, which affect preprocessor behavior.')
            return

        # Flang has omp_lib.h
        header_names = ('omp.h', 'omp_lib.h')
        for name in header_names:
            if self.clib_compiler.has_header(name, '', self.env, dependencies=[self], disable_cache=True)[0]:
                self.is_found = True
                break
        else:
            mlog.warning('OpenMP found but omp.h missing.', fatal=False)

packages['openmp'] = OpenMPDependency


class ThreadDependency(SystemDependency):
    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
        super().__init__(name, environment, kwargs)
        self.is_found = True
        # Happens if you are using a language with threads
        # concept without C, such as plain Cuda.
        if not self.clib_compiler:
            self.compile_args = []
            self.link_args = []
        else:
            self.compile_args = self.clib_compiler.thread_flags(environment)
            self.link_args = self.clib_compiler.thread_link_flags(environment)


class BlocksDependency(SystemDependency):
    def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]) -> None:
        super().__init__('blocks', environment, kwargs)
        self.name = 'blocks'
        self.is_found = False

        if self.env.machines[self.for_machine].is_darwin():
            self.compile_args = []
            self.link_args = []
        else:
            self.compile_args = ['-fblocks']
            self.link_args = ['-lBlocksRuntime']

            if not self.clib_compiler.has_header('Block.h', '', environment, disable_cache=True) or \
               not self.clib_compiler.find_library('BlocksRuntime', environment, []):
                mlog.log(mlog.red('ERROR:'), 'BlocksRuntime not found.')
                return

        source = '''
            int main(int argc, char **argv)
            {
                int (^callback)(void) = ^ int (void) { return 0; };
                return callback();
            }'''

        with self.clib_compiler.compile(source, extra_args=self.compile_args + self.link_args) as p:
            if p.returncode != 0:
                mlog.log(mlog.red('ERROR:'), 'Compiler does not support blocks extension.')
                return

            self.is_found = True

packages['blocks'] = BlocksDependency


class PcapDependencyConfigTool(ConfigToolDependency):

    tools = ['pcap-config']
    tool_name = 'pcap-config'

    # version 1.10.2 added error checking for invalid arguments
    # version 1.10.3 will hopefully add actual support for --version
    skip_version = '--help'

    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, environment, kwargs)
        if not self.is_found:
            return
        self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
        self.link_args = self.get_config_value(['--libs'], 'link_args')
        if self.version is None:
            # older pcap-config versions don't support this
            self.version = self.get_pcap_lib_version()

    def get_pcap_lib_version(self) -> T.Optional[str]:
        # Since we seem to need to run a program to discover the pcap version,
        # we can't do that when cross-compiling
        # FIXME: this should be handled if we have an exe_wrapper
        if not self.env.machines.matches_build_machine(self.for_machine):
            return None

        v = self.clib_compiler.get_return_value('pcap_lib_version', 'string',
                                                '#include <pcap.h>', self.env, [], [self])
        v = re.sub(r'libpcap version ', '', str(v))
        v = re.sub(r' -- Apple version.*$', '', v)
        return v


class CupsDependencyConfigTool(ConfigToolDependency):

    tools = ['cups-config']
    tool_name = 'cups-config'

    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, environment, kwargs)
        if not self.is_found:
            return
        self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
        self.link_args = self.get_config_value(['--ldflags', '--libs'], 'link_args')


class LibWmfDependencyConfigTool(ConfigToolDependency):

    tools = ['libwmf-config']
    tool_name = 'libwmf-config'

    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, environment, kwargs)
        if not self.is_found:
            return
        self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
        self.link_args = self.get_config_value(['--libs'], 'link_args')


class LibGCryptDependencyConfigTool(ConfigToolDependency):

    tools = ['libgcrypt-config']
    tool_name = 'libgcrypt-config'

    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, environment, kwargs)
        if not self.is_found:
            return
        self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
        self.link_args = self.get_config_value(['--libs'], 'link_args')
        self.version = self.get_config_value(['--version'], 'version')[0]


class GpgmeDependencyConfigTool(ConfigToolDependency):

    tools = ['gpgme-config']
    tool_name = 'gpg-config'

    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, environment, kwargs)
        if not self.is_found:
            return
        self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
        self.link_args = self.get_config_value(['--libs'], 'link_args')
        self.version = self.get_config_value(['--version'], 'version')[0]


class ShadercDependency(SystemDependency):

    def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__('shaderc', environment, kwargs)

        static_lib = 'shaderc_combined'
        shared_lib = 'shaderc_shared'

        libs = [shared_lib, static_lib]
        if self.static:
            libs.reverse()

        cc = self.get_compiler()

        for lib in libs:
            self.link_args = cc.find_library(lib, environment, [])
            if self.link_args is not None:
                self.is_found = True

                if self.static and lib != static_lib:
                    mlog.warning(f'Static library {static_lib!r} not found for dependency '
                                 f'{self.name!r}, may not be statically linked')

                break


class CursesConfigToolDependency(ConfigToolDependency):

    """Use the curses config tools."""

    tool = 'curses-config'
    # ncurses5.4-config is for macOS Catalina
    tools = ['ncursesw6-config', 'ncursesw5-config', 'ncurses6-config', 'ncurses5-config', 'ncurses5.4-config']

    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None):
        super().__init__(name, env, kwargs, language)
        if not self.is_found:
            return
        self.compile_args = self.get_config_value(['--cflags'], 'compile_args')
        self.link_args = self.get_config_value(['--libs'], 'link_args')


class CursesSystemDependency(SystemDependency):

    """Curses dependency the hard way.

    This replaces hand rolled find_library() and has_header() calls. We
    provide this for portability reasons, there are a large number of curses
    implementations, and the differences between them can be very annoying.
    """

    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, env, kwargs)

        candidates = [
            ('pdcurses', ['pdcurses/curses.h']),
            ('ncursesw',  ['ncursesw/ncurses.h', 'ncurses.h']),
            ('ncurses',  ['ncurses/ncurses.h', 'ncurses/curses.h', 'ncurses.h']),
            ('curses',  ['curses.h']),
        ]

        # Not sure how else to elegantly break out of both loops
        for lib, headers in candidates:
            l = self.clib_compiler.find_library(lib, env, [])
            if l:
                for header in headers:
                    h = self.clib_compiler.has_header(header, '', env)
                    if h[0]:
                        self.is_found = True
                        self.link_args = l
                        # Not sure how to find version for non-ncurses curses
                        # implementations. The one in illumos/OpenIndiana
                        # doesn't seem to have a version defined in the header.
                        if lib.startswith('ncurses'):
                            v, _ = self.clib_compiler.get_define('NCURSES_VERSION', f'#include <{header}>', env, [], [self])
                            self.version = v.strip('"')
                        if lib.startswith('pdcurses'):
                            v_major, _ = self.clib_compiler.get_define('PDC_VER_MAJOR', f'#include <{header}>', env, [], [self])
                            v_minor, _ = self.clib_compiler.get_define('PDC_VER_MINOR', f'#include <{header}>', env, [], [self])
                            self.version = f'{v_major}.{v_minor}'

                        # Check the version if possible, emit a warning if we can't
                        req = kwargs.get('version')
                        if req:
                            if self.version:
                                self.is_found = mesonlib.version_compare(self.version, req)
                            else:
                                mlog.warning('Cannot determine version of curses to compare against.')

                        if self.is_found:
                            mlog.debug('Curses library:', l)
                            mlog.debug('Curses header:', header)
                            break
            if self.is_found:
                break


class IconvBuiltinDependency(BuiltinDependency):
    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, env, kwargs)
        self.feature_since = ('0.60.0', "consider checking for `iconv_open` with and without `find_library('iconv')`")
        code = '''#include <iconv.h>\n\nint main() {\n    iconv_open("","");\n}''' # [ignore encoding] this is C, not python, Mr. Lint

        if self.clib_compiler.links(code, env)[0]:
            self.is_found = True


class IconvSystemDependency(SystemDependency):
    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, env, kwargs)
        self.feature_since = ('0.60.0', "consider checking for `iconv_open` with and without find_library('iconv')")

        h = self.clib_compiler.has_header('iconv.h', '', env)
        self.link_args = self.clib_compiler.find_library('iconv', env, [], self.libtype)

        if h[0] and self.link_args:
            self.is_found = True


class IntlBuiltinDependency(BuiltinDependency):
    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, env, kwargs)
        self.feature_since = ('0.59.0', "consider checking for `ngettext` with and without `find_library('intl')`")
        code = '''#include <libintl.h>\n\nint main() {\n    gettext("Hello world");\n}'''

        if self.clib_compiler.links(code, env)[0]:
            self.is_found = True


class IntlSystemDependency(SystemDependency):
    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, env, kwargs)
        self.feature_since = ('0.59.0', "consider checking for `ngettext` with and without `find_library('intl')`")

        h = self.clib_compiler.has_header('libintl.h', '', env)
        self.link_args = self.clib_compiler.find_library('intl', env, [], self.libtype)

        if h[0] and self.link_args:
            self.is_found = True

            if self.static:
                if not self._add_sub_dependency(iconv_factory(env, self.for_machine, {'static': True})):
                    self.is_found = False


class OpensslSystemDependency(SystemDependency):
    def __init__(self, name: str, env: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__(name, env, kwargs)

        dependency_kwargs = {
            'method': 'system',
            'static': self.static,
        }
        if not self.clib_compiler.has_header('openssl/ssl.h', '', env)[0]:
            return

        # openssl >= 3 only
        self.version = self.clib_compiler.get_define('OPENSSL_VERSION_STR', '#include <openssl/opensslv.h>', env, [], [self])[0]
        # openssl < 3 only
        if not self.version:
            version_hex = self.clib_compiler.get_define('OPENSSL_VERSION_NUMBER', '#include <openssl/opensslv.h>', env, [], [self])[0]
            if not version_hex:
                return
            version_hex = version_hex.rstrip('L')
            version_ints = [((int(version_hex.rstrip('L'), 16) >> 4 + i) & 0xFF) for i in (24, 16, 8, 0)]
            # since this is openssl, the format is 1.2.3a in four parts
            self.version = '.'.join(str(i) for i in version_ints[:3]) + chr(ord('a') + version_ints[3] - 1)

        if name == 'openssl':
            if self._add_sub_dependency(libssl_factory(env, self.for_machine, dependency_kwargs)) and \
                    self._add_sub_dependency(libcrypto_factory(env, self.for_machine, dependency_kwargs)):
                self.is_found = True
            return
        else:
            self.link_args = self.clib_compiler.find_library(name.lstrip('lib'), env, [], self.libtype)
            if not self.link_args:
                return

        if not self.static:
            self.is_found = True
        else:
            if name == 'libssl':
                if self._add_sub_dependency(libcrypto_factory(env, self.for_machine, dependency_kwargs)):
                    self.is_found = True
            elif name == 'libcrypto':
                use_threads = self.clib_compiler.has_header_symbol('openssl/opensslconf.h', 'OPENSSL_THREADS', '', env, dependencies=[self])[0]
                if not use_threads or self._add_sub_dependency(threads_factory(env, self.for_machine, {})):
                    self.is_found = True
                # only relevant on platforms where it is distributed with the libc, in which case it always succeeds
                sublib = self.clib_compiler.find_library('dl', env, [], self.libtype)
                if sublib:
                    self.link_args.extend(sublib)


class ObjFWDependency(ConfigToolDependency):

    tools = ['objfw-config']
    tool_name = 'objfw-config'

    def __init__(self, environment: 'Environment', kwargs: T.Dict[str, T.Any]):
        super().__init__('objfw', environment, kwargs)
        self.feature_since = ('1.5.0', '')
        if not self.is_found:
            return

        # TODO: Expose --reexport
        # TODO: Expose --framework-libs
        extra_flags = []

        for module in mesonlib.stringlistify(mesonlib.extract_as_list(kwargs, 'modules')):
            extra_flags.append('--package')
            extra_flags.append(module)

        # TODO: Once Meson supports adding flags per language, only add --objcflags to ObjC
        self.compile_args = self.get_config_value(['--cppflags', '--cflags', '--objcflags'] + extra_flags, 'compile_args')
        self.link_args = self.get_config_value(['--ldflags', '--libs'] + extra_flags, 'link_args')


@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.SYSTEM})
def curses_factory(env: 'Environment',
                   for_machine: 'mesonlib.MachineChoice',
                   kwargs: T.Dict[str, T.Any],
                   methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
    candidates: T.List['DependencyGenerator'] = []

    if DependencyMethods.PKGCONFIG in methods:
        pkgconfig_files = ['pdcurses', 'ncursesw', 'ncurses', 'curses']
        for pkg in pkgconfig_files:
            candidates.append(functools.partial(PkgConfigDependency, pkg, env, kwargs))

    # There are path handling problems with these methods on msys, and they
    # don't apply to windows otherwise (cygwin is handled separately from
    # windows)
    if not env.machines[for_machine].is_windows():
        if DependencyMethods.CONFIG_TOOL in methods:
            candidates.append(functools.partial(CursesConfigToolDependency, 'curses', env, kwargs))

        if DependencyMethods.SYSTEM in methods:
            candidates.append(functools.partial(CursesSystemDependency, 'curses', env, kwargs))

    return candidates
packages['curses'] = curses_factory


@factory_methods({DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM})
def shaderc_factory(env: 'Environment',
                    for_machine: 'mesonlib.MachineChoice',
                    kwargs: T.Dict[str, T.Any],
                    methods: T.List[DependencyMethods]) -> T.List['DependencyGenerator']:
    """Custom DependencyFactory for ShaderC.

    ShaderC's odd you get three different libraries from the same build
    thing are just easier to represent as a separate function than
    twisting DependencyFactory even more.
    """
    candidates: T.List['DependencyGenerator'] = []

    if DependencyMethods.PKGCONFIG in methods:
        # ShaderC packages their shared and static libs together
        # and provides different pkg-config files for each one. We
        # smooth over this difference by handling the static
        # keyword before handing off to the pkg-config handler.
        shared_libs = ['shaderc']
        static_libs = ['shaderc_combined', 'shaderc_static']

        if kwargs.get('static', env.coredata.get_option(mesonlib.OptionKey('prefer_static'))):
            c = [functools.partial(PkgConfigDependency, name, env, kwargs)
                 for name in static_libs + shared_libs]
        else:
            c = [functools.partial(PkgConfigDependency, name, env, kwargs)
                 for name in shared_libs + static_libs]
        candidates.extend(c)

    if DependencyMethods.SYSTEM in methods:
        candidates.append(functools.partial(ShadercDependency, env, kwargs))

    return candidates
packages['shaderc'] = shaderc_factory


packages['cups'] = cups_factory = DependencyFactory(
    'cups',
    [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL, DependencyMethods.EXTRAFRAMEWORK, DependencyMethods.CMAKE],
    configtool_class=CupsDependencyConfigTool,
    cmake_name='Cups',
)

packages['dl'] = dl_factory = DependencyFactory(
    'dl',
    [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM],
    builtin_class=DlBuiltinDependency,
    system_class=DlSystemDependency,
)

packages['gpgme'] = gpgme_factory = DependencyFactory(
    'gpgme',
    [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
    configtool_class=GpgmeDependencyConfigTool,
)

packages['libgcrypt'] = libgcrypt_factory = DependencyFactory(
    'libgcrypt',
    [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
    configtool_class=LibGCryptDependencyConfigTool,
)

packages['libwmf'] = libwmf_factory = DependencyFactory(
    'libwmf',
    [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
    configtool_class=LibWmfDependencyConfigTool,
)

packages['pcap'] = pcap_factory = DependencyFactory(
    'pcap',
    [DependencyMethods.PKGCONFIG, DependencyMethods.CONFIG_TOOL],
    configtool_class=PcapDependencyConfigTool,
    pkgconfig_name='libpcap',
)

packages['threads'] = threads_factory = DependencyFactory(
    'threads',
    [DependencyMethods.SYSTEM, DependencyMethods.CMAKE],
    cmake_name='Threads',
    system_class=ThreadDependency,
)

packages['iconv'] = iconv_factory = DependencyFactory(
    'iconv',
    [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM],
    builtin_class=IconvBuiltinDependency,
    system_class=IconvSystemDependency,
)

packages['intl'] = intl_factory = DependencyFactory(
    'intl',
    [DependencyMethods.BUILTIN, DependencyMethods.SYSTEM],
    builtin_class=IntlBuiltinDependency,
    system_class=IntlSystemDependency,
)

packages['openssl'] = openssl_factory = DependencyFactory(
    'openssl',
    [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM, DependencyMethods.CMAKE],
    system_class=OpensslSystemDependency,
    cmake_class=CMakeDependencyFactory('OpenSSL', modules=['OpenSSL::Crypto', 'OpenSSL::SSL']),
)

packages['libcrypto'] = libcrypto_factory = DependencyFactory(
    'libcrypto',
    [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM, DependencyMethods.CMAKE],
    system_class=OpensslSystemDependency,
    cmake_class=CMakeDependencyFactory('OpenSSL', modules=['OpenSSL::Crypto']),
)

packages['libssl'] = libssl_factory = DependencyFactory(
    'libssl',
    [DependencyMethods.PKGCONFIG, DependencyMethods.SYSTEM, DependencyMethods.CMAKE],
    system_class=OpensslSystemDependency,
    cmake_class=CMakeDependencyFactory('OpenSSL', modules=['OpenSSL::SSL']),
)

packages['objfw'] = ObjFWDependency
