const std = @import("std");
const LazyPath = std.Build.LazyPath;

pub const SourceItem = struct { name: []u8, api_export: bool };

pub fn nvim_gen_sources(
    b: *std.Build,
    nlua0: *std.Build.Step.Compile,
    nvim_sources: *std.ArrayList(SourceItem),
    nvim_headers: *std.ArrayList([]u8),
    api_headers: *std.ArrayList(LazyPath),
    versiondef_git: LazyPath,
    version_lua: LazyPath,
) !struct { *std.Build.Step.WriteFile, LazyPath } {
    const gen_headers = b.addWriteFiles();

    for (nvim_sources.items) |s| {
        const api_export = if (s.api_export) api_headers else null;
        const input_file = b.path(b.fmt("src/nvim/{s}", .{s.name}));
        _ = try generate_header_for(b, s.name, input_file, api_export, nlua0, gen_headers, false);
    }

    for (nvim_headers.items) |s| {
        const input_file = b.path(b.fmt("src/nvim/{s}", .{s}));
        _ = try generate_header_for(b, s, input_file, null, nlua0, gen_headers, true);
    }

    {
        const gen_step = b.addRunArtifact(nlua0);
        gen_step.addFileArg(b.path("src/gen/gen_ex_cmds.lua"));
        _ = gen_header(b, gen_step, "ex_cmds_enum.generated.h", gen_headers);
        _ = gen_header(b, gen_step, "ex_cmds_defs.generated.h", gen_headers);
        gen_step.addFileArg(b.path("src/nvim/ex_cmds.lua"));
    }

    {
        const gen_step = b.addRunArtifact(nlua0);
        gen_step.addFileArg(b.path("src/gen/gen_options.lua"));
        _ = gen_header(b, gen_step, "options.generated.h", gen_headers);
        _ = gen_header(b, gen_step, "options_enum.generated.h", gen_headers);
        _ = gen_header(b, gen_step, "options_map.generated.h", gen_headers);
        _ = gen_header(b, gen_step, "option_vars.generated.h", gen_headers);
        gen_step.addFileArg(b.path("src/nvim/options.lua"));

        const test_gen_step = b.step("wipopt", "debug one nlua0 (options)");
        test_gen_step.dependOn(&gen_step.step);
    }

    {
        const gen_step = b.addRunArtifact(nlua0);
        gen_step.addFileArg(b.path("src/gen/gen_events.lua"));
        _ = gen_header(b, gen_step, "auevents_enum.generated.h", gen_headers);
        _ = gen_header(b, gen_step, "auevents_name_map.generated.h", gen_headers);
        gen_step.addFileArg(b.path("src/nvim/auevents.lua"));
    }

    {
        const gen_step = b.addRunArtifact(nlua0);
        gen_step.addFileArg(b.path("src/gen/gen_keycodes.lua"));
        _ = gen_header(b, gen_step, "keycode_names.generated.h", gen_headers);
        gen_step.addFileArg(b.path("src/nvim/keycodes.lua"));
    }

    {
        const gen_step = b.addRunArtifact(nlua0);
        gen_step.addFileArg(b.path("src/gen/gen_char_blob.lua"));
        // TODO(bfredl): LUAC_PRG is missing. tricky with cross-compiling..
        // gen_step.addArg("-c");
        _ = gen_header(b, gen_step, "lua/vim_module.generated.h", gen_headers);
        // NB: vim._init_packages and vim.inspect must be be first and second ones
        // respectively, otherwise --luamod-dev won't work properly.
        const names = [_][]const u8{
            "_init_packages",
            "inspect",
            "filetype",
            "fs",
            "F",
            "keymap",
            "loader",
            "text",
        };
        for (names) |n| {
            gen_step.addFileArg(b.path(b.fmt("runtime/lua/vim/{s}.lua", .{n})));
            gen_step.addArg(b.fmt("vim.{s}", .{n}));
        }

        // Dynamically add all Lua _core/ modules (like CMakeLists.txt does)
        if (b.build_root.handle.openDir("runtime/lua/vim/_core", .{ .iterate = true })) |core_dir_handle| {
            var core_dir = core_dir_handle;
            defer core_dir.close();

            var iter = core_dir.iterate();
            var core_files = try std.ArrayList([]const u8).initCapacity(b.allocator, 0);
            defer core_files.deinit(b.allocator);

            while (try iter.next()) |entry| {
                if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".lua")) {
                    const module_name = try b.allocator.dupe(u8, entry.name[0 .. entry.name.len - 4]);
                    try core_files.append(b.allocator, module_name);
                }
            }

            std.mem.sort([]const u8, core_files.items, {}, struct {
                fn lessThan(_: void, a: []const u8, c: []const u8) bool {
                    return std.mem.lessThan(u8, a, c);
                }
            }.lessThan);

            for (core_files.items) |n| {
                gen_step.addFileArg(b.path(b.fmt("runtime/lua/vim/_core/{s}.lua", .{n})));
                gen_step.addArg(b.fmt("vim._core.{s}", .{n}));
            }
        } else |err| {
            std.debug.print("Warning: Could not open _core directory: {}\n", .{err});
        }
    }

    const ui_metadata = ui_step: {
        const gen_step = b.addRunArtifact(nlua0);
        gen_step.addFileArg(b.path("src/gen/gen_api_ui_events.lua"));
        gen_step.addFileArg(b.path("src/nvim/api/ui_events.in.h"));
        _ = try gen_header_with_header(b, gen_step, "ui_events_call.generated.h", nlua0, gen_headers);
        _ = try gen_header_with_header(b, gen_step, "ui_events_remote.generated.h", nlua0, gen_headers);
        const ui_metadata = gen_step.addOutputFileArg("ui_metadata.mpack");
        _ = try gen_header_with_header(b, gen_step, "ui_events_client.generated.h", nlua0, gen_headers);
        break :ui_step ui_metadata;
    };

    const eval_funcs_metadata, const exported_funcs_metadata = dispatch_step: {
        const gen_step = b.addRunArtifact(nlua0);
        gen_step.addFileArg(b.path("src/gen/gen_api_dispatch.lua"));
        _ = try gen_header_with_header(b, gen_step, "api/private/dispatch_wrappers.generated.h", nlua0, gen_headers);
        const exported_funcs_metadata = gen_step.addOutputFileArg("exported_funcs_metadata.mpack");
        const eval_funcs_metadata = gen_step.addOutputFileArg("eval_funcs_metadata.mpack");
        _ = gen_header(b, gen_step, "lua_api_c_bindings.generated.h", gen_headers);
        _ = gen_header(b, gen_step, "keysets_defs.generated.h", gen_headers);
        gen_step.addFileArg(b.path("src/nvim/api/dispatch_deprecated.lua"));
        // now follows all .h files with exported functions
        for (api_headers.items) |h| {
            gen_step.addFileArg(h);
        }

        break :dispatch_step .{ eval_funcs_metadata, exported_funcs_metadata };
    };

    {
        const gen_step = b.addRunArtifact(nlua0);
        gen_step.addFileArg(b.path("src/gen/gen_api_metadata.lua"));
        gen_step.addFileArg(exported_funcs_metadata);
        gen_step.addFileArg(ui_metadata);
        gen_step.addFileArg(b.path("src/nvim/api/ui.h"));
        gen_step.addFileArg(versiondef_git);
        gen_step.addFileArg(version_lua);
        gen_step.addFileArg(b.path("src/gen/dump_bin_array.lua"));
        _ = gen_header(b, gen_step, "api/private/api_metadata.generated.h", gen_headers);
    }

    const funcs_data = eval_step: {
        const gen_step = b.addRunArtifact(nlua0);
        gen_step.addFileArg(b.path("src/gen/gen_eval.lua"));
        _ = gen_header(b, gen_step, "funcs.generated.h", gen_headers);
        gen_step.addFileArg(eval_funcs_metadata);
        const funcs_data = gen_step.addOutputFileArg("funcs_data.mpack");
        gen_step.addFileArg(b.path("src/nvim/eval.lua"));
        break :eval_step funcs_data;
    };

    return .{ gen_headers, funcs_data };
}

fn gen_header(
    b: *std.Build,
    gen_step: *std.Build.Step.Run,
    name: []const u8,
    gen_headers: *std.Build.Step.WriteFile,
) std.Build.LazyPath {
    _ = b;
    const header = gen_step.addOutputFileArg(name);
    _ = gen_headers.addCopyFile(header, name);
    return header;
}

fn gen_header_with_header(
    b: *std.Build,
    gen_step: *std.Build.Step.Run,
    name: []const u8,
    nlua0: *std.Build.Step.Compile,
    gen_headers: *std.Build.Step.WriteFile,
) !std.Build.LazyPath {
    if (name.len < 12 or !std.mem.eql(u8, ".generated.h", name[name.len - 12 ..])) return error.InvalidBaseName;
    const h = gen_header(b, gen_step, name, gen_headers);
    _ = try generate_header_for(b, b.fmt("{s}.h", .{name[0 .. name.len - 12]}), h, null, nlua0, gen_headers, false);
    return h;
}

pub const PreprocessorOptions = struct {
    include_dirs: []const LazyPath = &.{},
    c_macros: []const []const u8 = &.{},
    target: ?std.Build.ResolvedTarget = null,
};

fn generate_header_for(
    b: *std.Build,
    name: []const u8,
    input_file: LazyPath,
    api_export: ?*std.ArrayList(LazyPath),
    nlua0: *std.Build.Step.Compile,
    gen_headers: *std.Build.Step.WriteFile,
    nvim_header: bool,
) !*std.Build.Step.Run {
    if (name.len < 2 or !(std.mem.eql(u8, ".c", name[name.len - 2 ..]) or std.mem.eql(u8, ".h", name[name.len - 2 ..]))) return error.InvalidBaseName;
    const basename = name[0 .. name.len - 2];
    const run_step = b.addRunArtifact(nlua0);
    run_step.addFileArg(b.path("src/gen/gen_declarations.lua"));
    run_step.addFileArg(input_file);
    const gen_name = b.fmt("{s}.{s}.generated.h", .{ basename, if (nvim_header) "h.inline" else "c" });
    _ = gen_header(b, run_step, gen_name, gen_headers);
    if (nvim_header) {
        run_step.addArg("SKIP");
    } else {
        const h_file = gen_header(b, run_step, b.fmt("{s}.h.generated.h", .{basename}), gen_headers);
        if (api_export) |api_files| {
            try api_files.append(b.allocator, h_file);
        }
    }

    run_step.addArg(gen_name);
    return run_step;
}
