const std = @import("std");

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

    // Executable
    {
        const exe = try addExecutable(b, .{ .target = target, .optimize = optimize });
        b.installArtifact(exe);

        const run_cmd = b.addRunArtifact(exe);
        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);
    }

    // Tests
    {
        const unit_tests = b.addTest(.{
            .name = "unit-tests",
            .root_source_file = b.path("src/main.zig"),
            .target = target,
            .optimize = optimize,
        });
        try attachModules(unit_tests);

        if (b.option(bool, "install-tests", "Install the unit tests in the `bin` folder") orelse false) {
            b.installArtifact(unit_tests);
        }

        const run_unit_tests = b.addRunArtifact(unit_tests);
        const test_step = b.step("test", "Run unit tests");
        test_step.dependOn(&run_unit_tests.step);
    }

    // Release
    {
        const target_triples = [_][]const u8{
            "x86_64-linux-musl",
            "aarch64-linux-musl",
            "x86_64-macos",
            "aarch64-macos",
            "x86_64-windows",
            "aarch64-windows",
        };
        const release_step = b.step("release", "Produce executables for targeted platforms");

        for (&target_triples) |triple| {
            const release_target = b.resolveTargetQuery(try std.Target.Query.parse(.{
                .arch_os_abi = triple,
                .cpu_features = "baseline",
            }));

            const exe = try addExecutable(b, .{ .target = release_target, .optimize = optimize });
            const install = b.addInstallArtifact(exe, .{
                .dest_dir = .{ .override = .{
                    .custom = b.pathJoin(&.{ triple, "bin" }),
                } },
            });

            release_step.dependOn(&install.step);
        }
    }
}

fn addExecutable(b: *std.Build, options: struct {
    target: std.Build.ResolvedTarget,
    optimize: std.builtin.OptimizeMode,
}) !*std.Build.Step.Compile {
    const exe = b.addExecutable(.{
        .name = "glsl_analyzer",
        .root_source_file = b.path("src/main.zig"),
        .target = options.target,
        .optimize = options.optimize,
    });
    try attachModules(exe);
    return exe;
}

fn attachModules(step: *std.Build.Step.Compile) !void {
    const b = step.step.owner;

    step.linkLibC();

    const compressed_spec = try CompressStep.create(b, "spec.json.zlib", b.path("spec/spec.json"));
    step.root_module.addAnonymousImport("glsl_spec.json.zlib", .{ .root_source_file = compressed_spec.getOutput() });

    const options = b.addOptions();
    const build_root_path = try std.fs.path.resolve(
        b.allocator,
        &.{b.build_root.path orelse "."},
    );
    options.addOption([]const u8, "build_root", build_root_path);
    options.addOption([]const u8, "version", b.run(&.{ "git", "describe", "--tags", "--always" }));
    step.root_module.addOptions("build_options", options);
}

const CompressStep = struct {
    step: std.Build.Step,
    generated_file: std.Build.GeneratedFile,
    input: std.Build.LazyPath,

    pub fn create(b: *std.Build, name: []const u8, path: std.Build.LazyPath) !*@This() {
        const self = try b.allocator.create(@This());
        self.* = .{
            .step = std.Build.Step.init(.{
                .id = .custom,
                .name = name,
                .owner = b,
                .makeFn = &make,
            }),
            .generated_file = .{ .step = &self.step },
            .input = path,
        };
        path.addStepDependencies(&self.step);
        return self;
    }

    pub fn getOutput(self: *@This()) std.Build.LazyPath {
        return .{ .generated = .{ .file = &self.generated_file } };
    }

    fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) anyerror!void {
        const b = step.owner;
        const self: *@This() = @fieldParentPtr("step", step);
        const input_path = self.input.getPath(b);

        var man = b.graph.cache.obtain();
        defer man.deinit();

        man.hash.add(@as(u32, 0x00000002));
        const input_index = try man.addFile(input_path, 16 << 20);

        const is_hit = try step.cacheHit(&man);

        const digest = man.final();

        const output_path = try b.cache_root.join(b.allocator, &.{ "o", &digest, step.name });
        self.generated_file.path = output_path;

        if (is_hit) return;

        const input_contents = man.files.keys()[input_index].contents.?;

        if (std.fs.path.dirname(output_path)) |dir| try b.cache_root.handle.makePath(dir);
        var output_file = b.cache_root.handle.createFile(output_path, .{}) catch |err| {
            std.log.err("could not open {s}: {s}", .{ output_path, @errorName(err) });
            return err;
        };
        defer output_file.close();

        var output_buffered = std.io.bufferedWriter(output_file.writer());
        {
            var compress_stream = try std.compress.zlib.compressor(output_buffered.writer(), .{});
            try compress_stream.writer().writeAll(input_contents);
            try compress_stream.finish();
        }
        try output_buffered.flush();

        try step.writeManifest(&man);
    }
};