//! Unlike `RunStep` this step will provide emulation, when enabled, to run foreign binaries.
//! When a binary is foreign, but emulation for the target is disabled, the specified binary
//! will not be run and therefore also not validated against its output.
//! This step can be useful when wishing to run a built binary on multiple platforms,
//! without having to verify if it's possible to be ran against.

const std = @import("../std.zig");
const build = std.build;
const Step = std.build.Step;
const Builder = std.build.Builder;
const LibExeObjStep = std.build.LibExeObjStep;
const RunStep = std.build.RunStep;

const fs = std.fs;
const process = std.process;
const EnvMap = process.EnvMap;

const EmulatableRunStep = @This();

pub const base_id = .emulatable_run;

const max_stdout_size = 1 * 1024 * 1024; // 1 MiB

step: Step,
builder: *Builder,

/// The artifact (executable) to be run by this step
exe: *LibExeObjStep,

/// Set this to `null` to ignore the exit code for the purpose of determining a successful execution
expected_exit_code: ?u8 = 0,

/// Override this field to modify the environment
env_map: ?*EnvMap,

/// Set this to modify the current working directory
cwd: ?[]const u8,

stdout_action: RunStep.StdIoAction = .inherit,
stderr_action: RunStep.StdIoAction = .inherit,

/// When set to true, hides the warning of skipping a foreign binary which cannot be run on the host
/// or through emulation.
hide_foreign_binaries_warning: bool,

/// Creates a step that will execute the given artifact. This step will allow running the
/// binary through emulation when any of the emulation options such as `enable_rosetta` are set to true.
/// When set to false, and the binary is foreign, running the executable is skipped.
/// Asserts given artifact is an executable.
pub fn create(builder: *Builder, name: []const u8, artifact: *LibExeObjStep) *EmulatableRunStep {
    std.debug.assert(artifact.kind == .exe or artifact.kind == .test_exe);
    const self = builder.allocator.create(EmulatableRunStep) catch unreachable;

    const option_name = "hide-foreign-warnings";
    const hide_warnings = if (builder.available_options_map.get(option_name) == null) warn: {
        break :warn builder.option(bool, option_name, "Hide the warning when a foreign binary which is incompatible is skipped") orelse false;
    } else false;

    self.* = .{
        .builder = builder,
        .step = Step.init(.emulatable_run, name, builder.allocator, make),
        .exe = artifact,
        .env_map = null,
        .cwd = null,
        .hide_foreign_binaries_warning = hide_warnings,
    };
    self.step.dependOn(&artifact.step);

    return self;
}

fn make(step: *Step) !void {
    const self = @fieldParentPtr(EmulatableRunStep, "step", step);
    const host_info = self.builder.host;

    var argv_list = std.ArrayList([]const u8).init(self.builder.allocator);
    defer argv_list.deinit();

    const need_cross_glibc = self.exe.target.isGnuLibC() and self.exe.is_linking_libc;
    switch (host_info.getExternalExecutor(self.exe.target_info, .{
        .qemu_fixes_dl = need_cross_glibc and self.builder.glibc_runtimes_dir != null,
        .link_libc = self.exe.is_linking_libc,
    })) {
        .native => {},
        .rosetta => if (!self.builder.enable_rosetta) return warnAboutForeignBinaries(self),
        .wine => |bin_name| if (self.builder.enable_wine) {
            try argv_list.append(bin_name);
        } else return,
        .qemu => |bin_name| if (self.builder.enable_qemu) {
            const glibc_dir_arg = if (need_cross_glibc)
                self.builder.glibc_runtimes_dir orelse return
            else
                null;
            try argv_list.append(bin_name);
            if (glibc_dir_arg) |dir| {
                // TODO look into making this a call to `linuxTriple`. This
                // needs the directory to be called "i686" rather than
                // "i386" which is why we do it manually here.
                const fmt_str = "{s}" ++ fs.path.sep_str ++ "{s}-{s}-{s}";
                const cpu_arch = self.exe.target.getCpuArch();
                const os_tag = self.exe.target.getOsTag();
                const abi = self.exe.target.getAbi();
                const cpu_arch_name: []const u8 = if (cpu_arch == .i386)
                    "i686"
                else
                    @tagName(cpu_arch);
                const full_dir = try std.fmt.allocPrint(self.builder.allocator, fmt_str, .{
                    dir, cpu_arch_name, @tagName(os_tag), @tagName(abi),
                });

                try argv_list.append("-L");
                try argv_list.append(full_dir);
            }
        } else return warnAboutForeignBinaries(self),
        .darling => |bin_name| if (self.builder.enable_darling) {
            try argv_list.append(bin_name);
        } else return warnAboutForeignBinaries(self),
        .wasmtime => |bin_name| if (self.builder.enable_wasmtime) {
            try argv_list.append(bin_name);
            try argv_list.append("--dir=.");
        } else return warnAboutForeignBinaries(self),
        else => return warnAboutForeignBinaries(self),
    }

    if (self.exe.target.isWindows()) {
        // On Windows we don't have rpaths so we have to add .dll search paths to PATH
        RunStep.addPathForDynLibsInternal(&self.step, self.builder, self.exe);
    }

    const executable_path = self.exe.installed_path orelse self.exe.getOutputSource().getPath(self.builder);
    try argv_list.append(executable_path);

    try RunStep.runCommand(
        argv_list.items,
        self.builder,
        self.expected_exit_code,
        self.stdout_action,
        self.stderr_action,
        .Inherit,
        self.env_map,
        self.cwd,
        false,
    );
}

pub fn expectStdErrEqual(self: *EmulatableRunStep, bytes: []const u8) void {
    self.stderr_action = .{ .expect_exact = self.builder.dupe(bytes) };
}

pub fn expectStdOutEqual(self: *EmulatableRunStep, bytes: []const u8) void {
    self.stdout_action = .{ .expect_exact = self.builder.dupe(bytes) };
}

fn warnAboutForeignBinaries(step: *EmulatableRunStep) void {
    if (step.hide_foreign_binaries_warning) return;
    const builder = step.builder;
    const artifact = step.exe;

    const host_name = builder.host.target.zigTriple(builder.allocator) catch unreachable;
    const foreign_name = artifact.target.zigTriple(builder.allocator) catch unreachable;
    const target_info = std.zig.system.NativeTargetInfo.detect(artifact.target) catch unreachable;
    const need_cross_glibc = artifact.target.isGnuLibC() and artifact.is_linking_libc;
    switch (builder.host.getExternalExecutor(target_info, .{
        .qemu_fixes_dl = need_cross_glibc and builder.glibc_runtimes_dir != null,
        .link_libc = artifact.is_linking_libc,
    })) {
        .native => unreachable,
        .bad_dl => |foreign_dl| {
            const host_dl = builder.host.dynamic_linker.get() orelse "(none)";
            std.debug.print("the host system does not appear to be capable of executing binaries from the target because the host dynamic linker is '{s}', while the target dynamic linker is '{s}'. Consider setting the dynamic linker as '{s}'.\n", .{
                host_dl, foreign_dl, host_dl,
            });
        },
        .bad_os_or_cpu => {
            std.debug.print("the host system ({s}) does not appear to be capable of executing binaries from the target ({s}).\n", .{
                host_name, foreign_name,
            });
        },
        .darling => if (!builder.enable_darling) {
            std.debug.print(
                "the host system ({s}) does not appear to be capable of executing binaries " ++
                    "from the target ({s}). Consider enabling darling.\n",
                .{ host_name, foreign_name },
            );
        },
        .rosetta => if (!builder.enable_rosetta) {
            std.debug.print(
                "the host system ({s}) does not appear to be capable of executing binaries " ++
                    "from the target ({s}). Consider enabling rosetta.\n",
                .{ host_name, foreign_name },
            );
        },
        .wine => if (!builder.enable_wine) {
            std.debug.print(
                "the host system ({s}) does not appear to be capable of executing binaries " ++
                    "from the target ({s}). Consider enabling wine.\n",
                .{ host_name, foreign_name },
            );
        },
        .qemu => if (!builder.enable_qemu) {
            std.debug.print(
                "the host system ({s}) does not appear to be capable of executing binaries " ++
                    "from the target ({s}). Consider enabling qemu.\n",
                .{ host_name, foreign_name },
            );
        },
        .wasmtime => {
            std.debug.print(
                "the host system ({s}) does not appear to be capable of executing binaries " ++
                    "from the target ({s}). Consider enabling wasmtime.\n",
                .{ host_name, foreign_name },
            );
        },
    }
}
