# SPDX-License-Identifier: Apache-2.0
# Copyright 2019-2022 The meson development team

from __future__ import annotations

"""Abstractions for the LLVM/Clang compiler family."""

import os
import shutil
import typing as T

from ... import mesonlib
from ... import options
from ...linkers.linkers import AppleDynamicLinker, ClangClDynamicLinker, LLVMDynamicLinker, GnuGoldDynamicLinker, \
    MoldDynamicLinker, MSVCDynamicLinker
from ...options import OptionKey
from ..compilers import CompileCheckMode
from .gnu import GnuLikeCompiler

if T.TYPE_CHECKING:
    from ...options import MutableKeyedOptionDictType
    from ...environment import Environment
    from ...dependencies import Dependency  # noqa: F401
    from ..compilers import Compiler

    CompilerMixinBase = Compiler
else:
    CompilerMixinBase = object

clang_color_args: T.Dict[str, T.List[str]] = {
    'auto': ['-fdiagnostics-color=auto'],
    'always': ['-fdiagnostics-color=always'],
    'never': ['-fdiagnostics-color=never'],
}

clang_optimization_args: T.Dict[str, T.List[str]] = {
    'plain': [],
    '0': ['-O0'],
    'g': ['-Og'],
    '1': ['-O1'],
    '2': ['-O2'],
    '3': ['-O3'],
    's': ['-Oz'],
}

clang_lang_map = {
    'c': 'c',
    'cpp': 'c++',
    'objc': 'objective-c',
    'objcpp': 'objective-c++',
}

class ClangCompiler(GnuLikeCompiler):

    id = 'clang'

    def __init__(self, defines: T.Optional[T.Dict[str, str]]):
        super().__init__()
        self.defines = defines or {}
        self.base_options.update(
            {OptionKey('b_colorout'), OptionKey('b_lto_threads'), OptionKey('b_lto_mode'), OptionKey('b_thinlto_cache'),
             OptionKey('b_thinlto_cache_dir')})

        # TODO: this really should be part of the linker base_options, but
        # linkers don't have base_options.
        if isinstance(self.linker, AppleDynamicLinker):
            self.base_options.add(OptionKey('b_bitcode'))
        elif isinstance(self.linker, MSVCDynamicLinker):
            self.base_options.add(OptionKey('b_vscrt'))
        # All Clang backends can also do LLVM IR
        self.can_compile_suffixes.add('ll')

    def get_colorout_args(self, colortype: str) -> T.List[str]:
        return clang_color_args[colortype][:]

    def has_builtin_define(self, define: str) -> bool:
        return define in self.defines

    def get_builtin_define(self, define: str) -> T.Optional[str]:
        return self.defines.get(define)

    def get_optimization_args(self, optimization_level: str) -> T.List[str]:
        return clang_optimization_args[optimization_level]

    def get_pch_suffix(self) -> str:
        return 'pch'

    def get_pch_use_args(self, pch_dir: str, header: str) -> T.List[str]:
        # Workaround for Clang bug http://llvm.org/bugs/show_bug.cgi?id=15136
        # This flag is internal to Clang (or at least not documented on the man page)
        # so it might change semantics at any time.
        return ['-include-pch', os.path.join(pch_dir, self.get_pch_name(header))]

    def get_compiler_check_args(self, mode: CompileCheckMode) -> T.List[str]:
        # Clang is different than GCC, it will return True when a symbol isn't
        # defined in a header. Specifically this is caused by a functionality
        # both GCC and clang have: for some "well known" functions, arbitrarily
        # chosen, they provide fixit suggestions for the header you should try
        # including.
        #
        # - With GCC, this is a note appended to the prexisting diagnostic
        #   "error: undeclared identifier"
        #
        # - With clang, the error is converted to a c89'ish implicit function
        #   declaration instead, which can be disabled with -Wno-error and on
        #   clang < 16, simply passes compilation by default.
        #
        # One example of a clang fixit suggestion is for `strlcat`, which
        # triggers this.
        #
        # This was reported in 2017 and promptly fixed. Just kidding!
        # https://github.com/llvm/llvm-project/issues/33905
        myargs: T.List[str] = ['-Werror=implicit-function-declaration']
        if mode is CompileCheckMode.COMPILE:
            myargs.extend(['-Werror=unknown-warning-option', '-Werror=unused-command-line-argument'])
            if mesonlib.version_compare(self.version, '>=3.6.0'):
                myargs.append('-Werror=ignored-optimization-argument')
        return super().get_compiler_check_args(mode) + myargs

    def has_function(self, funcname: str, prefix: str, env: 'Environment', *,
                     extra_args: T.Optional[T.List[str]] = None,
                     dependencies: T.Optional[T.List['Dependency']] = None) -> T.Tuple[bool, bool]:
        if extra_args is None:
            extra_args = []
        # Starting with XCode 8, we need to pass this to force linker
        # visibility to obey OS X/iOS/tvOS minimum version targets with
        # -mmacosx-version-min, -miphoneos-version-min, -mtvos-version-min etc.
        # https://github.com/Homebrew/homebrew-core/issues/3727
        # TODO: this really should be communicated by the linker
        if isinstance(self.linker, AppleDynamicLinker) and mesonlib.version_compare(self.version, '>=8.0'):
            extra_args.append('-Wl,-no_weak_imports')
        return super().has_function(funcname, prefix, env, extra_args=extra_args,
                                    dependencies=dependencies)

    def openmp_flags(self, env: Environment) -> T.List[str]:
        if mesonlib.version_compare(self.version, '>=3.8.0'):
            return ['-fopenmp']
        elif mesonlib.version_compare(self.version, '>=3.7.0'):
            return ['-fopenmp=libomp']
        else:
            # Shouldn't work, but it'll be checked explicitly in the OpenMP dependency.
            return []

    def gen_vs_module_defs_args(self, defsfile: str) -> T.List[str]:
        if isinstance(self.linker, (ClangClDynamicLinker, MSVCDynamicLinker)):
            # With MSVC, DLLs only export symbols that are explicitly exported,
            # so if a module defs file is specified, we use that to export symbols
            return ['-Wl,/DEF:' + defsfile]
        return super().gen_vs_module_defs_args(defsfile)

    @classmethod
    def use_linker_args(cls, linker: str, version: str) -> T.List[str]:
        # Clang additionally can use a linker specified as a path, which GCC
        # (and other gcc-like compilers) cannot. This is because clang (being
        # llvm based) is retargetable, while GCC is not.
        #

        # eld: Qualcomm's opensource embedded linker
        if linker == 'eld':
            return ['-fuse-ld=eld']
        # qcld: Qualcomm's deprecated linker
        if linker == 'qcld':
            return ['-fuse-ld=qcld']
        if linker == 'mold':
            return ['-fuse-ld=mold']

        if shutil.which(linker):
            if not shutil.which(linker):
                raise mesonlib.MesonException(
                    f'Cannot find linker {linker}.')
            return [f'-fuse-ld={linker}']
        return super().use_linker_args(linker, version)

    def get_has_func_attribute_extra_args(self, name: str) -> T.List[str]:
        # Clang only warns about unknown or ignored attributes, so force an
        # error.
        return ['-Werror=attributes']

    def get_coverage_link_args(self) -> T.List[str]:
        return ['--coverage']

    def get_lto_compile_args(self, *, threads: int = 0, mode: str = 'default') -> T.List[str]:
        args: T.List[str] = []
        if mode == 'thin':
            # ThinLTO requires the use of gold, lld, ld64, lld-link or mold 1.1+
            if isinstance(self.linker, (MoldDynamicLinker)):
                # https://github.com/rui314/mold/commit/46995bcfc3e3113133620bf16445c5f13cd76a18
                if not mesonlib.version_compare(self.linker.version, '>=1.1'):
                    raise mesonlib.MesonException("LLVM's ThinLTO requires mold 1.1+")
            elif not isinstance(self.linker, (AppleDynamicLinker, ClangClDynamicLinker, LLVMDynamicLinker, GnuGoldDynamicLinker)):
                raise mesonlib.MesonException(f"LLVM's ThinLTO only works with gold, lld, lld-link, ld64 or mold, not {self.linker.id}")
            args.append(f'-flto={mode}')
        else:
            assert mode == 'default', 'someone forgot to wire something up'
            args.extend(super().get_lto_compile_args(threads=threads))
        return args

    def linker_to_compiler_args(self, args: T.List[str]) -> T.List[str]:
        if isinstance(self.linker, (ClangClDynamicLinker, MSVCDynamicLinker)):
            return [flag if flag.startswith('-Wl,') or flag.startswith('-fuse-ld=') else f'-Wl,{flag}' for flag in args]
        else:
            return args

    def get_lto_link_args(self, *, threads: int = 0, mode: str = 'default',
                          thinlto_cache_dir: T.Optional[str] = None) -> T.List[str]:
        args = self.get_lto_compile_args(threads=threads, mode=mode)
        if mode == 'thin' and thinlto_cache_dir is not None:
            # We check for ThinLTO linker support above in get_lto_compile_args, and all of them support
            # get_thinlto_cache_args as well
            args.extend(self.linker.get_thinlto_cache_args(thinlto_cache_dir))
        # In clang -flto-jobs=0 means auto, and is the default if unspecified, just like in meson
        if threads > 0:
            if not mesonlib.version_compare(self.version, '>=4.0.0'):
                raise mesonlib.MesonException('clang support for LTO threads requires clang >=4.0')
            args.append(f'-flto-jobs={threads}')
        return args


class ClangCStds(CompilerMixinBase):

    """Mixin class for clang based compilers for setting C standards.

    This is used by both ClangCCompiler and ClangClCompiler, as they share
    the same versions
    """

    _C17_VERSION = '>=6.0.0'
    _C18_VERSION = '>=8.0.0'
    _C2X_VERSION = '>=9.0.0'
    _C23_VERSION = '>=18.0.0'
    _C2Y_VERSION = '>=19.0.0'

    def get_options(self) -> MutableKeyedOptionDictType:
        opts = super().get_options()
        stds = ['c89', 'c99', 'c11']
        # https://releases.llvm.org/6.0.0/tools/clang/docs/ReleaseNotes.html
        # https://en.wikipedia.org/wiki/Xcode#Latest_versions
        if mesonlib.version_compare(self.version, self._C17_VERSION):
            stds += ['c17']
        if mesonlib.version_compare(self.version, self._C18_VERSION):
            stds += ['c18']
        if mesonlib.version_compare(self.version, self._C2X_VERSION):
            stds += ['c2x']
        if mesonlib.version_compare(self.version, self._C23_VERSION):
            stds += ['c23']
        if mesonlib.version_compare(self.version, self._C2Y_VERSION):
            stds += ['c2y']
        key = self.form_compileropt_key('std')
        std_opt = opts[key]
        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
        std_opt.set_versions(stds, gnu=True)
        return opts


class ClangCPPStds(CompilerMixinBase):

    """Mixin class for clang based compilers for setting C++ standards.

    This is used by the ClangCPPCompiler
    """

    _CPP23_VERSION = '>=12.0.0'
    _CPP26_VERSION = '>=17.0.0'

    def get_options(self) -> MutableKeyedOptionDictType:
        opts = super().get_options()
        stds = [
            'c++98', 'c++03', 'c++11', 'c++14', 'c++17', 'c++1z', 'c++2a',
            'c++20',
        ]
        if mesonlib.version_compare(self.version, self._CPP23_VERSION):
            stds.append('c++23')
        if mesonlib.version_compare(self.version, self._CPP26_VERSION):
            stds.append('c++26')
        key = self.form_compileropt_key('std')
        std_opt = opts[key]
        assert isinstance(std_opt, options.UserStdOption), 'for mypy'
        std_opt.set_versions(stds, gnu=True)
        return opts
