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

from __future__ import annotations

import enum
import os
import re
import typing as T


from . import ExtensionModule, ModuleInfo
from . import ModuleReturnValue
from .. import mesonlib, build
from .. import mlog
from ..interpreter.type_checking import DEPEND_FILES_KW, DEPENDS_KW, INCLUDE_DIRECTORIES
from ..interpreterbase.decorators import ContainerTypeInfo, FeatureNew, KwargInfo, typed_kwargs, typed_pos_args
from ..mesonlib import MachineChoice, MesonException
from ..programs import ExternalProgram

if T.TYPE_CHECKING:
    from . import ModuleState
    from ..compilers.compilers import Language, Compiler
    from ..interpreter import Interpreter
    from ..programs import CommandList

    from typing_extensions import Literal, TypedDict

    class CompileResources(TypedDict):

        depend_files: T.List[mesonlib.FileOrString]
        depends: T.List[T.Union[build.BuildTarget, build.CustomTarget]]
        include_directories: T.List[T.Union[str, build.IncludeDirs]]
        implicit_include_directories: bool
        args: T.List[str]


class ResourceCompilerType(enum.Enum):
    windres = 1
    rc = 2
    wrc = 3

class WindowsModule(ExtensionModule):

    INFO = ModuleInfo('windows')

    def __init__(self, interpreter: 'Interpreter'):
        super().__init__(interpreter)
        self._rescomp: T.Optional[T.Tuple[ExternalProgram, ResourceCompilerType]] = None
        self.methods.update({
            'compile_resources': self.compile_resources,
        })

    def detect_compiler(self, compilers: T.Dict[Language, 'Compiler']) -> 'Compiler':
        # https://github.com/python/mypy/issues/18826
        # However, we need to support versions of mypy that cannot deduce the
        # tuple either.
        for l in T.cast('T.Tuple[Language, ...]', ('c', 'cpp')):
            if l in compilers:
                return compilers[l]
        raise MesonException('Resource compilation requires a C or C++ compiler.')

    def _find_resource_compiler(self, state: 'ModuleState') -> T.Tuple[ExternalProgram, ResourceCompilerType]:
        # FIXME: Does not handle `native: true` executables, see
        # See https://github.com/mesonbuild/meson/issues/1531
        # Take a parameter instead of the hardcoded definition below
        for_machine = MachineChoice.HOST

        if self._rescomp:
            return self._rescomp

        # Will try cross / native file and then env var
        rescomp = ExternalProgram.from_bin_list(state.environment, for_machine, 'windres')

        if not rescomp or not rescomp.found():
            def search_programs(names: T.List[str]) -> T.Optional[ExternalProgram]:
                for name in names:
                    program = ExternalProgram(name, silent=True)
                    if program.found():
                        return program
                return None

            comp = self.detect_compiler(state.environment.coredata.compilers[for_machine])
            if comp.id in {'msvc', 'clang-cl', 'intel-cl'} or (comp.linker and comp.linker.id in {'link', 'lld-link'}):
                rescomp = search_programs(['rc', 'llvm-rc'])
            else:
                rescomp = search_programs(['windres', 'llvm-windres'])

        if not rescomp:
            raise MesonException('Could not find Windows resource compiler')

        for (arg, match, rc_type) in [
                ('/?', '^.*Microsoft.*Resource Compiler.*$', ResourceCompilerType.rc),
                ('/?', 'LLVM Resource Converter.*$', ResourceCompilerType.rc),
                ('--version', '^.*GNU windres.*$', ResourceCompilerType.windres),
                ('--version', '^.*Wine Resource Compiler.*$', ResourceCompilerType.wrc),
        ]:
            p, o, e = mesonlib.Popen_safe(rescomp.get_command() + [arg])
            m = re.search(match, o, re.MULTILINE)
            if m:
                mlog.log('Windows resource compiler: %s' % m.group())
                self._rescomp = (rescomp, rc_type)
                break
        else:
            raise MesonException('Could not determine type of Windows resource compiler')

        return self._rescomp

    @typed_pos_args('windows.compile_resources', varargs=(str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex), min_varargs=1)
    @typed_kwargs(
        'windows.compile_resources',
        DEPEND_FILES_KW.evolve(since='0.47.0'),
        DEPENDS_KW.evolve(since='0.47.0'),
        INCLUDE_DIRECTORIES,
        KwargInfo('implicit_include_directories', bool, default=False, since='1.11.0'),
        KwargInfo('args', ContainerTypeInfo(list, str), default=[], listify=True),
    )
    def compile_resources(self, state: 'ModuleState',
                          args: T.Tuple[T.List[T.Union[str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex]]],
                          kwargs: 'CompileResources') -> ModuleReturnValue:
        extra_args = kwargs['args'].copy()
        wrc_depend_files = kwargs['depend_files']
        wrc_depends = kwargs['depends']
        for d in wrc_depends:
            if isinstance(d, build.CustomTarget):
                extra_args += state.get_include_args([
                    build.IncludeDirs('', [], False, [self.interpreter.backend.get_target_dir(d)])
                ])
        extra_args += state.get_include_args(kwargs['include_directories'], kwargs['implicit_include_directories'])

        rescomp, rescomp_type = self._find_resource_compiler(state)
        if rescomp_type == ResourceCompilerType.rc:
            # RC is used to generate .res files, a special binary resource
            # format, which can be passed directly to LINK (apparently LINK uses
            # CVTRES internally to convert this to a COFF object)
            suffix = 'res'
            res_args = extra_args + ['/nologo', '/fo@OUTPUT@', '@INPUT@']
        elif rescomp_type == ResourceCompilerType.windres:
            # ld only supports object files, so windres is used to generate a
            # COFF object
            suffix = 'o'
            res_args = extra_args + ['@INPUT@', '@OUTPUT@']

            m = 'Argument {!r} has a space which may not work with windres due to ' \
                'a MinGW bug: https://sourceware.org/bugzilla/show_bug.cgi?id=4933'
            for arg in extra_args:
                if ' ' in arg:
                    mlog.warning(m.format(arg), fatal=False)
        else:
            suffix = 'o'
            res_args = extra_args + ['@INPUT@', '-o', '@OUTPUT@']

        res_targets: T.List[build.CustomTarget] = []

        def get_names() -> T.Iterable[T.Tuple[str, str, T.Union[str, mesonlib.File, build.CustomTargetIndex]]]:
            for src in args[0]:
                if isinstance(src, str):
                    yield os.path.join(state.subdir, src), src, src
                elif isinstance(src, mesonlib.File):
                    yield src.relative_name(), src.fname, src
                elif isinstance(src, build.CustomTargetIndex):
                    FeatureNew.single_use('windows.compile_resource CustomTargetIndex in positional arguments', '0.61.0',
                                          state.subproject, location=state.current_node)
                    # This dance avoids a case where two indexes of the same
                    # target are given as separate arguments.
                    yield (f'{src.get_id()}_{src.target.get_outputs().index(src.output)}',
                           f'windows_compile_resources_{src.get_filename()}', src)
                else:
                    if len(src.get_outputs()) > 1:
                        FeatureNew.single_use('windows.compile_resource CustomTarget with multiple outputs in positional arguments',
                                              '0.61.0', state.subproject, location=state.current_node)
                    for i, out in enumerate(src.get_outputs()):
                        # Chances are that src.get_filename() is already the name of that
                        # target, add a prefix to avoid name clash.
                        yield f'{src.get_id()}_{i}', f'windows_compile_resources_{i}_{out}', src[i]

        for name, name_formatted, src in get_names():
            # Path separators are not allowed in target names
            name = name.replace('/', '_').replace('\\', '_').replace(':', '_')
            name_formatted = name_formatted.replace('/', '_').replace('\\', '_').replace(':', '_')
            output = f'{name}_@BASENAME@.{suffix}'
            depfile: T.Optional[str] = None
            depfile_type: T.Optional[Literal['gcc', 'msvc']] = None
            command: CommandList = []

            if rescomp_type == ResourceCompilerType.rc:
                compiler = self.detect_compiler(state.environment.coredata.compilers[MachineChoice.HOST])
                depfile_type = 'msvc'

                command.extend(state.environment.get_build_command())
                command.extend(['--internal', 'rc',
                                '--cl', compiler.get_exelist(False)[0],
                                '--rc'])

            command.append(rescomp)
            command.extend(res_args)

            # instruct binutils windres to generate a preprocessor depfile
            if rescomp_type == ResourceCompilerType.windres:
                depfile = f'{output}.d'
                depfile_type = 'gcc'
                command.extend(['--preprocessor-arg=-MD',
                                '--preprocessor-arg=-MQ@OUTPUT@',
                                '--preprocessor-arg=-MF@DEPFILE@'])

            res_targets.append(build.CustomTarget(
                name_formatted,
                state.subdir,
                state.subproject,
                state.environment,
                command,
                [src],
                [output],
                depfile=depfile,
                depfile_type=depfile_type,
                depend_files=wrc_depend_files,
                extra_depends=wrc_depends,
                description='Compiling Windows resource {}',
            ))

        return ModuleReturnValue(res_targets, [res_targets])

def initialize(interp: 'Interpreter') -> WindowsModule:
    return WindowsModule(interp)
