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

const LibBuildOpts = struct {
    target: std.Build.ResolvedTarget,
    optimize: std.builtin.OptimizeMode,
    baremetal: bool,
    bit32: bool,
};

fn addEngineSources(b: *std.Build, opts: LibBuildOpts) *std.Build.Module {
    const strict = opts.optimize == .Debug;

    const dataMod = b.createModule(.{
        .target = opts.target,
        .optimize = opts.optimize,
        .strip = if (opts.optimize == .Debug) false else true,
        .unwind_tables = if (opts.optimize == .Debug) null else .none,
        .pic = true,
        .sanitize_c = if (strict) .full else null,
    });

    dataMod.addCSourceFiles(.{
        .files = &[_][]const u8{
            "src/neonucleus.c",
            "src/ncomplib.c",
        },
        .flags = &.{
            if (opts.baremetal) "-DNN_BAREMETAL" else "",
            if (opts.bit32) "-DNN_BIT32" else "",
            if (strict) "-Wall" else "",
            if (strict) "-Werror" else "",
            "-std=gnu23",
            "-Wno-keyword-macro", // cuz bools
            "-fPIE",
        },
    });

    if (!opts.baremetal) {
        dataMod.link_libc = true; // we need a libc
    }

    dataMod.addIncludePath(b.path("src"));

    return dataMod;
}

const LuaVersion = enum {
    lua52,
    lua53,
    lua54,
};

fn compileRaylib(b: *std.Build, os: std.Target.Os.Tag, buildOpts: LibBuildOpts, c: *std.Build.Step.Compile) void {
    // TODO: find out how to send our target to this build cmd
    const targetStr = buildOpts.target.result.zigTriple(b.allocator) catch unreachable;
    defer b.allocator.free(targetStr);

    const targetArg = std.fmt.allocPrint(b.allocator, "-Dtarget={s}", .{targetStr}) catch unreachable;
    defer b.allocator.free(targetArg);

    // passing it breaks it for some reason?
    // TODO: make it not break
    const raylib = b.addSystemCommand(&.{ "zig", "build" });
    raylib.setCwd(b.path("foreign/raylib/"));
    raylib.stdio = .inherit;

    c.step.dependOn(&raylib.step);
    c.addIncludePath(b.path("foreign/raylib/zig-out/include/"));
    c.addLibraryPath(b.path("foreign/raylib/zig-out/lib/"));
    c.linkSystemLibrary("raylib");
    if (os == .windows) {
        c.linkSystemLibrary("WinMM");
        c.linkSystemLibrary("GDI32"); // <---
        c.linkSystemLibrary("User32"); //    ^ Windows can't just rely on GDI
        c.linkSystemLibrary("Shell32");
        c.linkSystemLibrary("OpenGL32");
    }
}

// For the test architecture, we specify the target Lua version we so desire.
// This can be checked for with Lua's _VERSION

fn compileTheRightLua(b: *std.Build, target: std.Build.ResolvedTarget, version: LuaVersion) !*std.Build.Step.Compile {
    const alloc = b.allocator;
    const dirName = @tagName(version);

    // its a static library because COFF is a pile of shit
    const c = b.addLibrary(.{
        .name = "lua",
        .root_module = b.addModule("luamod", .{ .link_libc = true, .optimize = .ReleaseFast, .target = target, .pic = true }),
        .linkage = .static,
    });

    const rootPath = try std.mem.join(alloc, std.fs.path.sep_str, &.{ "foreign", dirName });

    // get all the .c files
    var files = try std.ArrayList([]const u8).initCapacity(b.allocator, 0);
    errdefer files.deinit(b.allocator);

    var dir = try std.fs.cwd().openDir(rootPath, std.fs.Dir.OpenOptions{ .iterate = true });
    defer dir.close();

    var iter = dir.iterate();

    while (try iter.next()) |e| {
        if (std.mem.startsWith(u8, e.name, "l") and std.mem.endsWith(u8, e.name, ".c") and !std.mem.eql(u8, e.name, "lua.c")) {
            const name = try alloc.dupe(u8, e.name);
            try files.append(b.allocator, name);
        }
    }

    c.addCSourceFiles(.{
        .root = b.path(rootPath),
        .files = files.items,
    });

    return c;
}

fn includeTheRightLua(b: *std.Build, c: *std.Build.Step.Compile, version: LuaVersion) !void {
    const alloc = b.allocator;
    const dirName = @tagName(version);

    const rootPath = try std.mem.join(alloc, std.fs.path.sep_str, &.{ "foreign", dirName });

    c.addIncludePath(b.path(rootPath));
}

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

    const os = target.result.os.tag;

    const optimize = b.standardOptimizeOption(.{});

    const opts = LibBuildOpts{
        .target = target,
        .optimize = optimize,
        .baremetal = b.option(bool, "baremetal", "Compiles without libc integration") orelse false,
        .bit32 = target.result.ptrBitWidth() == 32,
    };

    const noEmu = b.option(bool, "noEmu", "Disable compiling the emulator (fixes some build system quirks)") orelse false;

    const includeFiles = b.addInstallHeaderFile(b.path("src/neonucleus.h"), "neonucleus.h");

    const engineMod = addEngineSources(b, opts);

    const engineStatic = b.addLibrary(.{
        .name = "neonucleus",
        .root_module = engineMod,
        .linkage = .static,
    });

    const engineShared = b.addLibrary(.{
        .name = if (os == .windows) "neonucleusdll" else "neonucleus",
        .root_module = engineMod,
        .linkage = .dynamic,
    });

    const engineStep = b.step("engine", "Builds the engine as a static library");
    engineStep.dependOn(&engineStatic.step);
    engineStep.dependOn(&includeFiles.step);
    engineStep.dependOn(&b.addInstallArtifact(engineStatic, .{}).step);

    const sharedStep = b.step("shared", "Builds the engine as a shared library");
    sharedStep.dependOn(&engineShared.step);
    sharedStep.dependOn(&includeFiles.step);
    sharedStep.dependOn(&b.addInstallArtifact(engineShared, .{}).step);

    if (!noEmu) {
        const emulator = b.addExecutable(.{
            .name = "neonucleus",
            .root_module = b.addModule("emulator", .{
                .target = target,
                .optimize = optimize,
            }),
        });
        emulator.linkLibC();

        const sysraylib_flag = b.option(bool, "sysraylib", "Use the system raylib instead of compiling raylib") orelse false;
        if (sysraylib_flag) {
            emulator.linkSystemLibrary("raylib");
        } else {
            compileRaylib(b, os, opts, emulator);
        }

        const luaVer = b.option(LuaVersion, "lua", "The version of Lua to use.") orelse LuaVersion.lua53;
        emulator.addCSourceFiles(.{
            .files = &.{
                "src/luaarch.c",
                "src/main.c",
                "src/glyphcache.c",
            },
            .flags = &.{
                if (opts.baremetal) "-DNN_BAREMETAL" else "",
                if (opts.bit32) "-DNN_BIT32" else "",
            },
        });
        const l = try compileTheRightLua(b, target, luaVer);
        try includeTheRightLua(b, emulator, luaVer);

        // forces us to link in everything too
        emulator.linkLibrary(l);
        emulator.linkLibrary(engineStatic);

        const emulatorStep = b.step("emulator", "Builds the emulator");
        emulatorStep.dependOn(&emulator.step);
        emulatorStep.dependOn(&b.addInstallArtifact(emulator, .{}).step);

        var run_cmd = b.addRunArtifact(emulator);

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

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