//! A wrapper simplifying common usages of the 'translate-c' tool within build scripts. As well as
//! generating a Zig source file, this wrapper also creates a `Build.Module` rooted at that source
//! file, so that Zig compilations may consume it. Typical `Translator` usage might look something
//! like this:
//! ```
//! const my_exe: *Build.Step.Compile = buildMyExe();
//! const libfoo: *Build.Step.Compile = buildLibfoo();
//! // Our goal is to expose `libfoo` to `my_exe.root_module` as a module named "foo".
//! const tc_dep = b.dependency("translate-c", .{});
//! const libfoo_zig: @import("translate-c").Translator = .init(tc_dep, .{
//!     .name = "libfoo",
//!     .c_source_file = b.addWriteFiles().add("wrapper.h",
//!         \\#define _GNU_SOURCE
//!         \\#include <libfoo.h>
//!         \\
//!     ),
//! });
//! libfoo_zig.linkLibrary(libfoo);
//! my_exe.root_module.addImport("foo", libfoo_zig.mod);
//! // Done! The `linkLibrary` call above has also linked `libfoo` into our binary.
//! ```

/// The Zig source file generated by translate-c. It is allowed to use this path directly. However,
/// for common use cases, consider using the `mod` field instead for convenience.
output_file: Build.LazyPath,
/// A `Build.Module` whose root source file is the Zig source file generated by translate-c. In
/// addition, calls to `Translator.linkLibrary`, `Translator.addObject`, etc, will also call the
/// corresponding function on this `Build.Module`, so using it in a Zig module graph will cause
/// the library in question to be automatically linked.
///
/// It is permitted for the caller to call methods such as `Build.Module.addIncludePath` on this
/// `Build.Module`. However, note that modifying this module does not impact C translation, even
/// if calling a method such as `Build.Module.addIncludePath`.
mod: *Build.Module,

/// The `Build.Step.Run` which is actually running translate-c. This can be modified directly to
/// add custom arguments, but prefer using methods like `linkLibrary` where possible.
run: *Build.Step.Run,

pub const Options = struct {
    /// If this name is given, the output file and the `Build.Step.Run` will be named after it.
    name: ?[]const u8 = null,
    /// The C source file being translated. This is typically either a header file (".h") from the
    /// library in question, or a "stub" source file (perhaps from `Build.Step.WriteFiles`) which
    /// contains only preprocessor directives (e.g. `#define` and `#include`).
    c_source_file: Build.LazyPath,
    target: Build.ResolvedTarget,
    optimize: std.builtin.OptimizeMode,
    link_libc: bool = true,
    warnings: enum { ignore, show, @"error" } = .ignore,

    /// Should libraries be imported as modules or installed.
    module_libs: bool = true,
    /// Should static functions be translated as pub.
    pub_static: ?bool = null,
    /// Should function bodies be translated.
    func_bodies: ?bool = null,
    /// Should literals that were expanded from macros be translated as uses of those macros.
    keep_macro_literals: ?bool = null,
    /// Should struct fields be default initalized.
    default_init: ?bool = null,
    /// Control when to treat a trailing array as a flexible array member (default: 2).
    /// 0: any trailing array, 1: size [0]/[1]/[], 2: size [0]/[] (default), 3: [] only.
    strict_flex_arrays: ?enum { @"0", @"1", @"2", @"3" } = null,
};

pub fn init(translate_c_dep: *Build.Dependency, options: Options) Translator {
    return initInner(translate_c_dep.builder, .{
        .exe = translate_c_dep.artifact("translate-c"),
        .aro_resource_dir = translate_c_dep.namedLazyPath("aro_resource_dir"),
        .c_builtins = translate_c_dep.module("c_builtins"),
        .helpers = translate_c_dep.module("helpers"),
    }, options);
}

/// Intended for use only by the translate-c package itself. Outside of this package, prefer `init`.
/// Aside from implementing `init`, this is used for testing the translate-c package, because there
/// is no `Build.Dependency` for us, and also we want to test with different `tc_exe`s.
pub fn initInner(
    b: *Build,
    tc_conf: TranslateCConfig,
    options: Options,
) Translator {
    // Try our best to get a reasonable name; we need one to name the steps and the output file.
    const name = options.name orelse std.fs.path.stem(options.c_source_file.getDisplayName());

    // We start with the basic command: 'path/to/translate-c in.c -o out.zig -MD -MV -MF deps.d'
    const run = b.addRunArtifact(tc_conf.exe);
    run.setName(b.fmt("translate-c {s}", .{name}));
    run.addFileArg(options.c_source_file);
    run.addArg("-o");
    const output_file = run.addOutputFileArg(b.fmt("{s}.zig", .{name}));
    run.addArgs(&.{ "-MD", "-MV", "-MF" });
    _ = run.addDepFileOutputArg("deps.d");
    // Now we are free to add extra args to `run` as needed.

    const mod = b.createModule(.{
        .root_source_file = output_file,
        .target = options.target,
        .optimize = options.optimize,
        .link_libc = options.link_libc,
    });
    mod.addImport("c_builtins", tc_conf.c_builtins);
    mod.addImport("helpers", tc_conf.helpers);

    if (!options.target.query.isNative()) {
        const triple = options.target.query.zigTriple(b.graph.arena) catch @panic("OOM");
        run.addArg(b.fmt("--target={s}", .{triple}));
    }
    if (options.link_libc) {
        // If we're cross-compiling, we need to use Zig's libc directories.
        //
        // Currently calling into Zig's libc detection is also necessary for native targets other
        // than Linux due to deficiencies in Aro's toolchains for non-Linux targets.
        if (!options.target.query.isNative() or options.target.result.os.tag != .linux) {
            run.addArg("-nostdlibinc"); // Aro should still check its builtin dir, but we're providing everything else
            const libc = detectLibCDirs(b, &options.target);
            for (libc.libc_include_dir_list) |include_dir| {
                appendIncludeArg(run, "-isystem", .{ .cwd_relative = include_dir });
            }
            for (libc.libc_framework_dir_list) |framework_dir| {
                appendIncludeArg(run, "-iframework", .{ .cwd_relative = framework_dir });
            }

            // Add Clang builtin includes with `-idirafter` to supplement missing headers
            // like `mm_malloc.h` and `intrin.h` while still preferring Aro's own ones when they exist.
            const clang_intrinsic_include_dir = b.graph.zig_lib_directory.join(b.graph.arena, &.{"include"}) catch @panic("OOM");
            appendIncludeArg(run, "-idirafter", .{ .cwd_relative = clang_intrinsic_include_dir });
        }
    } else {
        run.addArg("-nostdlibinc"); // Builtin headers should be included even when not linking libc
    }

    if (options.target.query.isNativeOs() and options.target.query.isNativeAbi() and options.link_libc) {
        const paths = std.zig.system.NativePaths.detect(
            b.graph.arena,
            b.graph.io,
            &options.target.result,
            &b.graph.environ_map,
        ) catch |err| {
            std.debug.panic("failed to detect native system paths: {t}", .{err});
        };
        for (paths.warnings.items) |warning| {
            std.log.warn("{s}", .{warning});
        }
        for (paths.include_dirs.items) |include_dir| {
            appendIncludeArg(run, "-isystem", .{ .cwd_relative = include_dir });
        }
        for (paths.framework_dirs.items) |framework_dir| {
            appendIncludeArg(run, "-iframework", .{ .cwd_relative = framework_dir });
        }
        // `paths.rpaths` and `paths.lib_dirs` are intentionally omitted because Aro does not yet support
        // the `-rpath` or `-L` flags. That's fine because Aro isn't doing any codegen, only parsing and
        // semantic analysis, so only needs to know include paths.
    }

    for (b.search_prefixes.items) |search_prefix| {
        appendIncludeArg(run, "-I", .{ .cwd_relative = b.pathJoin(&.{ search_prefix, "include" }) });
    }

    switch (options.warnings) {
        .ignore => run.addArg("-w"),
        .show => {},
        .@"error" => run.addArg("-Werror"),
    }

    appendIncludeArg(run, "-resource-dir", tc_conf.aro_resource_dir);

    addFlag(run, "module-libs", options.module_libs);
    addFlag(run, "pub-static", options.pub_static);
    addFlag(run, "func-bodies", options.func_bodies);
    addFlag(run, "keep-macro-literals", options.keep_macro_literals);
    addFlag(run, "default-init", options.default_init);
    if (options.strict_flex_arrays) |level| {
        run.addArg(b.fmt("-fstrict-flex-arrays={d}", .{@intFromEnum(level)}));
    }

    return .{
        .output_file = output_file,
        .mod = mod,
        .run = run,
    };
}
/// Intended for use only by the translate-c package itself.
pub const TranslateCConfig = struct {
    exe: *Build.Step.Compile,
    aro_resource_dir: Build.LazyPath,
    c_builtins: *Build.Module,
    helpers: *Build.Module,
};

/// Links `lib` to `t.mod`, and exposes any headers installed by `lib` to translate-c.
pub fn linkLibrary(t: *const Translator, lib: *Build.Step.Compile) void {
    t.mod.linkLibrary(lib);
    appendIncludeArg(t.run, "-I", lib.getEmittedIncludeTree());
}
/// Adds `obj` to `t.mod`, and exposes any headers installed by `obj` to translate-c.
pub fn addObject(t: *const Translator, obj: *Build.Step.Compile) void {
    t.mod.addObject(obj);
    appendIncludeArg(t.run, "-I", obj.getEmittedIncludeTree());
}
/// Exposes the include path `path` to both translate-c and to `t.mod`.
pub fn addIncludePath(t: *const Translator, path: Build.LazyPath) void {
    t.mod.addIncludePath(path);
    appendIncludeArg(t.run, "-I", path);
}
/// Exposes the system include path `path` to both translate-c and to `t.mod`.
pub fn addSystemIncludePath(t: *const Translator, path: Build.LazyPath) void {
    t.mod.addSystemIncludePath(path);
    appendIncludeArg(t.run, "-isystem", path);
}
/// Exposes the "after" include path `path` to both translate-c and to `t.mod`.
pub fn addAfterIncludePath(t: *const Translator, path: Build.LazyPath) void {
    t.mod.addAfterIncludePath(path);
    appendIncludeArg(t.run, "-idirafter", path);
}
/// Exposes the framework path `path` to both translate-c and to `t.mod`.
pub fn addFrameworkPath(t: *const Translator, path: Build.LazyPath) void {
    t.mod.addFrameworkPath(path);
    appendIncludeArg(t.run, "-F", path);
}
/// Exposes the system framework path `path` to both translate-c and to `t.mod`.
pub fn addSystemFrameworkPath(t: *const Translator, path: Build.LazyPath) void {
    t.mod.addSystemFrameworkPath(path);
    appendIncludeArg(t.run, "-iframework", path);
}
/// Exposes the embed path `path` to both translate-c and to `t.mod`.
pub fn addEmbedPath(t: *const Translator, path: Build.LazyPath) void {
    t.mod.addEmbedPath(path);
    t.run.addPrefixedDirectoryArg("--embed-dir=", path);
}
/// Exposes the config header generaeted by `ch` to both translate-c and to `t.mod`.
pub fn addConfigHeader(t: *const Translator, ch: *Build.Step.ConfigHeader) void {
    t.mod.addConfigHeader(ch);
    appendIncludeArg(t.run, "-I", ch.getOutputDir());
}

/// Links `lib` to `t.mod`, and exposes any headers installed by `lib` to translate-c.
pub fn linkSystemLibrary(
    t: *const Translator,
    name: []const u8,
    options: std.Build.Module.LinkSystemLibraryOptions,
) void {
    t.mod.linkSystemLibrary(name, options);
    if (options.use_pkg_config != .no) {
        if (Build.Step.Compile.runPkgConfig(&t.run.step, name)) |result| {
            t.run.addArgs(result.cflags);
        } else |err| {
            std.debug.panic("pkg-config failed for library {s}: {t}", .{ name, err });
        }
    }
}

/// If the value is omitted, it is set to 1.
/// `name` and `value` need not live longer than the function call.
pub fn defineCMacro(t: *const Translator, name: []const u8, value: ?[]const u8) void {
    const b = t.mod.owner;
    const macro = b.fmt("-D{s}={s}", .{ name, value orelse "1" });
    t.mod.c_macros.append(b.allocator, macro) catch @panic("OOM");
    t.run.addArg(macro);
}

fn addFlag(run: *Build.Step.Run, name: []const u8, opt_value: ?bool) void {
    const value = opt_value orelse return;
    const prefix = if (value) "-f" else "-fno-";
    const arg = run.step.owner.fmt("{s}{s}", .{ prefix, name });
    run.addArg(arg);
}

/// Helper function for adding things like `-I /path/to/include/dir` to arg vectors.
fn appendIncludeArg(run: *Build.Step.Run, arg: []const u8, path: Build.LazyPath) void {
    run.addArg(arg);
    run.addDirectoryArg(path);
}
/// This is just a wrapper for `std.zig.LibCDirs.detect` which caches the per-target results. It is
/// desirable to cache this because `detect` has been observed to be quite slow on macOS for native
/// targets, which leads to the 'configure' phase for this project taking a full minute(!) due to
/// all of the test cases.
///
/// This function assumes `link_libc == true`.
fn detectLibCDirs(b: *Build, target: *const Build.ResolvedTarget) std.zig.LibCDirs {
    // TODO: this is a bad solution. One of three things needs to happen:
    // * The Zig build system gets a better way to cache state like this
    // * Aro or translate-c starts performing this query itself
    // * `LibCDirs` stops being slow, making the caching unnecessary
    const S = struct {
        var lib_c_dirs_cache: std.AutoArrayHashMapUnmanaged(std.Target.Query, std.zig.LibCDirs) = .empty;
    };
    // Assumes that `b.graph.arena` is the same every time this is called. That assumption is valid
    // because `b.graph` is state which is local to the build runner and shared with the whole graph.
    const gop = S.lib_c_dirs_cache.getOrPut(b.graph.arena, target.query) catch @panic("OOM");
    if (!gop.found_existing) {
        gop.value_ptr.* = std.zig.LibCDirs.detect(
            b.graph.arena,
            b.graph.io,
            b.graph.zig_lib_directory.path orelse ".",
            &target.result,
            target.query.isNativeAbi(),
            true,
            null,
            &b.graph.environ_map,
        ) catch |err| std.debug.panic("failed to locate libc: {s}", .{@errorName(err)});
    }
    return gop.value_ptr.*;
}

const std = @import("std");
const Build = std.Build;
const Translator = @This();
