"""Target-specific code generation, parsing, and processing."""

import asyncio
import dataclasses
import hashlib
import json
import os
import pathlib
import re
import sys
import tempfile
import typing
import shlex

import _dwarf
import _llvm
import _optimizers
import _schema
import _stencils
import _writer

if sys.version_info < (3, 11):
    raise RuntimeError("Building the JIT compiler requires Python 3.11 or newer!")

TOOLS_JIT_BUILD = pathlib.Path(__file__).resolve()
TOOLS_JIT = TOOLS_JIT_BUILD.parent
TOOLS = TOOLS_JIT.parent
CPYTHON = TOOLS.parent
EXTERNALS = CPYTHON / "externals"
PYTHON_EXECUTOR_CASES_C_H = CPYTHON / "Python" / "executor_cases.c.h"
TOOLS_JIT_TEMPLATE_C = TOOLS_JIT / "template.c"

ASYNCIO_RUNNER = asyncio.Runner()

_S = typing.TypeVar("_S", _schema.COFFSection, _schema.ELFSection, _schema.MachOSection)
_R = typing.TypeVar(
    "_R", _schema.COFFRelocation, _schema.ELFRelocation, _schema.MachORelocation
)


_ELF_UNWIND_AARCH64 = _dwarf.ELFUnwindConfig(
    frame_pointer="W29",
    return_address="W30",
    register_numbers={
        "W29": 29,
        "W30": 30,
    },
    call_instruction_prefixes=("blr ",),
)

_ELF_UNWIND_X86_64 = _dwarf.ELFUnwindConfig(
    frame_pointer="RBP",
    return_address="RIP",
    register_numbers={
        "RBP": 6,
        "RIP": 16,
    },
    call_instruction_prefixes=("callq ", "call "),
)


@dataclasses.dataclass
class _Target(typing.Generic[_S, _R]):
    triple: str
    condition: str
    _: dataclasses.KW_ONLY
    args: typing.Sequence[str] = ()
    optimizer: type[_optimizers.Optimizer] = _optimizers.Optimizer
    label_prefix: typing.ClassVar[str]
    symbol_prefix: typing.ClassVar[str]
    re_global: typing.ClassVar[re.Pattern[str]]
    stable: bool = False
    debug: bool = False
    verbose: bool = False
    cflags: str = ""
    frame_pointers: bool = False
    unwind: _dwarf.ELFUnwindConfig | None = None
    llvm_version: str = _llvm._LLVM_VERSION
    llvm_tools_install_dir: str | None = None
    known_symbols: dict[str, int] = dataclasses.field(default_factory=dict)
    pyconfig_dir: pathlib.Path = pathlib.Path.cwd().resolve()

    def _compile_args(self) -> list[str]:
        return list(self.args)

    def _shim_compile_args(self) -> list[str]:
        return []

    def _get_nop(self) -> bytes:
        if re.fullmatch(r"aarch64-.*", self.triple):
            nop = b"\x1f\x20\x03\xd5"
        elif re.fullmatch(r"x86_64-.*|i686.*", self.triple):
            nop = b"\x90"
        else:
            raise ValueError(f"NOP not defined for {self.triple}")
        return nop

    def _compute_digest(self) -> str:
        hasher = hashlib.sha256()
        hasher.update(self.triple.encode())
        hasher.update(self.debug.to_bytes())
        hasher.update(self.cflags.encode())
        # These dependencies are also reflected in _JITSources in regen.targets:
        hasher.update(PYTHON_EXECUTOR_CASES_C_H.read_bytes())
        hasher.update((self.pyconfig_dir / "pyconfig.h").read_bytes())
        for dirpath, _, filenames in sorted(os.walk(TOOLS_JIT)):
            # Exclude cache files from digest computation to ensure reproducible builds.
            if dirpath.endswith("__pycache__"):
                continue
            for filename in sorted(filenames):
                hasher.update(pathlib.Path(dirpath, filename).read_bytes())
        return hasher.hexdigest()

    def _write_generated_header(
        self,
        output: pathlib.Path,
        *,
        digest: str,
        comment: str,
        lines: typing.Iterable[str],
    ) -> None:
        output_new = output.with_name(f"{output.name}.new")
        try:
            with output_new.open("w") as file:
                file.write(digest)
                if comment:
                    file.write(f"// {comment}\n")
                file.write("\n")
                for line in lines:
                    file.write(f"{line}\n")
            try:
                output_new.replace(output)
            except FileNotFoundError:
                # another process probably already moved the file
                if not output.is_file():
                    raise
        finally:
            output_new.unlink(missing_ok=True)

    async def _parse(self, path: pathlib.Path) -> _stencils.StencilGroup:
        group = _stencils.StencilGroup()
        args = ["--disassemble", "--reloc", f"{path}"]
        output = await _llvm.maybe_run(
            "llvm-objdump",
            args,
            echo=self.verbose,
            llvm_version=self.llvm_version,
            llvm_tools_install_dir=self.llvm_tools_install_dir,
        )
        if output is not None:
            # Make sure that full paths don't leak out (for reproducibility):
            long, short = str(path), str(path.name)
            group.code.disassembly.extend(
                line.expandtabs().strip().replace(long, short)
                for line in output.splitlines()
            )
        args = [
            "--elf-output-style=JSON",
            "--expand-relocs",
            # "--pretty-print",
            "--section-data",
            "--section-relocations",
            "--section-symbols",
            "--sections",
            f"{path}",
        ]
        output = await _llvm.run(
            "llvm-readobj",
            args,
            echo=self.verbose,
            llvm_version=self.llvm_version,
            llvm_tools_install_dir=self.llvm_tools_install_dir,
        )
        # --elf-output-style=JSON is only *slightly* broken on Mach-O...
        output = output.replace("PrivateExtern\n", "\n")
        output = output.replace("Extern\n", "\n")
        # ...and also COFF:
        output = output[output.index("[", 1, None) :]
        output = output[: output.rindex("]", None, -1) + 1]
        sections: list[dict[typing.Literal["Section"], _S]] = json.loads(output)
        for wrapped_section in sections:
            self._handle_section(wrapped_section["Section"], group)
        assert group.symbols["_JIT_ENTRY"] == (_stencils.HoleValue.CODE, 0)
        if group.data.body:
            line = f"0: {str(bytes(group.data.body)).removeprefix('b')}"
            group.data.disassembly.append(line)
        return group

    def _handle_section(self, section: _S, group: _stencils.StencilGroup) -> None:
        raise NotImplementedError(type(self))

    def _handle_relocation(
        self, base: int, relocation: _R, raw: bytearray
    ) -> _stencils.Hole:
        raise NotImplementedError(type(self))

    def _base_clang_args(self, opname: str, tempdir: pathlib.Path) -> list[str]:
        return [
            f"--target={self.triple}",
            "-DPy_BUILD_CORE_MODULE",
            "-D_DEBUG" if self.debug else "-DNDEBUG",
            f"-DSUPPORTS_SMALL_CONSTS={1 if self.optimizer.supports_small_constants else 0}",
            f"-D_JIT_OPCODE={opname}",
            "-D_PyJIT_ACTIVE",
            "-D_Py_JIT",
            f"-I{self.pyconfig_dir}",
            f"-I{CPYTHON / 'Include'}",
            f"-I{CPYTHON / 'Include' / 'internal'}",
            f"-I{CPYTHON / 'Include' / 'internal' / 'mimalloc'}",
            f"-I{CPYTHON / 'Python'}",
            f"-I{CPYTHON / 'Tools' / 'jit'}",
            # -O2 and -O3 include some optimizations that make sense for
            # standalone functions, but not for snippets of code that are going
            # to be laid out end-to-end (like ours)... common examples include
            # passes like tail-duplication, or aligning jump targets with nops.
            # -Os is equivalent to -O2 with many of these problematic passes
            # disabled. Based on manual review, for *our* purposes it usually
            # generates better code than -O2 (and -O2 usually generates better
            # code than -O3). As a nice benefit, it uses less memory too:
            "-Os",
            # Shorten full absolute file paths in the generated code (like the
            # __FILE__ macro and assert failure messages) for reproducibility:
            f"-ffile-prefix-map={CPYTHON}=.",
            f"-ffile-prefix-map={tempdir}=.",
            # Don't call built-in functions that we can't find or patch:
            "-fno-builtin",
            # Don't call stack-smashing canaries that we can't find or patch:
            "-fno-stack-protector",
            "-std=c11",
        ]

    async def _build_stencil_group(
        self, opname: str, c: pathlib.Path, tempdir: pathlib.Path
    ) -> _stencils.StencilGroup:
        s = tempdir / f"{opname}.s"
        o = tempdir / f"{opname}.o"
        args_s = self._base_clang_args(opname, tempdir)
        args_s += [
            "-S",
            # Stencils do not need unwind info, and the optimizer does not
            # preserve .cfi_* directives correctly. On Darwin,
            # -fno-asynchronous-unwind-tables alone still leaves synchronous
            # unwind directives in the assembly, so disable both forms here.
            "-fno-unwind-tables",
            "-fno-asynchronous-unwind-tables",
            "-o",
            f"{s}",
            f"{c}",
        ]
        if self.frame_pointers:
            args_s += ["-Xclang", "-mframe-pointer=reserved"]
        args_s += self._compile_args()
        # Allow user-provided CFLAGS to override any defaults
        args_s += shlex.split(self.cflags)
        await _llvm.run(
            "clang",
            args_s,
            echo=self.verbose,
            llvm_version=self.llvm_version,
            llvm_tools_install_dir=self.llvm_tools_install_dir,
        )
        self.optimizer(
            s,
            label_prefix=self.label_prefix,
            symbol_prefix=self.symbol_prefix,
            re_global=self.re_global,
            frame_pointers=self.frame_pointers,
        ).run()
        args_o = [f"--target={self.triple}", "-c", "-o", f"{o}", f"{s}"]
        await _llvm.run(
            "clang",
            args_o,
            echo=self.verbose,
            llvm_version=self.llvm_version,
            llvm_tools_install_dir=self.llvm_tools_install_dir,
        )
        return await self._parse(o)

    async def _build_shim_object(self, output: pathlib.Path) -> None:
        with tempfile.TemporaryDirectory() as tempdir:
            work = pathlib.Path(tempdir).resolve()
            args_o = self._base_clang_args("shim", work)
            args_o += self._shim_compile_args()
            args_o += [
                "-c",
                # The shim is a real function in the final binary, so
                # keep unwind info for debuggers and stack walkers.
                "-fasynchronous-unwind-tables",
            ]
            if self.frame_pointers:
                args_o += ["-Xclang", "-mframe-pointer=all"]
            args_o += self._compile_args()
            args_o += shlex.split(self.cflags)
            args_o += ["-o", f"{output}", f"{TOOLS_JIT / 'shim.c'}"]
            await _llvm.run(
                "clang",
                args_o,
                echo=self.verbose,
                llvm_version=self.llvm_version,
                llvm_tools_install_dir=self.llvm_tools_install_dir,
            )

    async def _get_shim_unwind_info(
        self, output: pathlib.Path
    ) -> _dwarf.UnwindInfo | None:
        return None

    async def _build_stencils(self) -> dict[str, _stencils.StencilGroup]:
        generated_cases = PYTHON_EXECUTOR_CASES_C_H.read_text()
        cases_and_opnames = sorted(
            re.findall(
                r"\n {8}(case (\w+): \{\n.*?\n {8}\})", generated_cases, flags=re.DOTALL
            )
        )
        tasks = []
        # If you need to see the generated assembly files,
        # uncomment line below (and comment out line below that)
        #   with tempfile.TemporaryDirectory("-stencils-assembly", delete=False) as tempdir:
        with tempfile.TemporaryDirectory() as tempdir:
            work = pathlib.Path(tempdir).resolve()
            async with asyncio.TaskGroup() as group:
                template = TOOLS_JIT_TEMPLATE_C.read_text()
                for case, opname in cases_and_opnames:
                    # Write out a copy of the template with *only* this case
                    # inserted. This is about twice as fast as #include'ing all
                    # of executor_cases.c.h each time we compile (since the C
                    # compiler wastes a bunch of time parsing the dead code for
                    # all of the other cases):
                    c = work / f"{opname}.c"
                    c.write_text(template.replace("CASE", case))
                    coro = self._build_stencil_group(opname, c, work)
                    tasks.append(group.create_task(coro, name=opname))
        stencil_groups = {task.get_name(): task.result() for task in tasks}
        for stencil_group in stencil_groups.values():
            stencil_group.convert_labels_to_relocations()
            stencil_group.process_relocations(self.known_symbols)
        return stencil_groups

    def build(
        self,
        *,
        comment: str = "",
        force: bool = False,
        jit_stencils: pathlib.Path,
        jit_shim_object: pathlib.Path,
        jit_unwind_info: pathlib.Path,
    ) -> None:
        """Build jit_stencils.h and the shim object in the given directory."""
        jit_stencils.parent.mkdir(parents=True, exist_ok=True)
        if not self.stable:
            warning = f"JIT support for {self.triple} is still experimental!"
            request = "Please report any issues you encounter.".center(len(warning))
            if self.llvm_version != _llvm._LLVM_VERSION:
                request = f"Warning! Building with an LLVM version other than {_llvm._LLVM_VERSION} is not supported."
            outline = "=" * len(warning)
            print("\n".join(["", outline, warning, request, outline, ""]))
        digest = f"// {self._compute_digest()}\n"
        # The generated headers include the input digest as their first line.
        # If every generated artifact is current, skip the expensive rebuild.
        if (
            not force
            and jit_stencils.exists()
            and jit_stencils.read_text().startswith(digest)
            and jit_shim_object.exists()
            and jit_unwind_info.exists()
            and jit_unwind_info.read_text().startswith(digest)
        ):
            return
        # Build the shim first so its compiled DWARF CFI can be used to derive
        # the unwind rules emitted into jit_unwind_info-<triple>.h.
        ASYNCIO_RUNNER.run(self._build_shim_object(jit_shim_object))
        unwind_info = ASYNCIO_RUNNER.run(self._get_shim_unwind_info(jit_shim_object))
        self._write_generated_header(
            jit_unwind_info,
            digest=digest,
            comment=comment,
            lines=_writer.dump_unwind_info(unwind_info),
        )
        # Build the uop stencils after the shim metadata has been emitted.
        stencil_groups = ASYNCIO_RUNNER.run(self._build_stencils())
        self._write_generated_header(
            jit_stencils,
            digest=digest,
            comment=comment,
            lines=_writer.dump(stencil_groups, self.known_symbols),
        )


class _COFF(
    _Target[_schema.COFFSection, _schema.COFFRelocation]
):  # pylint: disable = too-few-public-methods
    def _shim_compile_args(self) -> list[str]:
        # The shim is part of pythoncore, not a shared extension.
        # On Windows, Py_BUILD_CORE_MODULE makes public APIs import from
        # pythonXY.lib, which creates a self-dependency when linking
        # pythoncore.dll. Build the shim with builtin/core semantics.
        return ["-UPy_BUILD_CORE_MODULE", "-DPy_BUILD_CORE_BUILTIN"]

    def _handle_section(
        self, section: _schema.COFFSection, group: _stencils.StencilGroup
    ) -> None:
        name = section["Name"]["Value"]
        if name == ".debug$S":
            # skip debug sections
            return
        flags = {flag["Name"] for flag in section["Characteristics"]["Flags"]}
        if "SectionData" in section:
            section_data_bytes = section["SectionData"]["Bytes"]
        else:
            # Zeroed BSS data, seen with printf debugging calls:
            section_data_bytes = [0] * section["RawDataSize"]
        if "IMAGE_SCN_MEM_EXECUTE" in flags:
            value = _stencils.HoleValue.CODE
            stencil = group.code
        elif "IMAGE_SCN_MEM_READ" in flags:
            value = _stencils.HoleValue.DATA
            stencil = group.data
        else:
            return
        base = len(stencil.body)
        group.symbols[section["Number"]] = value, base
        stencil.body.extend(section_data_bytes)
        for wrapped_symbol in section["Symbols"]:
            symbol = wrapped_symbol["Symbol"]
            offset = base + symbol["Value"]
            name = symbol["Name"]
            name = name.removeprefix(self.symbol_prefix)
            if name not in group.symbols:
                group.symbols[name] = value, offset
        for wrapped_relocation in section["Relocations"]:
            relocation = wrapped_relocation["Relocation"]
            hole = self._handle_relocation(base, relocation, stencil.body)
            stencil.holes.append(hole)

    def _unwrap_dllimport(self, name: str) -> tuple[_stencils.HoleValue, str | None]:
        if name.startswith("__imp_"):
            name = name.removeprefix("__imp_")
            name = name.removeprefix(self.symbol_prefix)
            return _stencils.HoleValue.GOT, name
        name = name.removeprefix(self.symbol_prefix)
        return _stencils.symbol_to_value(name)

    def _handle_relocation(
        self, base: int, relocation: _schema.COFFRelocation, raw: bytearray
    ) -> _stencils.Hole:
        match relocation:
            case {
                "Offset": offset,
                "Symbol": s,
                "Type": {"Name": "IMAGE_REL_I386_DIR32" as kind},
            }:
                offset += base
                value, symbol = self._unwrap_dllimport(s)
                addend = int.from_bytes(raw[offset : offset + 4], "little")
            case {
                "Offset": offset,
                "Symbol": s,
                "Type": {
                    "Name": "IMAGE_REL_AMD64_REL32" | "IMAGE_REL_I386_REL32" as kind
                },
            }:
                offset += base
                value, symbol = self._unwrap_dllimport(s)
                addend = (
                    int.from_bytes(raw[offset : offset + 4], "little", signed=True) - 4
                )
            case {
                "Offset": offset,
                "Symbol": s,
                "Type": {
                    "Name": "IMAGE_REL_ARM64_BRANCH19"
                    | "IMAGE_REL_ARM64_BRANCH26"
                    | "IMAGE_REL_ARM64_PAGEBASE_REL21"
                    | "IMAGE_REL_ARM64_PAGEOFFSET_12A"
                    | "IMAGE_REL_ARM64_PAGEOFFSET_12L" as kind
                },
            }:
                offset += base
                value, symbol = self._unwrap_dllimport(s)
                addend = 0
            case _:
                raise NotImplementedError(relocation)
        return _stencils.Hole(offset, kind, value, symbol, addend)


class _COFF32(_COFF):
    # These mangle like Mach-O and other "older" formats:
    label_prefix = "L"
    symbol_prefix = "_"
    re_global = re.compile(r'\s*\.def\s+(?P<label>[\w."$?@]+);')


class _COFF64(_COFF):
    # These mangle like ELF and other "newer" formats:
    label_prefix = ".L"
    symbol_prefix = ""
    re_global = re.compile(r'\s*\.def\s+(?P<label>[\w."$?@]+);')

    def _compile_args(self) -> list[str]:
        runtime = "-fms-runtime-lib=dll_dbg" if self.debug else "-fms-runtime-lib=dll"
        return [runtime, *self.args]


class _ELF(
    _Target[_schema.ELFSection, _schema.ELFRelocation]
):  # pylint: disable = too-few-public-methods
    label_prefix = ".L"
    symbol_prefix = ""
    re_global = re.compile(r'\s*\.globl\s+(?P<label>[\w."$?@]+)(\s+.*)?')

    async def _get_shim_unwind_info(
        self, output: pathlib.Path
    ) -> _dwarf.UnwindInfo | None:
        assert self.unwind is not None
        return await _dwarf.ELFUnwindInfo(
            self.triple,
            config=self.unwind,
            verbose=self.verbose,
            llvm_version=self.llvm_version,
            llvm_tools_install_dir=self.llvm_tools_install_dir,
            llvm_run=_llvm.run,
        ).extract(output)

    def _handle_section(
        self, section: _schema.ELFSection, group: _stencils.StencilGroup
    ) -> None:
        section_type = section["Type"]["Name"]
        flags = {flag["Name"] for flag in section["Flags"]["Flags"]}
        if section_type == "SHT_RELA":
            assert "SHF_INFO_LINK" in flags, flags
            assert not section["Symbols"]
            maybe_symbol = group.symbols.get(section["Info"])
            if maybe_symbol is None:
                # These are relocations for a section we're not emitting. Skip:
                return
            value, base = maybe_symbol
            if value is _stencils.HoleValue.CODE:
                stencil = group.code
            else:
                assert value is _stencils.HoleValue.DATA
                stencil = group.data
            for wrapped_relocation in section["Relocations"]:
                relocation = wrapped_relocation["Relocation"]
                hole = self._handle_relocation(base, relocation, stencil.body)
                stencil.holes.append(hole)
        elif section_type == "SHT_PROGBITS":
            if "SHF_ALLOC" not in flags:
                return
            if "SHF_EXECINSTR" in flags:
                value = _stencils.HoleValue.CODE
                stencil = group.code
            else:
                value = _stencils.HoleValue.DATA
                stencil = group.data
            group.symbols[section["Index"]] = value, len(stencil.body)
            for wrapped_symbol in section["Symbols"]:
                symbol = wrapped_symbol["Symbol"]
                offset = len(stencil.body) + symbol["Value"]
                name = symbol["Name"]["Name"]
                name = name.removeprefix(self.symbol_prefix)
                group.symbols[name] = value, offset
            stencil.body.extend(section["SectionData"]["Bytes"])
            assert not section["Relocations"]
        else:
            assert section_type in {
                "SHT_GROUP",
                "SHT_LLVM_ADDRSIG",
                "SHT_NOTE",
                "SHT_NULL",
                "SHT_STRTAB",
                "SHT_SYMTAB",
            }, section_type

    def _handle_relocation(
        self, base: int, relocation: _schema.ELFRelocation, raw: bytearray
    ) -> _stencils.Hole:
        symbol: str | None
        match relocation:
            case {
                "Addend": addend,
                "Offset": offset,
                "Symbol": {"Name": s},
                "Type": {
                    "Name": "R_AARCH64_ADR_GOT_PAGE"
                    | "R_AARCH64_LD64_GOT_LO12_NC"
                    | "R_X86_64_GOTPCREL"
                    | "R_X86_64_GOTPCRELX"
                    | "R_X86_64_REX_GOTPCRELX" as kind
                },
            }:
                offset += base
                s = s.removeprefix(self.symbol_prefix)
                value, symbol = _stencils.HoleValue.GOT, s
            case {
                "Addend": addend,
                "Offset": offset,
                "Symbol": {"Name": s},
                "Type": {"Name": kind},
            }:
                offset += base
                s = s.removeprefix(self.symbol_prefix)
                value, symbol = _stencils.symbol_to_value(s)
            case _:
                raise NotImplementedError(relocation)
        return _stencils.Hole(offset, kind, value, symbol, addend)


class _MachO(
    _Target[_schema.MachOSection, _schema.MachORelocation]
):  # pylint: disable = too-few-public-methods
    label_prefix = "L"
    symbol_prefix = "_"
    re_global = re.compile(r'\s*\.globl\s+(?P<label>[\w."$?@]+)(\s+.*)?')

    def _handle_section(
        self, section: _schema.MachOSection, group: _stencils.StencilGroup
    ) -> None:
        assert section["Address"] >= len(group.code.body)
        assert "SectionData" in section
        flags = {flag["Name"] for flag in section["Attributes"]["Flags"]}
        name = section["Name"]["Value"]
        name = name.removeprefix(self.symbol_prefix)
        if "Debug" in flags:
            return
        if "PureInstructions" in flags:
            value = _stencils.HoleValue.CODE
            stencil = group.code
            start_address = 0
            group.symbols[name] = value, section["Address"] - start_address
        else:
            value = _stencils.HoleValue.DATA
            stencil = group.data
            start_address = len(group.code.body)
            group.symbols[name] = value, len(group.code.body)
        base = section["Address"] - start_address
        group.symbols[section["Index"]] = value, base
        stencil.body.extend(
            [0] * (section["Address"] - len(group.code.body) - len(group.data.body))
        )
        stencil.body.extend(section["SectionData"]["Bytes"])
        assert "Symbols" in section
        for wrapped_symbol in section["Symbols"]:
            symbol = wrapped_symbol["Symbol"]
            offset = symbol["Value"] - start_address
            name = symbol["Name"]["Name"]
            name = name.removeprefix(self.symbol_prefix)
            group.symbols[name] = value, offset
        assert "Relocations" in section
        for wrapped_relocation in section["Relocations"]:
            relocation = wrapped_relocation["Relocation"]
            hole = self._handle_relocation(base, relocation, stencil.body)
            stencil.holes.append(hole)

    def _handle_relocation(
        self, base: int, relocation: _schema.MachORelocation, raw: bytearray
    ) -> _stencils.Hole:
        symbol: str | None
        match relocation:
            case {
                "Offset": offset,
                "Symbol": {"Name": s},
                "Type": {
                    "Name": "ARM64_RELOC_GOT_LOAD_PAGE21"
                    | "ARM64_RELOC_GOT_LOAD_PAGEOFF12" as kind
                },
            }:
                offset += base
                s = s.removeprefix(self.symbol_prefix)
                value, symbol = _stencils.HoleValue.GOT, s
                addend = 0
            case {
                "Offset": offset,
                "Symbol": {"Name": s},
                "Type": {"Name": "X86_64_RELOC_GOT" | "X86_64_RELOC_GOT_LOAD" as kind},
            }:
                offset += base
                s = s.removeprefix(self.symbol_prefix)
                value, symbol = _stencils.HoleValue.GOT, s
                addend = (
                    int.from_bytes(raw[offset : offset + 4], "little", signed=True) - 4
                )
            case {
                "Offset": offset,
                "Section": {"Name": s},
                "Type": {"Name": "X86_64_RELOC_SIGNED" as kind},
            } | {
                "Offset": offset,
                "Symbol": {"Name": s},
                "Type": {"Name": "X86_64_RELOC_BRANCH" | "X86_64_RELOC_SIGNED" as kind},
            }:
                offset += base
                s = s.removeprefix(self.symbol_prefix)
                value, symbol = _stencils.symbol_to_value(s)
                addend = (
                    int.from_bytes(raw[offset : offset + 4], "little", signed=True) - 4
                )
            case {
                "Offset": offset,
                "Section": {"Name": s},
                "Type": {"Name": kind},
            } | {
                "Offset": offset,
                "Symbol": {"Name": s},
                "Type": {"Name": kind},
            }:
                offset += base
                s = s.removeprefix(self.symbol_prefix)
                value, symbol = _stencils.symbol_to_value(s)
                addend = 0
            case _:
                raise NotImplementedError(relocation)
        return _stencils.Hole(offset, kind, value, symbol, addend)


def get_target(host: str) -> _COFF32 | _COFF64 | _ELF | _MachO:
    """Build a _Target for the given host "triple" and options."""
    optimizer: type[_optimizers.Optimizer]
    target: _COFF32 | _COFF64 | _ELF | _MachO
    if re.fullmatch(r"aarch64-apple-darwin.*", host):
        host = "aarch64-apple-darwin"
        condition = "defined(__aarch64__) && defined(__APPLE__)"
        optimizer = _optimizers.OptimizerAArch64
        target = _MachO(host, condition, optimizer=optimizer)
    elif re.fullmatch(r"aarch64-pc-windows-msvc", host):
        host = "aarch64-pc-windows-msvc"
        condition = "defined(_M_ARM64)"
        optimizer = _optimizers.OptimizerAArch64
        target = _COFF64(host, condition, optimizer=optimizer)
    elif re.fullmatch(r"aarch64-.*-linux-gnu", host):
        host = "aarch64-unknown-linux-gnu"
        condition = "defined(__aarch64__) && defined(__linux__)"
        # -mno-outline-atomics: Keep intrinsics from being emitted.
        args = ["-fpic", "-mno-outline-atomics"]
        optimizer = _optimizers.OptimizerAArch64
        target = _ELF(
            host,
            condition,
            args=args,
            optimizer=optimizer,
            frame_pointers=True,
            unwind=_ELF_UNWIND_AARCH64,
        )
    elif re.fullmatch(r"i686-pc-windows-msvc", host):
        host = "i686-pc-windows-msvc"
        condition = "defined(_M_IX86)"
        # -Wno-ignored-attributes: __attribute__((preserve_none)) is not supported here.
        # -mno-sse: Use x87 FPU instead of SSE for float math. The COFF32
        # stencil converter cannot handle _xmm register references.
        args = ["-DPy_NO_ENABLE_SHARED", "-Wno-ignored-attributes", "-mno-sse"]
        optimizer = _optimizers.OptimizerX86
        target = _COFF32(host, condition, args=args, optimizer=optimizer)
    elif re.fullmatch(r"x86_64-apple-darwin.*", host):
        host = "x86_64-apple-darwin"
        condition = "defined(__x86_64__) && defined(__APPLE__)"
        optimizer = _optimizers.OptimizerX86
        target = _MachO(host, condition, optimizer=optimizer)
    elif re.fullmatch(r"x86_64-pc-windows-msvc", host):
        host = "x86_64-pc-windows-msvc"
        condition = "defined(_M_X64)"
        optimizer = _optimizers.OptimizerX86
        target = _COFF64(host, condition, optimizer=optimizer)
    elif re.fullmatch(r"x86_64-.*-linux-gnu", host):
        host = "x86_64-unknown-linux-gnu"
        condition = "defined(__x86_64__) && defined(__linux__)"
        args = ["-fno-pic", "-mcmodel=medium", "-mlarge-data-threshold=0", "-fno-plt"]
        optimizer = _optimizers.OptimizerX86
        target = _ELF(
            host,
            condition,
            args=args,
            optimizer=optimizer,
            frame_pointers=True,
            unwind=_ELF_UNWIND_X86_64,
        )
    else:
        raise ValueError(host)
    return target
