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

pub const emsdk_ver_major = "4";
pub const emsdk_ver_minor = "0";
pub const emsdk_ver_tiny = "3";
pub const emsdk_version = emsdk_ver_major ++ "." ++ emsdk_ver_minor ++ "." ++ emsdk_ver_tiny;

pub fn build(b: *std.Build) void {
    _ = b.addModule("root", .{ .root_source_file = b.path("src/zemscripten.zig") });
}

pub fn emccPath(b: *std.Build) []const u8 {
    return std.fs.path.join(b.allocator, &.{
        b.dependency("emsdk", .{}).path("").getPath(b),
        "upstream",
        "emscripten",
        switch (builtin.target.os.tag) {
            .windows => "emcc.bat",
            else => "emcc",
        },
    }) catch unreachable;
}

pub fn emrunPath(b: *std.Build) []const u8 {
    return std.fs.path.join(b.allocator, &.{
        b.dependency("emsdk", .{}).path("").getPath(b),
        "upstream",
        "emscripten",
        switch (builtin.target.os.tag) {
            .windows => "emrun.bat",
            else => "emrun",
        },
    }) catch unreachable;
}

pub fn htmlPath(b: *std.Build) []const u8 {
    return std.fs.path.join(b.allocator, &.{
        b.dependency("emsdk", .{}).path("").getPath(b),
        "upstream",
        "emscripten",
        "src",
        "shell.html",
    }) catch unreachable;
}

pub fn activateEmsdkStep(b: *std.Build) *std.Build.Step {
    const emsdk_script_path = std.fs.path.join(b.allocator, &.{
        b.dependency("emsdk", .{}).path("").getPath(b),
        switch (builtin.target.os.tag) {
            .windows => "emsdk.bat",
            else => "emsdk",
        },
    }) catch unreachable;

    var emsdk_install = b.addSystemCommand(&.{ emsdk_script_path, "install", emsdk_version });

    switch (builtin.target.os.tag) {
        .linux, .macos => {
            emsdk_install.step.dependOn(&b.addSystemCommand(&.{ "chmod", "+x", emsdk_script_path }).step);
        },
        .windows => {
            emsdk_install.step.dependOn(&b.addSystemCommand(&.{ "takeown", "/f", emsdk_script_path }).step);
        },
        else => {},
    }

    var emsdk_activate = b.addSystemCommand(&.{ emsdk_script_path, "activate", emsdk_version });
    emsdk_activate.step.dependOn(&emsdk_install.step);

    const step = b.allocator.create(std.Build.Step) catch unreachable;
    step.* = std.Build.Step.init(.{
        .id = .custom,
        .name = "Activate EMSDK",
        .owner = b,
        .makeFn = &struct {
            fn make(_: *std.Build.Step, _: std.Build.Step.MakeOptions) anyerror!void {}
        }.make,
    });

    switch (builtin.target.os.tag) {
        .linux, .macos => {
            const chmod_emcc = b.addSystemCommand(&.{ "chmod", "+x", emccPath(b) });
            chmod_emcc.step.dependOn(&emsdk_activate.step);
            step.dependOn(&chmod_emcc.step);

            const chmod_emrun = b.addSystemCommand(&.{ "chmod", "+x", emrunPath(b) });
            chmod_emrun.step.dependOn(&emsdk_activate.step);
            step.dependOn(&chmod_emrun.step);
        },
        .windows => {
            const takeown_emcc = b.addSystemCommand(&.{ "takeown", "/f", emccPath(b) });
            takeown_emcc.step.dependOn(&emsdk_activate.step);
            step.dependOn(&takeown_emcc.step);

            const takeown_emrun = b.addSystemCommand(&.{ "takeown", "/f", emrunPath(b) });
            takeown_emrun.step.dependOn(&emsdk_activate.step);
            step.dependOn(&takeown_emrun.step);
        },
        else => {},
    }

    return step;
}

pub const EmccFlags = std.StringHashMap(void);

pub const EmccDefaultFlagsOverrides = struct {
    optimize: std.builtin.OptimizeMode,
    fsanitize: bool,
};

pub fn emccDefaultFlags(allocator: std.mem.Allocator, options: EmccDefaultFlagsOverrides) EmccFlags {
    var args = EmccFlags.init(allocator);
    switch (options.optimize) {
        .Debug => {
            args.put("-O0", {}) catch unreachable;
            args.put("-gsource-map", {}) catch unreachable;
            if (options.fsanitize)
                args.put("-fsanitize=undefined", {}) catch unreachable;
        },
        .ReleaseSafe => {
            args.put("-O3", {}) catch unreachable;
            if (options.fsanitize) {
                args.put("-fsanitize=undefined", {}) catch unreachable;
                args.put("-fsanitize-minimal-runtime", {}) catch unreachable;
            }
        },
        .ReleaseFast => {
            args.put("-O3", {}) catch unreachable;
        },
        .ReleaseSmall => {
            args.put("-Oz", {}) catch unreachable;
        },
    }
    return args;
}

pub const EmccSettings = std.StringHashMap([]const u8);

pub const EmsdkAllocator = enum {
    none,
    dlmalloc,
    emmalloc,
    @"emmalloc-debug",
    @"emmalloc-memvalidate",
    @"emmalloc-verbose",
    mimalloc,
};

pub const EmccDefaultSettingsOverrides = struct {
    optimize: std.builtin.OptimizeMode,
    emsdk_allocator: EmsdkAllocator = .emmalloc,
    shell_file: ?[]const u8 = null,
};

pub fn emccDefaultSettings(allocator: std.mem.Allocator, options: EmccDefaultSettingsOverrides) EmccSettings {
    var settings = EmccSettings.init(allocator);
    switch (options.optimize) {
        .Debug, .ReleaseSafe => {
            settings.put("SAFE_HEAP", "1") catch unreachable;
            settings.put("STACK_OVERFLOW_CHECK", "1") catch unreachable;
            settings.put("ASSERTIONS", "1") catch unreachable;
        },
        else => {},
    }
    settings.put("USE_OFFSET_CONVERTER", "1") catch unreachable;
    settings.put("MALLOC", @tagName(options.emsdk_allocator)) catch unreachable;
    return settings;
}

pub const EmccFilePath = struct {
    src_path: []const u8,
    virtual_path: ?[]const u8 = null,
};

pub const StepOptions = struct {
    optimize: std.builtin.OptimizeMode,
    flags: EmccFlags,
    settings: EmccSettings,
    use_preload_plugins: bool = false,
    embed_paths: ?[]const EmccFilePath = null,
    preload_paths: ?[]const EmccFilePath = null,
    shell_file_path: ?std.Build.LazyPath = null,
    install_dir: std.Build.InstallDir,
};

pub fn emccStep(
    b: *std.Build,
    wasm: *std.Build.Step.Compile,
    options: StepOptions,
) *std.Build.Step {
    var emcc = b.addSystemCommand(&.{emccPath(b)});

    var iterFlags = options.flags.iterator();
    while (iterFlags.next()) |kvp| {
        emcc.addArg(kvp.key_ptr.*);
    }

    var iterSettings = options.settings.iterator();
    while (iterSettings.next()) |kvp| {
        emcc.addArg(std.fmt.allocPrint(
            b.allocator,
            "-s{s}={s}",
            .{ kvp.key_ptr.*, kvp.value_ptr.* },
        ) catch unreachable);
    }

    emcc.addArtifactArg(wasm);
    {
        for (wasm.root_module.getGraph().modules) |module| {
            for (module.link_objects.items) |link_object| {
                switch (link_object) {
                    .other_step => |compile_step| {
                        switch (compile_step.kind) {
                            .lib => {
                                emcc.addArtifactArg(compile_step);
                            },
                            else => {},
                        }
                    },
                    else => {},
                }
            }
        }
    }

    emcc.addArg("-o");
    const out_file = emcc.addOutputFileArg(b.fmt("{s}.html", .{wasm.name}));

    if (options.use_preload_plugins) {
        emcc.addArg("--use-preload-plugins");
    }

    if (options.embed_paths) |embed_paths| {
        for (embed_paths) |path| {
            const path_arg = if (path.virtual_path) |virtual_path|
                std.fmt.allocPrint(
                    b.allocator,
                    "{s}@{s}",
                    .{ path.src_path, virtual_path },
                ) catch unreachable
            else
                path.src_path;
            emcc.addArgs(&.{ "--embed-file", path_arg });
        }
    }

    if (options.preload_paths) |preload_paths| {
        for (preload_paths) |path| {
            const path_arg = if (path.virtual_path) |virtual_path|
                std.fmt.allocPrint(
                    b.allocator,
                    "{s}@{s}",
                    .{ path.src_path, virtual_path },
                ) catch unreachable
            else
                path.src_path;
            emcc.addArgs(&.{ "--preload-file", path_arg });
        }
    }

    if (options.shell_file_path) |shell_file_path| {
        emcc.addArg("--shell-file");
        emcc.addFileArg(shell_file_path);
        emcc.addFileInput(shell_file_path);
    }

    const install_step = b.addInstallDirectory(.{
        .source_dir = out_file.dirname(),
        .install_dir = options.install_dir,
        .install_subdir = "",
    });
    install_step.step.dependOn(&emcc.step);

    return &install_step.step;
}

pub fn emrunStep(
    b: *std.Build,
    html_path: []const u8,
    extra_args: []const []const u8,
) *std.Build.Step {
    var emrun = b.addSystemCommand(&.{emrunPath(b)});
    emrun.addArgs(extra_args);
    emrun.addArg(html_path);
    // emrun.addArg("--");

    return &emrun.step;
}
