# Copyright 2013-2019 The Meson development team

# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

#     http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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

import functools
import os
import re
import subprocess
from pathlib import Path

from ..mesonlib import Popen_safe, OrderedSet, join_args
from ..programs import ExternalProgram
from .base import DependencyException, DependencyMethods
from .configtool import ConfigToolDependency
from .detect import packages
from .pkgconfig import PkgConfigDependency
from .factory import factory_methods
import typing as T

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


class HDF5PkgConfigDependency(PkgConfigDependency):

    """Handle brokenness in the HDF5 pkg-config files."""

    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
        language = language or 'c'
        if language not in {'c', 'cpp', 'fortran'}:
            raise DependencyException(f'Language {language} is not supported with HDF5.')

        super().__init__(name, environment, kwargs, language)
        if not self.is_found:
            return

        # some broken pkgconfig don't actually list the full path to the needed includes
        newinc = []  # type: T.List[str]
        for arg in self.compile_args:
            if arg.startswith('-I'):
                stem = 'static' if self.static else 'shared'
                if (Path(arg[2:]) / stem).is_dir():
                    newinc.append('-I' + str(Path(arg[2:]) / stem))
        self.compile_args += newinc

        link_args = []  # type: T.List[str]
        for larg in self.get_link_args():
            lpath = Path(larg)
            # some pkg-config hdf5.pc (e.g. Ubuntu) don't include the commonly-used HL HDF5 libraries,
            # so let's add them if they exist
            # additionally, some pkgconfig HDF5 HL files are malformed so let's be sure to find HL anyway
            if lpath.is_file():
                hl = []
                if language == 'cpp':
                    hl += ['_hl_cpp', '_cpp']
                elif language == 'fortran':
                    hl += ['_hl_fortran', 'hl_fortran', '_fortran']
                hl += ['_hl']  # C HL library, always needed

                suffix = '.' + lpath.name.split('.', 1)[1]  # in case of .dll.a
                for h in hl:
                    hlfn = lpath.parent / (lpath.name.split('.', 1)[0] + h + suffix)
                    if hlfn.is_file():
                        link_args.append(str(hlfn))
                # HDF5 C libs are required by other HDF5 languages
                link_args.append(larg)
            else:
                link_args.append(larg)

        self.link_args = link_args


class HDF5ConfigToolDependency(ConfigToolDependency):

    """Wrapper around hdf5 binary config tools."""

    version_arg = '-showconfig'

    def __init__(self, name: str, environment: 'Environment', kwargs: T.Dict[str, T.Any], language: T.Optional[str] = None) -> None:
        language = language or 'c'
        if language not in {'c', 'cpp', 'fortran'}:
            raise DependencyException(f'Language {language} is not supported with HDF5.')

        if language == 'c':
            cenv = 'CC'
            tools = ['h5cc', 'h5pcc']
        elif language == 'cpp':
            cenv = 'CXX'
            tools = ['h5c++', 'h5pc++']
        elif language == 'fortran':
            cenv = 'FC'
            tools = ['h5fc', 'h5pfc']
        else:
            raise DependencyException('How did you get here?')

        # We need this before we call super()
        for_machine = self.get_for_machine_from_kwargs(kwargs)

        nkwargs = kwargs.copy()
        nkwargs['tools'] = tools

        # Override the compiler that the config tools are going to use by
        # setting the environment variables that they use for the compiler and
        # linkers.
        compiler = environment.coredata.compilers[for_machine][language]
        try:
            os.environ[f'HDF5_{cenv}'] = join_args(compiler.get_exelist())
            os.environ[f'HDF5_{cenv}LINKER'] = join_args(compiler.get_linker_exelist())
            super().__init__(name, environment, nkwargs, language)
        finally:
            del os.environ[f'HDF5_{cenv}']
            del os.environ[f'HDF5_{cenv}LINKER']
        if not self.is_found:
            return

        # We first need to call the tool with -c to get the compile arguments
        # and then without -c to get the link arguments.
        args = self.get_config_value(['-show', '-c'], 'args')[1:]
        args += self.get_config_value(['-show', '-noshlib' if self.static else '-shlib'], 'args')[1:]
        for arg in args:
            if arg.startswith(('-I', '-f', '-D')) or arg == '-pthread':
                self.compile_args.append(arg)
            elif arg.startswith(('-L', '-l', '-Wl')):
                self.link_args.append(arg)
            elif Path(arg).is_file():
                self.link_args.append(arg)

        # If the language is not C we need to add C as a subdependency
        if language != 'c':
            nkwargs = kwargs.copy()
            nkwargs['language'] = 'c'
            # I'm being too clever for mypy and pylint
            self.is_found = self._add_sub_dependency(hdf5_factory(environment, for_machine, nkwargs))  # pylint: disable=no-value-for-parameter

    def _sanitize_version(self, ver: str) -> str:
        v = re.search(r'\s*HDF5 Version: (\d+\.\d+\.\d+)', ver)
        return v.group(1)


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

    if DependencyMethods.PKGCONFIG in methods:
        # Use an ordered set so that these remain the first tried pkg-config files
        pkgconfig_files = OrderedSet(['hdf5', 'hdf5-serial'])
        PCEXE = PkgConfigDependency._detect_pkgbin(False, env, for_machine)
        pcenv = PkgConfigDependency.setup_env(os.environ, env, for_machine)
        if PCEXE:
            assert isinstance(PCEXE, ExternalProgram)
            # some distros put hdf5-1.2.3.pc with version number in .pc filename.
            ret, stdout, _ = Popen_safe(PCEXE.get_command() + ['--list-all'], stderr=subprocess.DEVNULL, env=pcenv)
            if ret.returncode == 0:
                for pkg in stdout.split('\n'):
                    if pkg.startswith('hdf5'):
                        pkgconfig_files.add(pkg.split(' ', 1)[0])

        for pkg in pkgconfig_files:
            candidates.append(functools.partial(HDF5PkgConfigDependency, pkg, env, kwargs, language))

    if DependencyMethods.CONFIG_TOOL in methods:
        candidates.append(functools.partial(HDF5ConfigToolDependency, 'hdf5', env, kwargs, language))

    return candidates

packages['hdf5'] = hdf5_factory
