# Copyright 2012-2017 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.

import os.path, subprocess
import typing

from ..mesonlib import (
    EnvironmentException, MachineChoice, version_compare, is_windows, is_osx
)

from .compilers import (
    CompilerType,
    d_dmd_buildtype_args,
    d_gdc_buildtype_args,
    d_ldc_buildtype_args,
    clike_debug_args,
    Compiler,
    CompilerArgs,
)
from .mixins.gnu import GnuCompiler
from .mixins.islinker import LinkerEnvVarsMixin, BasicLinkerIsCompilerMixin

d_feature_args = {'gcc':  {'unittest': '-funittest',
                           'debug': '-fdebug',
                           'version': '-fversion',
                           'import_dir': '-J'
                           },
                  'llvm': {'unittest': '-unittest',
                           'debug': '-d-debug',
                           'version': '-d-version',
                           'import_dir': '-J'
                           },
                  'dmd':  {'unittest': '-unittest',
                           'debug': '-debug',
                           'version': '-version',
                           'import_dir': '-J'
                           }
                  }

ldc_optimization_args = {'0': [],
                         'g': [],
                         '1': ['-O1'],
                         '2': ['-O2'],
                         '3': ['-O3'],
                         's': ['-Os'],
                         }

dmd_optimization_args = {'0': [],
                         'g': [],
                         '1': ['-O'],
                         '2': ['-O'],
                         '3': ['-O'],
                         's': ['-O'],
                         }


class DmdLikeCompilerMixin:

    LINKER_PREFIX = '-L'

    def get_output_args(self, target):
        return ['-of=' + target]

    def get_linker_output_args(self, target):
        return ['-of=' + target]

    def get_include_args(self, path, is_system):
        return ['-I=' + path]

    def compute_parameters_with_absolute_paths(self, parameter_list, build_dir):
        for idx, i in enumerate(parameter_list):
            if i[:3] == '-I=':
                parameter_list[idx] = i[:3] + os.path.normpath(os.path.join(build_dir, i[3:]))
            if i[:4] == '-L-L':
                parameter_list[idx] = i[:4] + os.path.normpath(os.path.join(build_dir, i[4:]))
            if i[:5] == '-L=-L':
                parameter_list[idx] = i[:5] + os.path.normpath(os.path.join(build_dir, i[5:]))
            if i[:6] == '-Wl,-L':
                parameter_list[idx] = i[:6] + os.path.normpath(os.path.join(build_dir, i[6:]))

        return parameter_list

    def get_warn_args(self, level):
        return ['-wi']

    def get_werror_args(self):
        return ['-w']

    def get_dependency_gen_args(self, outtarget, outfile):
        # DMD and LDC does not currently return Makefile-compatible dependency info.
        return []

    def get_coverage_args(self):
        return ['-cov']

    def get_preprocess_only_args(self):
        return ['-E']

    def get_compile_only_args(self):
        return ['-c']

    def depfile_for_object(self, objfile):
        return objfile + '.' + self.get_depfile_suffix()

    def get_depfile_suffix(self):
        return 'deps'

    def get_pic_args(self):
        if is_windows():
            return []
        return ['-fPIC']

    def get_feature_args(self, kwargs, build_to_src):
        res = []
        if 'unittest' in kwargs:
            unittest = kwargs.pop('unittest')
            unittest_arg = d_feature_args[self.id]['unittest']
            if not unittest_arg:
                raise EnvironmentException('D compiler %s does not support the "unittest" feature.' % self.name_string())
            if unittest:
                res.append(unittest_arg)

        if 'debug' in kwargs:
            debug_level = -1
            debugs = kwargs.pop('debug')
            if not isinstance(debugs, list):
                debugs = [debugs]

            debug_arg = d_feature_args[self.id]['debug']
            if not debug_arg:
                raise EnvironmentException('D compiler %s does not support conditional debug identifiers.' % self.name_string())

            # Parse all debug identifiers and the largest debug level identifier
            for d in debugs:
                if isinstance(d, int):
                    if d > debug_level:
                        debug_level = d
                elif isinstance(d, str) and d.isdigit():
                    if int(d) > debug_level:
                        debug_level = int(d)
                else:
                    res.append('{0}={1}'.format(debug_arg, d))

            if debug_level >= 0:
                res.append('{0}={1}'.format(debug_arg, debug_level))

        if 'versions' in kwargs:
            version_level = -1
            versions = kwargs.pop('versions')
            if not isinstance(versions, list):
                versions = [versions]

            version_arg = d_feature_args[self.id]['version']
            if not version_arg:
                raise EnvironmentException('D compiler %s does not support conditional version identifiers.' % self.name_string())

            # Parse all version identifiers and the largest version level identifier
            for v in versions:
                if isinstance(v, int):
                    if v > version_level:
                        version_level = v
                elif isinstance(v, str) and v.isdigit():
                    if int(v) > version_level:
                        version_level = int(v)
                else:
                    res.append('{0}={1}'.format(version_arg, v))

            if version_level >= 0:
                res.append('{0}={1}'.format(version_arg, version_level))

        if 'import_dirs' in kwargs:
            import_dirs = kwargs.pop('import_dirs')
            if not isinstance(import_dirs, list):
                import_dirs = [import_dirs]

            import_dir_arg = d_feature_args[self.id]['import_dir']
            if not import_dir_arg:
                raise EnvironmentException('D compiler %s does not support the "string import directories" feature.' % self.name_string())
            for idir_obj in import_dirs:
                basedir = idir_obj.get_curdir()
                for idir in idir_obj.get_incdirs():
                    # Avoid superfluous '/.' at the end of paths when d is '.'
                    if idir not in ('', '.'):
                        expdir = os.path.join(basedir, idir)
                    else:
                        expdir = basedir
                    srctreedir = os.path.join(build_to_src, expdir)
                    res.append('{0}{1}'.format(import_dir_arg, srctreedir))

        if kwargs:
            raise EnvironmentException('Unknown D compiler feature(s) selected: %s' % ', '.join(kwargs.keys()))

        return res

    def get_buildtype_linker_args(self, buildtype):
        if buildtype != 'plain':
            return self.get_target_arch_args()
        return []

    def get_std_exe_link_args(self):
        return []

    def gen_import_library_args(self, implibname):
        return ['-Wl,--out-implib=' + implibname]

    def build_rpath_args(self, env, build_dir, from_dir, rpath_paths, build_rpath, install_rpath):
        if is_windows():
            return []

        # This method is to be used by LDC and DMD.
        # GDC can deal with the verbatim flags.
        if not rpath_paths and not install_rpath:
            return []
        paths = ':'.join([os.path.join(build_dir, p) for p in rpath_paths])
        if build_rpath != '':
            paths += ':' + build_rpath
        if len(paths) < len(install_rpath):
            padding = 'X' * (len(install_rpath) - len(paths))
            if not paths:
                paths = padding
            else:
                paths = paths + ':' + padding
        return ['-Wl,-rpath,{}'.format(paths)]

    @classmethod
    def translate_args_to_nongnu(cls, args):
        dcargs = []
        # Translate common arguments to flags the LDC/DMD compilers
        # can understand.
        # The flags might have been added by pkg-config files,
        # and are therefore out of the user's control.
        for arg in args:
            # Translate OS specific arguments first.
            osargs = []
            if is_windows():
                osargs = cls.translate_arg_to_windows(arg)
            elif is_osx():
                osargs = cls.translate_arg_to_osx(arg)
            if osargs:
                dcargs.extend(osargs)
                continue

            # Translate common D arguments here.
            if arg == '-pthread':
                continue
            if arg.startswith('-fstack-protector'):
                continue
            if arg.startswith('-D'):
                continue
            if arg.startswith('-Wl,'):
                # Translate linker arguments here.
                linkargs = arg[arg.index(',') + 1:].split(',')
                for la in linkargs:
                    dcargs.append('-L=' + la.strip())
                continue
            elif arg.startswith(('-link-defaultlib', '-linker', '-link-internally', '-linkonce-templates', '-lib')):
                # these are special arguments to the LDC linker call,
                # arguments like "-link-defaultlib-shared" do *not*
                # denote a library to be linked, but change the default
                # Phobos/DRuntime linking behavior, while "-linker" sets the
                # default linker.
                dcargs.append(arg)
                continue
            elif arg.startswith('-l'):
                # translate library link flag
                dcargs.append('-L=' + arg)
                continue
            elif arg.startswith('-isystem'):
                # translate -isystem system include path
                # this flag might sometimes be added by C library Cflags via
                # pkg-config.
                # NOTE: -isystem and -I are not 100% equivalent, so this is just
                # a workaround for the most common cases.
                if arg.startswith('-isystem='):
                    dcargs.append('-I=' + arg[9:])
                else:
                    dcargs.append('-I' + arg[8:])
                continue
            elif arg.startswith('-idirafter'):
                # same as -isystem, but appends the path instead
                if arg.startswith('-idirafter='):
                    dcargs.append('-I=' + arg[11:])
                else:
                    dcargs.append('-I' + arg[10:])
                continue
            elif arg.startswith('-L/') or arg.startswith('-L./'):
                # we need to handle cases where -L is set by e.g. a pkg-config
                # setting to select a linker search path. We can however not
                # unconditionally prefix '-L' with '-L' because the user might
                # have set this flag too to do what it is intended to for this
                # compiler (pass flag through to the linker)
                # Hence, we guess here whether the flag was intended to pass
                # a linker search path.

                # Make sure static library files are passed properly to the linker.
                if arg.endswith('.a') or arg.endswith('.lib'):
                    if arg.startswith('-L='):
                        farg = arg[3:]
                    else:
                        farg = arg[2:]
                    if len(farg) > 0 and not farg.startswith('-'):
                        dcargs.append('-L=' + farg)
                        continue

                dcargs.append('-L=' + arg)
                continue
            elif not arg.startswith('-') and arg.endswith(('.a', '.lib')):
                # ensure static libraries are passed through to the linker
                dcargs.append('-L=' + arg)
                continue
            else:
                dcargs.append(arg)

        return dcargs

    @classmethod
    def translate_arg_to_windows(cls, arg):
        args = []
        if arg.startswith('-Wl,'):
            # Translate linker arguments here.
            linkargs = arg[arg.index(',') + 1:].split(',')
            for la in linkargs:
                if la.startswith('--out-implib='):
                    # Import library name
                    args.append('-L=/IMPLIB:' + la[13:].strip())
        elif arg.startswith('-mscrtlib='):
            args.append(arg)
            mscrtlib = arg[10:].lower()
            if cls is LLVMDCompiler:
                # Default crt libraries for LDC2 must be excluded for other
                # selected crt options.
                if mscrtlib != 'libcmt':
                    args.append('-L=/NODEFAULTLIB:libcmt')
                    args.append('-L=/NODEFAULTLIB:libvcruntime')

                # Fixes missing definitions for printf-functions in VS2017
                if mscrtlib.startswith('msvcrt'):
                    args.append('-L=/DEFAULTLIB:legacy_stdio_definitions.lib')

        return args

    @classmethod
    def translate_arg_to_osx(cls, arg):
        args = []
        if arg.startswith('-install_name'):
            args.append('-L=' + arg)
        return args

    def get_debug_args(self, is_debug):
        ddebug_args = []
        if is_debug:
            ddebug_args = [d_feature_args[self.id]['debug']]

        return clike_debug_args[is_debug] + ddebug_args

    def get_crt_args(self, crt_val, buildtype):
        if not is_windows():
            return []

        if crt_val in self.mscrt_args:
            return self.mscrt_args[crt_val]
        assert(crt_val == 'from_buildtype')

        # Match what build type flags used to do.
        if buildtype == 'plain':
            return []
        elif buildtype == 'debug':
            return self.mscrt_args['mdd']
        elif buildtype == 'debugoptimized':
            return self.mscrt_args['md']
        elif buildtype == 'release':
            return self.mscrt_args['md']
        elif buildtype == 'minsize':
            return self.mscrt_args['md']
        else:
            assert(buildtype == 'custom')
            raise EnvironmentException('Requested C runtime based on buildtype, but buildtype is "custom".')

    def get_soname_args(self, *args, **kwargs) -> typing.List[str]:
        # LDC and DMD actually do use a linker, but they proxy all of that with
        # their own arguments
        return Compiler.get_soname_args(self, *args, **kwargs)


class DCompiler(Compiler):
    mscrt_args = {
        'none': ['-mscrtlib='],
        'md': ['-mscrtlib=msvcrt'],
        'mdd': ['-mscrtlib=msvcrtd'],
        'mt': ['-mscrtlib=libcmt'],
        'mtd': ['-mscrtlib=libcmtd'],
    }

    def __init__(self, exelist, version, for_machine: MachineChoice, arch, **kwargs):
        self.language = 'd'
        super().__init__(exelist, version, for_machine, **kwargs)
        self.id = 'unknown'
        self.arch = arch

    def sanity_check(self, work_dir, environment):
        source_name = os.path.join(work_dir, 'sanity.d')
        output_name = os.path.join(work_dir, 'dtest')
        with open(source_name, 'w') as ofile:
            ofile.write('''void main() { }''')
        pc = subprocess.Popen(self.exelist + self.get_output_args(output_name) + self.get_target_arch_args() + [source_name], cwd=work_dir)
        pc.wait()
        if pc.returncode != 0:
            raise EnvironmentException('D compiler %s can not compile programs.' % self.name_string())
        if subprocess.call(output_name) != 0:
            raise EnvironmentException('Executables created by D compiler %s are not runnable.' % self.name_string())

    def needs_static_linker(self):
        return True

    def depfile_for_object(self, objfile):
        return objfile + '.' + self.get_depfile_suffix()

    def get_depfile_suffix(self):
        return 'deps'

    def get_pic_args(self):
        if is_windows():
            return []
        return ['-fPIC']

    def get_feature_args(self, kwargs, build_to_src):
        res = []
        if 'unittest' in kwargs:
            unittest = kwargs.pop('unittest')
            unittest_arg = d_feature_args[self.id]['unittest']
            if not unittest_arg:
                raise EnvironmentException('D compiler %s does not support the "unittest" feature.' % self.name_string())
            if unittest:
                res.append(unittest_arg)

        if 'debug' in kwargs:
            debug_level = -1
            debugs = kwargs.pop('debug')
            if not isinstance(debugs, list):
                debugs = [debugs]

            debug_arg = d_feature_args[self.id]['debug']
            if not debug_arg:
                raise EnvironmentException('D compiler %s does not support conditional debug identifiers.' % self.name_string())

            # Parse all debug identifiers and the largest debug level identifier
            for d in debugs:
                if isinstance(d, int):
                    if d > debug_level:
                        debug_level = d
                elif isinstance(d, str) and d.isdigit():
                    if int(d) > debug_level:
                        debug_level = int(d)
                else:
                    res.append('{0}={1}'.format(debug_arg, d))

            if debug_level >= 0:
                res.append('{0}={1}'.format(debug_arg, debug_level))

        if 'versions' in kwargs:
            version_level = -1
            versions = kwargs.pop('versions')
            if not isinstance(versions, list):
                versions = [versions]

            version_arg = d_feature_args[self.id]['version']
            if not version_arg:
                raise EnvironmentException('D compiler %s does not support conditional version identifiers.' % self.name_string())

            # Parse all version identifiers and the largest version level identifier
            for v in versions:
                if isinstance(v, int):
                    if v > version_level:
                        version_level = v
                elif isinstance(v, str) and v.isdigit():
                    if int(v) > version_level:
                        version_level = int(v)
                else:
                    res.append('{0}={1}'.format(version_arg, v))

            if version_level >= 0:
                res.append('{0}={1}'.format(version_arg, version_level))

        if 'import_dirs' in kwargs:
            import_dirs = kwargs.pop('import_dirs')
            if not isinstance(import_dirs, list):
                import_dirs = [import_dirs]

            import_dir_arg = d_feature_args[self.id]['import_dir']
            if not import_dir_arg:
                raise EnvironmentException('D compiler %s does not support the "string import directories" feature.' % self.name_string())
            for idir_obj in import_dirs:
                basedir = idir_obj.get_curdir()
                for idir in idir_obj.get_incdirs():
                    # Avoid superfluous '/.' at the end of paths when d is '.'
                    if idir not in ('', '.'):
                        expdir = os.path.join(basedir, idir)
                    else:
                        expdir = basedir
                    srctreedir = os.path.join(build_to_src, expdir)
                    res.append('{0}{1}'.format(import_dir_arg, srctreedir))

        if kwargs:
            raise EnvironmentException('Unknown D compiler feature(s) selected: %s' % ', '.join(kwargs.keys()))

        return res

    def get_buildtype_linker_args(self, buildtype):
        if buildtype != 'plain':
            return self.get_target_arch_args()
        return []

    def get_std_exe_link_args(self):
        return []

    def _get_compiler_check_args(self, env, extra_args, dependencies, mode='compile'):
        if callable(extra_args):
            extra_args = extra_args(mode)
        if extra_args is None:
            extra_args = []
        elif isinstance(extra_args, str):
            extra_args = [extra_args]
        if dependencies is None:
            dependencies = []
        elif not isinstance(dependencies, list):
            dependencies = [dependencies]
        # Collect compiler arguments
        args = CompilerArgs(self)
        for d in dependencies:
            # Add compile flags needed by dependencies
            args += d.get_compile_args()
            if mode == 'link':
                # Add link flags needed to find dependencies
                args += d.get_link_args()

        if mode == 'compile':
            # Add DFLAGS from the env
            args += env.coredata.get_external_args(self.for_machine, self.language)
        elif mode == 'link':
            # Add LDFLAGS from the env
            args += env.coredata.get_external_link_args(self.for_machine, self.language)
        # extra_args must override all other arguments, so we add them last
        args += extra_args
        return args

    def compiles(self, code, env, *, extra_args=None, dependencies=None, mode='compile'):
        args = self._get_compiler_check_args(env, extra_args, dependencies, mode)

        with self.cached_compile(code, env.coredata, extra_args=args, mode=mode) as p:
            return p.returncode == 0, p.cached

    def has_multi_arguments(self, args, env):
        return self.compiles('int i;\n', env, extra_args=args)

    def get_target_arch_args(self):
        # LDC2 on Windows targets to current OS architecture, but
        # it should follow the target specified by the MSVC toolchain.
        if is_windows():
            if self.arch == 'x86_64':
                return ['-m64']
            return ['-m32']
        return []

    def get_crt_compile_args(self, crt_val, buildtype):
        return []

    def get_crt_link_args(self, crt_val, buildtype):
        return []

    def thread_link_flags(self, env):
        return ['-pthread']

    def name_string(self):
        return ' '.join(self.exelist)


class GnuDCompiler(DCompiler, GnuCompiler):

    # we mostly want DCompiler, but that gives us the Compiler.LINKER_PREFIX instead
    LINKER_PREFIX = GnuCompiler.LINKER_PREFIX

    def __init__(self, exelist, version, for_machine: MachineChoice, arch, **kwargs):
        DCompiler.__init__(self, exelist, version, for_machine, arch, **kwargs)
        self.id = 'gcc'
        default_warn_args = ['-Wall', '-Wdeprecated']
        self.warn_args = {'0': [],
                          '1': default_warn_args,
                          '2': default_warn_args + ['-Wextra'],
                          '3': default_warn_args + ['-Wextra', '-Wpedantic']}
        self.base_options = ['b_colorout', 'b_sanitize', 'b_staticpic', 'b_vscrt']

        self._has_color_support = version_compare(self.version, '>=4.9')
        # dependencies were implemented before, but broken - support was fixed in GCC 7.1+
        # (and some backported versions)
        self._has_deps_support = version_compare(self.version, '>=7.1')

    def get_colorout_args(self, colortype):
        if self._has_color_support:
            super().get_colorout_args(colortype)
        return []

    def get_dependency_gen_args(self, outtarget, outfile):
        if self._has_deps_support:
            return super().get_dependency_gen_args(outtarget, outfile)
        return []

    def get_warn_args(self, level):
        return self.warn_args[level]

    def get_coverage_args(self):
        return []

    def get_buildtype_args(self, buildtype):
        return d_gdc_buildtype_args[buildtype]

    def compute_parameters_with_absolute_paths(self, parameter_list, build_dir):
        for idx, i in enumerate(parameter_list):
            if i[:2] == '-I' or i[:2] == '-L':
                parameter_list[idx] = i[:2] + os.path.normpath(os.path.join(build_dir, i[2:]))

        return parameter_list


class LLVMDCompiler(DmdLikeCompilerMixin, LinkerEnvVarsMixin, BasicLinkerIsCompilerMixin, DCompiler):
    def __init__(self, exelist, version, for_machine: MachineChoice, arch, **kwargs):
        DCompiler.__init__(self, exelist, version, for_machine, arch, **kwargs)
        self.id = 'llvm'
        self.base_options = ['b_coverage', 'b_colorout', 'b_vscrt']

    def get_colorout_args(self, colortype):
        if colortype == 'always':
            return ['-enable-color']
        return []

    def get_warn_args(self, level):
        if level == '2' or level == '3':
            return ['-wi', '-dw']
        elif level == '1':
            return ['-wi']
        else:
            return []

    def get_buildtype_args(self, buildtype):
        if buildtype != 'plain':
            return self.get_target_arch_args() + d_ldc_buildtype_args[buildtype]
        return d_ldc_buildtype_args[buildtype]

    def get_pic_args(self):
        return ['-relocation-model=pic']

    def get_crt_link_args(self, crt_val, buildtype):
        return self.get_crt_args(crt_val, buildtype)

    @classmethod
    def unix_args_to_native(cls, args):
        return cls.translate_args_to_nongnu(args)

    def get_optimization_args(self, optimization_level):
        return ldc_optimization_args[optimization_level]


class DmdDCompiler(DmdLikeCompilerMixin, LinkerEnvVarsMixin, BasicLinkerIsCompilerMixin, DCompiler):
    def __init__(self, exelist, version, for_machine: MachineChoice, arch, **kwargs):
        DCompiler.__init__(self, exelist, version, for_machine, arch, **kwargs)
        self.id = 'dmd'
        self.base_options = ['b_coverage', 'b_colorout', 'b_vscrt']

    def get_colorout_args(self, colortype):
        if colortype == 'always':
            return ['-color=on']
        return []

    def get_buildtype_args(self, buildtype):
        if buildtype != 'plain':
            return self.get_target_arch_args() + d_dmd_buildtype_args[buildtype]
        return d_dmd_buildtype_args[buildtype]

    def get_std_exe_link_args(self):
        if is_windows():
            # DMD links against D runtime only when main symbol is found,
            # so these needs to be inserted when linking static D libraries.
            if self.arch == 'x86_64':
                return ['phobos64.lib']
            elif self.arch == 'x86_mscoff':
                return ['phobos32mscoff.lib']
            return ['phobos.lib']
        return []

    def get_std_shared_lib_link_args(self):
        libname = 'libphobos2.so'
        if is_windows():
            if self.arch == 'x86_64':
                libname = 'phobos64.lib'
            elif self.arch == 'x86_mscoff':
                libname = 'phobos32mscoff.lib'
            else:
                libname = 'phobos.lib'
        return ['-shared', '-defaultlib=' + libname]

    def get_target_arch_args(self):
        # DMD32 and DMD64 on 64-bit Windows defaults to 32-bit (OMF).
        # Force the target to 64-bit in order to stay consistent
        # across the different platforms.
        if is_windows():
            if self.arch == 'x86_64':
                return ['-m64']
            elif self.arch == 'x86_mscoff':
                return ['-m32mscoff']
            return ['-m32']
        return []

    def get_crt_compile_args(self, crt_val, buildtype):
        return self.get_crt_args(crt_val, buildtype)

    @classmethod
    def unix_args_to_native(cls, args):
        return cls.translate_args_to_nongnu(args)

    def get_optimization_args(self, optimization_level):
        return dmd_optimization_args[optimization_level]
