#!/usr/bin/env python3
"""Compile an ARM assembly file and run it with qemu-arm-static."""

from __future__ import annotations

import argparse
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path


def parse_args() -> argparse.Namespace:
    if "--" in sys.argv:
        separator_index = sys.argv.index("--")
        tool_args = sys.argv[1:separator_index]
        program_args = sys.argv[separator_index + 1 :]
    else:
        tool_args = sys.argv[1:]
        program_args = []

    parser = argparse.ArgumentParser(
        description=(
            "Compile an ARM assembly file with arm-linux-gnueabihf-gcc, "
            "run it with qemu-arm-static, then print stdout and $?."
        )
    )
    parser.add_argument("asm_file", type=Path, help="path to the assembly file")
    parser.add_argument(
        "--cc",
        default="arm-linux-gnueabihf-gcc",
        help="cross compiler to use (default: arm-linux-gnueabihf-gcc)",
    )
    parser.add_argument(
        "--qemu",
        default="qemu-arm-static",
        help="ARM qemu runner to use (default: qemu-arm-static)",
    )
    parser.add_argument(
        "-o",
        "--output",
        type=Path,
        help="output executable path; defaults to an auto-cleaned temporary file",
    )
    parser.add_argument(
        "-g",
        "--gdb",
        action="store_true",
        help="compile with -g and run qemu in gdb stub mode",
    )
    parser.add_argument(
        "--gdb-port",
        default="1234",
        metavar="PORT",
        help="qemu gdb stub port used with -g/--gdb (default: 1234)",
    )
    parser.add_argument(
        "--gdb-command",
        help=(
            "gdb command shown in debug instructions "
            "(default: first available of arm-linux-gnueabihf-gdb, gdb-multiarch, gdb)"
        ),
    )
    parser.add_argument(
        "--no-static",
        action="store_true",
        help="do not pass -static to the compiler",
    )
    parser.add_argument(
        "--gcc-arg",
        action="append",
        default=[],
        help="extra argument passed to gcc; repeat for multiple args",
    )
    parser.add_argument(
        "--std-c",
        type=Path,
        default=Path("tests/std.c"),
        help="C runtime source compiled with the assembly file (default: tests/std.c)",
    )
    parser.add_argument(
        "--no-std-c",
        action="store_true",
        help="do not compile tests/std.c with the assembly file",
    )
    parser.add_argument(
        "--stdin-file",
        type=Path,
        help="file used as stdin when running the compiled program",
    )
    args = parser.parse_args(tool_args)
    args.program_args = program_args
    return args


def require_command(command: str) -> None:
    if shutil.which(command) is None:
        print(f"error: command not found: {command}", file=sys.stderr)
        sys.exit(127)


def run_command(
    command: list[str],
    *,
    stdin: bytes | None = None,
) -> subprocess.CompletedProcess[bytes]:
    try:
        return subprocess.run(command, input=stdin, capture_output=True, check=False)
    except OSError as err:
        print(f"error: failed to run {command[0]}: {err}", file=sys.stderr)
        sys.exit(127)


def choose_gdb_command(requested_command: str | None) -> str:
    if requested_command:
        return requested_command

    for command in ("arm-linux-gnueabihf-gdb", "gdb-multiarch", "gdb"):
        if shutil.which(command) is not None:
            return command

    return "gdb-multiarch"


def main() -> int:
    args = parse_args()
    asm_file = args.asm_file
    if not asm_file.is_file():
        print(f"error: assembly file not found: {asm_file}", file=sys.stderr)
        return 2

    std_c = args.std_c
    if not args.no_std_c and not std_c.is_file():
        print(f"error: std C file not found: {std_c}", file=sys.stderr)
        return 2
    if args.stdin_file is not None and not args.stdin_file.is_file():
        print(f"error: stdin file not found: {args.stdin_file}", file=sys.stderr)
        return 2

    require_command(args.cc)
    require_command(args.qemu)

    with tempfile.TemporaryDirectory(prefix="run-arm-asm-") as temp_dir:
        output = args.output or Path(temp_dir) / asm_file.with_suffix("").name

        compile_cmd = [args.cc]
        if not args.no_static:
            compile_cmd.append("-static")
        if args.gdb:
            compile_cmd.append("-g")
        compile_cmd.extend(args.gcc_arg)
        compile_cmd.append(str(asm_file))
        if not args.no_std_c:
            compile_cmd.append(str(std_c))
        compile_cmd.extend(["-o", str(output)])

        compile_result = run_command(compile_cmd)
        sys.stdout.buffer.write(compile_result.stdout)
        sys.stderr.buffer.write(compile_result.stderr)
        if compile_result.returncode != 0:
            return compile_result.returncode

        qemu_cmd = [args.qemu]
        if args.gdb:
            qemu_cmd.extend(["-g", args.gdb_port])
            resolved_output = output.resolve()
            gdb_command = choose_gdb_command(args.gdb_command)
            print(f"executable: {resolved_output}", file=sys.stderr, flush=True)
            print(
                f"gdb: {gdb_command} {resolved_output} "
                f"-ex 'target remote :{args.gdb_port}'",
                file=sys.stderr,
                flush=True,
            )
            print("gdb: use 'si' or 'c'; do not use 'run'", file=sys.stderr, flush=True)
        qemu_cmd.extend([str(output), *args.program_args])

        stdin = args.stdin_file.read_bytes() if args.stdin_file is not None else None
        run_result = run_command(qemu_cmd, stdin=stdin)
        sys.stdout.buffer.write(run_result.stdout)
        sys.stderr.buffer.write(run_result.stderr)
        if run_result.stdout and not run_result.stdout.endswith(b"\n"):
            print()
        print(f"$? = {run_result.returncode}")
        return 0


if __name__ == "__main__":
    raise SystemExit(main())
