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

fn run_process_and_capture_stdout(b: *std.Build, argv: []const []const u8) ![]const u8 {
    const result = std.process.Child.run(.{
        .allocator = b.allocator,
        .argv = argv,
    }) catch |err| return err;
    switch (result.term) {
        .Exited => |exit_code| {
            if (exit_code != 0) {
                return error.SpawnError;
            }
        },
        else => return error.SpawnError,
    }

    return result.stdout;
}

fn file_find_in_path(allocator: std.mem.Allocator, file_name: []const u8, path_env: []const u8, extension: []const u8) ?[]const u8 {
    const path_env_separator = switch (builtin.os.tag) {
        .windows => ';',
        else => ':',
    };
    const path_separator = switch (builtin.os.tag) {
        .windows => '\\',
        else => '/',
    };
    var env_it = std.mem.splitScalar(u8, path_env, path_env_separator);
    const result: ?[]const u8 = while (env_it.next()) |dir_path| {
        const full_path = std.mem.concatWithSentinel(allocator, u8, &.{ dir_path, &[1]u8{path_separator}, file_name, extension }, 0) catch unreachable;
        const file = std.fs.cwd().openFile(full_path, .{}) catch continue;
        file.close();
        break full_path;
    } else null;
    return result;
}

fn executable_find_in_path(allocator: std.mem.Allocator, file_name: []const u8, path_env: []const u8) ?[]const u8 {
    const extension = switch (builtin.os.tag) {
        .windows => ".exe",
        else => "",
    };
    return file_find_in_path(allocator, file_name, path_env, extension);
}

const CmakeBuildType = enum {
    Debug,
    RelWithDebInfo,
    MinSizeRel,
    Release,

    fn from_zig_build_type(o: std.builtin.OptimizeMode) CmakeBuildType {
        return switch (o) {
            .Debug => .Debug,
            .ReleaseSafe => .RelWithDebInfo,
            .ReleaseSmall => .MinSizeRel,
            .ReleaseFast => .Release,
        };
    }
};

var system_llvm: bool = undefined;
var target: std.Build.ResolvedTarget = undefined;
var optimize: std.builtin.OptimizeMode = undefined;
var env: std.process.EnvMap = undefined;

const BuildMode = enum {
    debug_none,
    debug_fast,
    debug_size,
    soft_optimize,
    optimize_for_speed,
    optimize_for_size,
    aggressively_optimize_for_speed,
    aggressively_optimize_for_size,
};

pub fn build(b: *std.Build) !void {
    env = try std.process.getEnvMap(b.allocator);
    target = b.standardTargetOptions(.{});
    optimize = b.standardOptimizeOption(.{});
    system_llvm = b.option(bool, "system_llvm", "Link against system LLVM libraries") orelse false;

    const c_abi = b.addObject(.{
        .name = "c_abi",
        .link_libc = true,
        .root_module = b.createModule(.{
            .target = target,
            .optimize = optimize,
            .link_libc = true,
            .sanitize_c = false,
        }),
        .optimize = optimize,
    });
    c_abi.addCSourceFiles(.{
        .files = &.{"tests/c_abi.c"},
        .flags = &.{"-g"},
    });

    const path = env.get("PATH") orelse unreachable;

    const stack_trace_library = b.addObject(.{
        .name = "stack_trace",
        .root_module = b.createModule(.{
            .target = target,
            .optimize = .ReleaseFast,
            .root_source_file = b.path("src/stack_trace.zig"),
            .link_libc = true,
        }),
    });

    const exe_mod = b.createModule(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
        .link_libc = true,
        .sanitize_c = false,
    });
    const configuration = b.addOptions();
    configuration.addOptionPath("c_abi_object_path", c_abi.getEmittedBin());
    exe_mod.addOptions("configuration", configuration);

    const exe = b.addExecutable(.{
        .name = "bloat-buster",
        .root_module = exe_mod,
        .link_libc = true,
    });
    exe.addObject(stack_trace_library);
    var llvm_libs = std.ArrayList([]const u8).init(b.allocator);
    var flags = std.ArrayList([]const u8).init(b.allocator);
    const llvm_config_path = if (b.option([]const u8, "llvm_prefix", "LLVM prefix")) |llvm_prefix| blk: {
        const full_path = try std.mem.concat(b.allocator, u8, &.{ llvm_prefix, "/bin/llvm-config" });
        const f = std.fs.cwd().openFile(full_path, .{}) catch return error.llvm_not_found;
        f.close();
        break :blk full_path;
    } else if (system_llvm) executable_find_in_path(b.allocator, "llvm-config", path) orelse return error.llvm_not_found else blk: {
        const home_env = switch (@import("builtin").os.tag) {
            .windows => "USERPROFILE",
            else => "HOME",
        };
        const home_path = env.get(home_env) orelse unreachable;
        const is_ci = std.mem.eql(u8, (env.get("BB_CI") orelse "0"), "1");
        const download_dir = try std.mem.concat(b.allocator, u8, &.{ home_path, "/Downloads" });
        std.fs.makeDirAbsolute(download_dir) catch {};
        const cmake_build_type = if (is_ci) CmakeBuildType.from_zig_build_type(optimize) else CmakeBuildType.Release;
        const llvm_base = try std.mem.concat(b.allocator, u8, &.{ "llvm-", @tagName(target.result.cpu.arch), "-", @tagName(target.result.os.tag), "-", @tagName(cmake_build_type) });
        const base = try std.mem.concat(b.allocator, u8, &.{ download_dir, "/", llvm_base });
        const full_path = try std.mem.concat(b.allocator, u8, &.{ base, "/bin/llvm-config" });

        const f = std.fs.cwd().openFile(full_path, .{}) catch {
            const url = try std.mem.concat(b.allocator, u8, &.{ "https://github.com/birth-software/llvm/releases/download/v19.1.7/", llvm_base, ".7z" });
            var result = try std.process.Child.run(.{
                .allocator = b.allocator,
                .argv = &.{ "wget", "-P", download_dir, url },
                .max_output_bytes = std.math.maxInt(usize),
            });
            var success = false;
            switch (result.term) {
                .Exited => |exit_code| {
                    success = exit_code == 0;
                },
                else => {},
            }

            if (!success) {
                std.debug.print("{s}\n{s}\n", .{ result.stdout, result.stderr });
            }

            if (success) {
                const file_7z = try std.mem.concat(b.allocator, u8, &.{ base, ".7z" });
                result = try std.process.Child.run(.{
                    .allocator = b.allocator,
                    .argv = &.{ "7z", "x", try std.mem.concat(b.allocator, u8, &.{ "-o", download_dir }), file_7z },
                    .max_output_bytes = std.math.maxInt(usize),
                });
                success = false;
                switch (result.term) {
                    .Exited => |exit_code| {
                        success = exit_code == 0;
                    },
                    else => {},
                }

                if (!success) {
                    std.debug.print("{s}\n{s}\n", .{ result.stdout, result.stderr });
                }

                break :blk full_path;
            }

            return error.llvm_not_found;
        };

        f.close();
        break :blk full_path;
    };
    const llvm_components_result = try run_process_and_capture_stdout(b, &.{ llvm_config_path, "--components" });
    var it = std.mem.splitScalar(u8, llvm_components_result, ' ');
    {
        var args = std.ArrayList([]const u8).init(b.allocator);
        try args.append(llvm_config_path);
        try args.append("--libs");
        while (it.next()) |component| {
            try args.append(std.mem.trimRight(u8, component, "\n"));
        }
        const llvm_libs_result = try run_process_and_capture_stdout(b, args.items);
        it = std.mem.splitScalar(u8, llvm_libs_result, ' ');
    }

    while (it.next()) |lib| {
        const llvm_lib = std.mem.trimLeft(u8, std.mem.trimRight(u8, lib, "\n"), "-l");
        try llvm_libs.append(llvm_lib);
    }

    const llvm_cxx_flags_result = try run_process_and_capture_stdout(b, &.{ llvm_config_path, "--cxxflags" });
    it = std.mem.splitScalar(u8, llvm_cxx_flags_result, ' ');
    while (it.next()) |flag| {
        const llvm_cxx_flag = std.mem.trimRight(u8, flag, "\n");
        try flags.append(llvm_cxx_flag);
    }

    const llvm_lib_dir = std.mem.trimRight(u8, try run_process_and_capture_stdout(b, &.{ llvm_config_path, "--libdir" }), "\n");

    if (optimize != .ReleaseSmall) {
        try flags.append("-g");
    }

    try flags.append("-fno-rtti");

    exe.addLibraryPath(.{ .cwd_relative = llvm_lib_dir });

    const a = std.fs.cwd().openDir("/usr/lib/x86_64-linux-gnu/", .{});
    if (a) |_| {
        var dir = a catch unreachable;
        dir.close();
        exe.addLibraryPath(.{ .cwd_relative = "/usr/lib/x86_64-linux-gnu/" });
    } else |err| {
        err catch {};
    }

    exe.addCSourceFiles(.{
        .files = &.{"src/llvm.cpp"},
        .flags = flags.items,
    });

    var dir = try std.fs.cwd().openDir("/usr/include/c++", .{
        .iterate = true,
    });
    var iterator = dir.iterate();
    const gcc_version = while (try iterator.next()) |entry| {
        if (entry.kind == .directory) {
            break entry.name;
        }
    } else return error.include_cpp_dir_not_found;
    dir.close();
    const general_cpp_include_dir = try std.mem.concat(b.allocator, u8, &.{ "/usr/include/c++/", gcc_version });
    exe.addIncludePath(.{ .cwd_relative = general_cpp_include_dir });

    {
        const arch_cpp_include_dir = try std.mem.concat(b.allocator, u8, &.{ general_cpp_include_dir, "/x86_64-pc-linux-gnu" });
        const d2 = std.fs.cwd().openDir(arch_cpp_include_dir, .{});
        if (d2) |_| {
            var d = d2 catch unreachable;
            d.close();
            exe.addIncludePath(.{ .cwd_relative = arch_cpp_include_dir });
        } else |err| err catch {};
    }

    {
        const arch_cpp_include_dir = try std.mem.concat(b.allocator, u8, &.{ "/usr/include/x86_64-linux-gnu/c++/", gcc_version });
        const d2 = std.fs.cwd().openDir(arch_cpp_include_dir, .{});
        if (d2) |_| {
            var d = d2 catch unreachable;
            d.close();
            exe.addIncludePath(.{ .cwd_relative = arch_cpp_include_dir });
        } else |err| err catch {};
    }

    var found_libcpp = false;

    if (std.fs.cwd().openFile("/usr/lib/libstdc++.so.6", .{})) |file| {
        file.close();
        found_libcpp = true;
        exe.addObjectFile(.{ .cwd_relative = "/usr/lib/libstdc++.so.6" });
    } else |err| {
        err catch {};
    }

    if (std.fs.cwd().openFile("/usr/lib/x86_64-linux-gnu/libstdc++.so.6", .{})) |file| {
        file.close();
        found_libcpp = true;
        exe.addObjectFile(.{ .cwd_relative = "/usr/lib/x86_64-linux-gnu/libstdc++.so.6" });
    } else |err| {
        err catch {};
    }

    if (!found_libcpp) {
        return error.libcpp_not_found;
    }

    const needed_libraries: []const []const u8 = &.{ "unwind", "z", "zstd" };
    for (needed_libraries) |lib| {
        exe.linkSystemLibrary(lib);
    }

    for (llvm_libs.items) |lib| {
        exe.linkSystemLibrary(lib);
    }

    const lld_libs: []const []const u8 = &.{ "lldCommon", "lldCOFF", "lldELF", "lldMachO", "lldMinGW", "lldWasm" };
    for (lld_libs) |lib| {
        exe.linkSystemLibrary(lib);
    }

    b.installArtifact(exe);

    for ([_]bool{ false, true }) |is_test| {
        const run_step_name = switch (is_test) {
            true => "test",
            false => "run",
        };

        const debug_step_name = switch (is_test) {
            true => "debug_test",
            false => "debug",
        };

        const command = b.addRunArtifact(exe);
        command.step.dependOn(b.getInstallStep());

        if (is_test) {
            command.addArg("test");
        }

        if (b.args) |args| {
            command.addArgs(args);
        }

        const run_step = b.step(run_step_name, "");
        run_step.dependOn(&command.step);

        const debug_command = std.Build.Step.Run.create(b, b.fmt("{s} {s}", .{ debug_step_name, exe.name }));
        debug_command.addArg("gdb");
        debug_command.addArg("-ex");
        debug_command.addArg("r");
        debug_command.addArg("--args");
        debug_command.addArtifactArg(exe);

        if (is_test) {
            debug_command.addArg("test");
        }

        if (b.args) |args| {
            debug_command.addArgs(args);
        }

        const debug_step = b.step(debug_step_name, "");
        debug_step.dependOn(&debug_command.step);
    }
}