const std = @import("std");
const builtin = @import("builtin");

const headers = &[_][]const u8{
    "common.h",
    "ast.h",
    "parser.h",
    "zir.h",
    "astgen.h",
};

const c_lib_files = &[_][]const u8{
    "tokenizer.c",
    "ast.c",
    "zig0.c",
    "parser.c",
    "zir.c",
    "astgen.c",
};

const all_c_files = c_lib_files ++ &[_][]const u8{"main.c"};

const cflags = &[_][]const u8{
    "-std=c11",
    "-Wall",
    "-Wvla",
    "-Wextra",
    "-Werror",
    "-Wshadow",
    "-Wswitch",
    "-Walloca",
    "-Wformat=2",
    "-fno-common",
    "-Wconversion",
    "-Wuninitialized",
    "-Wdouble-promotion",
    "-fstack-protector-all",
    "-Wimplicit-fallthrough",
    "-Wno-unused-function", // TODO remove once refactoring is done
    //"-D_FORTIFY_SOURCE=2", // consider when optimization flags are enabled
};

const compilers = &[_][]const u8{ "zig", "clang", "gcc", "tcc" };

pub fn build(b: *std.Build) !void {
    const optimize = b.standardOptimizeOption(.{});

    const cc = b.option([]const u8, "cc", "C compiler") orelse "zig";
    const no_exec = b.option(bool, "no-exec", "Compile test binary without running it") orelse false;
    const valgrind = b.option(bool, "valgrind", "Run tests under valgrind") orelse false;
    const test_timeout = b.option([]const u8, "test-timeout", "Test execution timeout (default: 10s, none with valgrind)");

    const target = blk: {
        var query = b.standardTargetOptionsQueryOnly(.{});
        if (valgrind) {
            const arch = query.cpu_arch orelse builtin.cpu.arch;
            if (arch == .x86_64) {
                query.cpu_features_sub.addFeature(@intFromEnum(std.Target.x86.Feature.avx512f));
            }
        }
        break :blk b.resolveTargetQuery(query);
    };

    const test_step = b.step("test", "Run unit tests");
    addTestStep(b, test_step, target, optimize, cc, no_exec, valgrind, test_timeout);

    const fmt_step = b.step("fmt", "clang-format");
    const clang_format = b.addSystemCommand(&.{ "clang-format", "-i" });
    for (all_c_files ++ headers) |f| clang_format.addFileArg(b.path(f));
    fmt_step.dependOn(&clang_format.step);

    const lint_step = b.step("lint", "Run linters");

    for (all_c_files) |cfile| {
        const clang_analyze = b.addSystemCommand(&.{
            "clang",
            "--analyze",
            "--analyzer-output",
            "text",
            "-Wno-unused-command-line-argument",
            "-Werror",
            // false positive in astgen.c comptimeDecl: analyzer cannot track
            // scratch_instructions ownership through pointer parameters.
            "-Xclang",
            "-analyzer-disable-checker",
            "-Xclang",
            "unix.Malloc",
        });
        clang_analyze.addFileArg(b.path(cfile));
        clang_analyze.expectExitCode(0);
        lint_step.dependOn(&clang_analyze.step);

        // TODO(motiejus) re-enable once project
        // nears completion. Takes too long for comfort.
        //const gcc_analyze = b.addSystemCommand(&.{
        //    "gcc",
        //    "-c",
        //    "--analyzer",
        //    "-Werror",
        //    "-o",
        //    "/dev/null",
        //});
        //gcc_analyze.addFileArg(b.path(cfile));
        //gcc_analyze.expectExitCode(0);
        //lint_step.dependOn(&gcc_analyze.step);

        const cppcheck = b.addSystemCommand(&.{
            "cppcheck",
            "--quiet",
            "--error-exitcode=1",
            "--check-level=exhaustive",
            "--enable=all",
            "--inline-suppr",
            "--suppress=missingIncludeSystem",
            "--suppress=checkersReport",
            "--suppress=unusedFunction", // TODO remove after plumbing is done
            "--suppress=unusedStructMember", // TODO remove after plumbing is done
            "--suppress=unmatchedSuppression",
        });
        cppcheck.addFileArg(b.path(cfile));
        cppcheck.expectExitCode(0);
        lint_step.dependOn(&cppcheck.step);
    }

    const fmt_check = b.addSystemCommand(&.{ "clang-format", "--dry-run", "-Werror" });
    for (all_c_files ++ headers) |f| fmt_check.addFileArg(b.path(f));
    fmt_check.expectExitCode(0);
    b.default_step.dependOn(&fmt_check.step);

    for (compilers) |compiler| {
        addTestStep(b, b.default_step, target, optimize, compiler, false, valgrind, test_timeout);
    }

    const all_step = b.step("all", "Run fmt check, lint, and tests with all compilers");
    all_step.dependOn(b.default_step);
    all_step.dependOn(lint_step);
}

fn addTestStep(
    b: *std.Build,
    step: *std.Build.Step,
    target: std.Build.ResolvedTarget,
    optimize: std.builtin.OptimizeMode,
    cc: []const u8,
    no_exec: bool,
    valgrind: bool,
    test_timeout: ?[]const u8,
) void {
    const test_mod = b.createModule(.{
        .root_source_file = b.path("test_all.zig"),
        .optimize = optimize,
        .target = target,
    });
    test_mod.addIncludePath(b.path("."));

    // TODO(zig 0.16+): remove this if block entirely; keep only the addLibrary branch.
    // Also delete addCObjectsDirectly.
    // Zig 0.15's ELF archive parser fails on archives containing odd-sized objects
    // (off-by-one after 2-byte alignment). This is fixed on zig master/0.16.
    if (comptime builtin.zig_version.order(.{ .major = 0, .minor = 16, .patch = 0 }) == .lt) {
        addCObjectsDirectly(b, test_mod, cc, optimize);
    } else {
        const lib_mod = b.createModule(.{
            .optimize = optimize,
            .target = target,
            .link_libc = true,
        });
        const lib = b.addLibrary(.{
            .name = b.fmt("zig0-{s}", .{cc}),
            .root_module = lib_mod,
        });
        addCSources(b, lib.root_module, cc, optimize);
        test_mod.linkLibrary(lib);
    }

    const test_exe = b.addTest(.{
        .root_module = test_mod,
        .use_llvm = false,
        .use_lld = false,
    });
    const timeout: ?[]const u8 = test_timeout orelse if (valgrind) null else "10";
    if (valgrind) {
        if (timeout) |t|
            test_exe.setExecCmd(&.{
                "timeout",
                t,
                "valgrind",
                "--error-exitcode=2",
                "--leak-check=full",
                "--show-leak-kinds=all",
                "--errors-for-leak-kinds=all",
                "--track-fds=yes",
                null,
            })
        else
            test_exe.setExecCmd(&.{
                "valgrind",
                "--error-exitcode=2",
                "--leak-check=full",
                "--show-leak-kinds=all",
                "--errors-for-leak-kinds=all",
                "--track-fds=yes",
                null,
            });
    } else {
        test_exe.setExecCmd(&.{ "timeout", timeout orelse "10", null });
    }
    if (no_exec) {
        const install = b.addInstallArtifact(test_exe, .{});
        step.dependOn(&install.step);
    } else {
        step.dependOn(&b.addRunArtifact(test_exe).step);
    }
}

fn addCSources(
    b: *std.Build,
    mod: *std.Build.Module,
    cc: []const u8,
    optimize: std.builtin.OptimizeMode,
) void {
    if (std.mem.eql(u8, cc, "zig")) {
        mod.addCSourceFiles(.{ .files = c_lib_files, .flags = cflags });
    } else for (c_lib_files) |cfile| {
        const cc1 = b.addSystemCommand(&.{cc});
        cc1.addArgs(cflags ++ .{"-g"});
        cc1.addArg(switch (optimize) {
            .Debug => "-O0",
            .ReleaseFast, .ReleaseSafe => "-O3",
            .ReleaseSmall => "-Os",
        });
        cc1.addArg("-c");
        cc1.addFileArg(b.path(cfile));
        cc1.addArg("-o");
        mod.addObjectFile(cc1.addOutputFileArg(b.fmt("{s}.o", .{cfile[0 .. cfile.len - 2]})));
    }
}

// TODO(zig 0.16+): delete this function.
fn addCObjectsDirectly(
    b: *std.Build,
    mod: *std.Build.Module,
    cc: []const u8,
    optimize: std.builtin.OptimizeMode,
) void {
    addCSources(b, mod, cc, optimize);
    mod.linkSystemLibrary("c", .{});
}
