const std = @import("std");
const resolve_path = @import("./src/resolver/resolve_path.zig");

pub fn addPicoHTTP(step: *std.build.LibExeObjStep, comptime with_obj: bool) void {
    const picohttp = step.addPackage(.{
        .name = "picohttp",
        .path = .{ .path = "src/deps/picohttp.zig" },
    });

    _ = step.addPackage(.{
        .name = "iguanaTLS",
        .path = .{ .path = "src/deps/iguanaTLS/src/main.zig" },
    });

    step.addIncludeDir("src/deps");

    if (with_obj) {
        step.addObjectFile("src/deps/picohttpparser.o");
    }

    // step.add("/Users/jarred/Code/WebKit/WebKitBuild/Release/lib/libWTF.a");

    // ./Tools/Scripts/build-jsc --jsc-only  --cmakeargs="-DENABLE_STATIC_JSC=ON"
    // set -gx ICU_INCLUDE_DIRS "/usr/local/opt/icu4c/include"
    // homebrew-provided icu4c
}
var x64 = "x64";
pub fn build(b: *std.build.Builder) !void {
    // Standard target options allows the person running `zig build` to choose
    // what target to build for. Here we do not override the defaults, which
    // means any target is allowed, and the default is native. Other options
    // for restricting supported target set are available.
    const target = b.standardTargetOptions(.{});
    // Standard release options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
    const mode = b.standardReleaseOptions();

    var cwd_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
    const cwd: []const u8 = b.pathFromRoot(".");
    var exe: *std.build.LibExeObjStep = undefined;
    var output_dir_buf = std.mem.zeroes([4096]u8);
    var bin_label = if (mode == std.builtin.Mode.Debug) "packages/debug-bun-cli-" else "packages/bun-cli-";

    var triplet_buf: [64]u8 = undefined;
    var os_tagname = @tagName(target.getOs().tag);
    if (std.mem.eql(u8, os_tagname, "macos")) {
        os_tagname = "darwin";
    }

    std.mem.copy(
        u8,
        &triplet_buf,
        os_tagname,
    );
    var osname = triplet_buf[0..os_tagname.len];
    triplet_buf[osname.len] = '-';

    std.mem.copy(u8, triplet_buf[osname.len + 1 ..], @tagName(target.getCpuArch()));
    var cpuArchName = triplet_buf[osname.len + 1 ..][0..@tagName(target.getCpuArch()).len];
    std.mem.replaceScalar(u8, cpuArchName, '_', '-');
    if (std.mem.eql(u8, cpuArchName, "x86-64")) {
        std.mem.copy(u8, cpuArchName, "x64");
        cpuArchName = cpuArchName[0..3];
    }

    var triplet = triplet_buf[0 .. osname.len + cpuArchName.len + 1];

    const output_dir_base = try std.fmt.bufPrint(&output_dir_buf, "{s}{s}/bin", .{ bin_label, triplet });
    const output_dir = b.pathFromRoot(output_dir_base);
    const bun_executable_name = if (mode == std.builtin.Mode.Debug) "bun-debug" else "bun";

    if (target.getOsTag() == .wasi) {
        exe.enable_wasmtime = true;
        exe = b.addExecutable(bun_executable_name, "src/main_wasi.zig");
        exe.linkage = .dynamic;
        exe.setOutputDir(output_dir);
    } else if (target.getCpuArch().isWasm()) {
        // exe = b.addExecutable(
        //     "bun",
        //     "src/main_wasm.zig",
        // );
        // exe.is_linking_libc = false;
        // exe.is_dynamic = true;
        var lib = b.addExecutable(bun_executable_name, "src/main_wasm.zig");
        lib.single_threaded = true;
        // exe.want_lto = true;
        // exe.linkLibrary(lib);

        if (mode == std.builtin.Mode.Debug) {
            // exception_handling
            var features = target.getCpuFeatures();
            features.addFeature(2);
            target.updateCpuFeatures(&features);
        } else {
            // lib.strip = true;
        }

        lib.setOutputDir(output_dir);
        lib.want_lto = true;
        b.install_path = lib.getOutputSource().getPath(b);

        std.debug.print("Build: ./{s}\n", .{b.install_path});
        b.default_step.dependOn(&lib.step);
        b.verbose_link = true;
        lib.setTarget(target);
        lib.setBuildMode(mode);

        std.fs.deleteTreeAbsolute(std.fs.path.join(b.allocator, &.{ cwd, lib.getOutputSource().getPath(b) }) catch unreachable) catch {};
        var install = b.getInstallStep();
        lib.strip = false;
        lib.install();

        const run_cmd = lib.run();
        run_cmd.step.dependOn(b.getInstallStep());
        if (b.args) |args| {
            run_cmd.addArgs(args);
        }

        const run_step = b.step("run", "Run the app");
        run_step.dependOn(&run_cmd.step);

        return;
    } else {
        exe = b.addExecutable(bun_executable_name, "src/main.zig");
    }
    // exe.setLibCFile("libc.txt");
    exe.linkLibC();
    // exe.linkLibCpp();
    exe.addPackage(.{
        .name = "clap",
        .path = .{ .path = "src/deps/zig-clap/clap.zig" },
    });

    exe.setOutputDir(output_dir);
    var cwd_dir = std.fs.cwd();

    const runtime_hash = std.hash.Wyhash.hash(0, @embedFile("./src/runtime.out.js"));
    const runtime_version_file = std.fs.cwd().openFile("src/runtime.version", .{ .write = true }) catch unreachable;
    runtime_version_file.writer().print("{x}", .{runtime_hash}) catch unreachable;
    defer runtime_version_file.close();

    const fallback_hash = std.hash.Wyhash.hash(0, @embedFile("./src/fallback.out.js"));
    const fallback_version_file = std.fs.cwd().openFile("src/fallback.version", .{ .write = true }) catch unreachable;
    fallback_version_file.writer().print("{x}", .{fallback_hash}) catch unreachable;
    defer fallback_version_file.close();

    exe.setTarget(target);
    exe.setBuildMode(mode);
    b.install_path = output_dir;

    var javascript = b.addExecutable("spjs", "src/main_javascript.zig");
    var typings_exe = b.addExecutable("typescript-decls", "src/javascript/jsc/typescript.zig");
    javascript.setMainPkgPath(b.pathFromRoot("."));
    typings_exe.setMainPkgPath(b.pathFromRoot("."));
    exe.setMainPkgPath(b.pathFromRoot("."));

    // exe.want_lto = true;
    if (!target.getCpuArch().isWasm()) {
        b.default_step.dependOn(&exe.step);

        const bindings_dir = std.fs.path.join(
            b.allocator,
            &.{
                cwd,
                "src",
                "javascript",
                "jsc",
                "bindings-obj",
            },
        ) catch unreachable;

        var bindings_dir_ = cwd_dir.openDir(bindings_dir, .{ .iterate = true }) catch unreachable;
        var bindings_walker = bindings_dir_.walk(b.allocator) catch unreachable;

        var bindings_files = std.ArrayList([]const u8).init(b.allocator);

        while (bindings_walker.next() catch unreachable) |entry| {
            if (std.mem.eql(u8, std.fs.path.extension(entry.basename), ".o")) {
                bindings_files.append(bindings_dir_.realpathAlloc(b.allocator, entry.path) catch unreachable) catch unreachable;
            }
        }

        // // References:
        // // - https://github.com/mceSystems/node-jsc/blob/master/deps/jscshim/webkit.gyp
        // // - https://github.com/mceSystems/node-jsc/blob/master/deps/jscshim/docs/webkit_fork_and_compilation.md#webkit-port-and-compilation
        // const flags = [_][]const u8{
        //     "-Isrc/JavaScript/jsc/WebKit/WebKitBuild/Release/JavaScriptCore/PrivateHeaders",
        //     "-Isrc/JavaScript/jsc/WebKit/WebKitBuild/Release/WTF/Headers",
        //     "-Isrc/javascript/jsc/WebKit/WebKitBuild/Release/ICU/Headers",
        //     "-DSTATICALLY_LINKED_WITH_JavaScriptCore=1",
        //     "-DSTATICALLY_LINKED_WITH_WTF=1",
        //     "-DBUILDING_WITH_CMAKE=1",
        //     "-DNOMINMAX",
        //     "-DENABLE_INSPECTOR_ALTERNATE_DISPATCHERS=0",
        //     "-DBUILDING_JSCONLY__",
        //     "-DASSERT_ENABLED=0", // missing symbol errors like this will happen "JSC::DFG::DoesGCCheck::verifyCanGC(JSC::VM&)"
        //     "-Isrc/JavaScript/jsc/WebKit/WebKitBuild/Release/", // config.h,
        //     "-Isrc/JavaScript/jsc/bindings/",
        //     "-Isrc/javascript/jsc/WebKit/Source/bmalloc",
        //     "-std=gnu++17",
        //     if (target.getOsTag() == .macos) "-DUSE_FOUNDATION=1" else "",
        //     if (target.getOsTag() == .macos) "-DUSE_CF_RETAIN_PTR=1" else "",
        // };
        const headers_step = b.step("headers", "JSC headers");
        var headers_exec: *std.build.LibExeObjStep = b.addExecutable("headers", "src/javascript/jsc/bindings/bindings-generator.zig");
        var headers_runner = headers_exec.run();
        headers_exec.setMainPkgPath(javascript.main_pkg_path.?);
        headers_step.dependOn(&headers_runner.step);
        var translate_c: *std.build.TranslateCStep = b.addTranslateC(.{ .path = b.pathFromRoot("src/javascript/jsc/bindings/headers.h") });
        translate_c.out_basename = "headers";
        translate_c.output_dir = b.pathFromRoot("src/javascript/jsc/bindings/");
        headers_step.dependOn(&translate_c.step);
        headers_zig_file = b.pathFromRoot("src/javascript/jsc/bindings/headers.zig");

        original_make_fn = headers_step.makeFn;
        headers_step.makeFn = HeadersMaker.make;
        b.default_step.dependOn(&exe.step);
        var steps = [_]*std.build.LibExeObjStep{ exe, javascript, typings_exe, headers_exec };

        // const single_threaded = b.option(bool, "single-threaded", "Build single-threaded") orelse false;

        for (steps) |step, i| {
            step.linkLibC();
            step.linkLibCpp();
            addPicoHTTP(
                step,
                true,
            );

            step.addObjectFile("src/deps/libJavaScriptCore.a");
            step.addObjectFile("src/deps/libWTF.a");
            step.addObjectFile("src/deps/libcrypto.a");
            step.addObjectFile("src/deps/libbmalloc.a");
            step.addObjectFile("src/deps/libarchive.a");
            step.addObjectFile("src/deps/libs2n.a");

            step.addObjectFile("src/deps/zlib/libz.a");

            step.addObjectFile("src/deps/mimalloc/libmimalloc.a");
            step.addLibPath("src/deps/mimalloc");
            step.addIncludeDir("src/deps/mimalloc");

            // step.single_threaded = single_threaded;

            if (target.getOsTag() == .macos) {
                const homebrew_prefix = comptime if (std.Target.current.cpu.arch == .aarch64)
                    "/opt/homebrew/"
                else
                    "/usr/local/";

                // We must link ICU statically
                step.addObjectFile(homebrew_prefix ++ "opt/icu4c/lib/libicudata.a");
                step.addObjectFile(homebrew_prefix ++ "opt/icu4c/lib/libicui18n.a");
                step.addObjectFile(homebrew_prefix ++ "opt/icu4c/lib/libicuuc.a");
                step.addObjectFile(homebrew_prefix ++ "opt/libiconv/lib/libiconv.a");
                // icucore is a weird macOS only library
                step.linkSystemLibrary("icucore");
                step.addLibPath(homebrew_prefix ++ "opt/icu4c/lib");
                step.addIncludeDir(homebrew_prefix ++ "opt/icu4c/include");
            } else {
                step.linkSystemLibrary("icuuc");
                step.linkSystemLibrary("icudata");
                step.linkSystemLibrary("icui18n");
                step.linkSystemLibrary("iconv");
            }

            for (bindings_files.items) |binding| {
                step.addObjectFile(
                    binding,
                );
            }
        }

        var obj_step = b.step("obj", "Build Bun as a .o file");
        var obj = b.addObject(bun_executable_name, exe.root_src.?.path);
        obj.setTarget(target);
        addPicoHTTP(obj, false);
        obj.addPackage(.{
            .name = "clap",
            .path = .{ .path = "src/deps/zig-clap/clap.zig" },
        });

        obj_step.dependOn(&obj.step);
        obj.setOutputDir(output_dir);
        obj.setBuildMode(mode);
        obj.linkLibC();
        obj.linkLibCpp();

        obj.bundle_compiler_rt = true;

        if (target.getOsTag() == .linux) {
            // obj.want_lto = tar;
            obj.link_emit_relocs = true;
            obj.link_function_sections = true;
        }
    } else {
        b.default_step.dependOn(&exe.step);
    }

    javascript.strip = false;
    javascript.packages = std.ArrayList(std.build.Pkg).fromOwnedSlice(b.allocator, b.allocator.dupe(std.build.Pkg, exe.packages.items) catch unreachable);

    javascript.setOutputDir(output_dir);
    javascript.setBuildMode(mode);

    const run_cmd = exe.run();
    run_cmd.step.dependOn(b.getInstallStep());
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    var log_step = b.addLog("Destination: {s}/{s}\n", .{ output_dir, bun_executable_name });
    log_step.step.dependOn(&exe.step);

    var typings_cmd: *std.build.RunStep = typings_exe.run();
    typings_cmd.cwd = cwd;
    typings_cmd.addArg(cwd);
    typings_cmd.addArg("types");
    typings_cmd.step.dependOn(&typings_exe.step);

    typings_exe.linkLibC();
    typings_exe.linkLibCpp();
    typings_exe.setMainPkgPath(cwd);

    var typings_step = b.step("types", "Build TypeScript types");
    typings_step.dependOn(&typings_cmd.step);

    var javascript_cmd = b.step("spjs", "Build standalone JavaScript runtime. Must run \"make jsc\" first.");
    javascript_cmd.dependOn(&javascript.step);
}

pub var original_make_fn: ?fn (step: *std.build.Step) anyerror!void = null;
pub var headers_zig_file: ?[]const u8 = null;
const HeadersMaker = struct {
    pub fn make(self: *std.build.Step) anyerror!void {
        try original_make_fn.?(self);
        var headers_zig: std.fs.File = try std.fs.openFileAbsolute(headers_zig_file.?, .{ .write = true });
        var contents = try headers_zig.readToEndAlloc(std.heap.page_allocator, headers_zig.getEndPos() catch unreachable);
        const last_extern_i = std.mem.lastIndexOf(u8, contents, "pub extern fn") orelse @panic("Expected contents");
        const last_newline = std.mem.indexOf(u8, contents[last_extern_i..], "\n") orelse @panic("Expected newline");
        const to_splice = "usingnamespace @import(\"./headers-replacements.zig\");\n";
        var new_contents = try std.heap.page_allocator.alloc(u8, contents.len + to_splice.len);
        std.mem.copy(u8, new_contents, to_splice);
        std.mem.copy(u8, new_contents[to_splice.len..], contents);
        var i: usize = to_splice.len;
        var remainder = new_contents[i..];
        while (remainder.len > 0) {
            i = std.mem.indexOf(u8, remainder, "\npub const struct_b") orelse break + "\npub const struct_b".len;
            var begin = remainder[i..];
            const end_line = std.mem.indexOf(u8, begin, "extern struct {") orelse break;
            const end_struct = std.mem.indexOf(u8, begin, "\n};\n") orelse break + "\n};\n".len;

            std.mem.set(u8, begin[1 .. end_struct + 3], ' ');
            remainder = begin[end_struct..];
        }
        i = to_splice.len;
        remainder = new_contents[i..];
        while (remainder.len > 0) {
            i = std.mem.indexOf(u8, remainder, "\npub const struct_") orelse break + "\npub const struct_".len;
            var begin = remainder[i..];
            var end_struct = std.mem.indexOf(u8, begin, "opaque {};") orelse break;
            end_struct += std.mem.indexOf(u8, begin[end_struct..], "\n") orelse break;
            i = 0;

            std.mem.set(u8, begin[1..end_struct], ' ');
            remainder = begin[end_struct..];
        }

        const HARDCODE = [_][]const u8{
            "[*c][*c]JSC__Exception",
            "*?*JSC__Exception     ",
            "[*c]?*c_void",
            "[*c]*c_void",
        };
        i = 0;
        while (i < HARDCODE.len) : (i += 2) {
            _ = std.mem.replace(u8, new_contents, HARDCODE[i], HARDCODE[i + 1], new_contents);
        }

        const js_value_start = std.mem.indexOf(u8, new_contents, "pub const JSC__JSValue") orelse unreachable;
        const js_value_end = std.mem.indexOf(u8, new_contents[js_value_start..], "\n") orelse unreachable;
        std.mem.set(u8, new_contents[js_value_start..][0..js_value_end], ' ');

        try headers_zig.seekTo(0);
        try headers_zig.writeAll(new_contents);
        try headers_zig.setEndPos(last_newline + last_extern_i + to_splice.len);
    }
};
