const GhosttyDist = @This();

const std = @import("std");
const Config = @import("Config.zig");
const SharedDeps = @import("SharedDeps.zig");
const GhosttyFrameData = @import("GhosttyFrameData.zig");

/// The final source tarball.
archive: std.Build.LazyPath,

/// The step to install the tarball.
install_step: *std.Build.Step,

/// The step to depend on
archive_step: *std.Build.Step,

/// The step to depend on for checking the dist
check_step: *std.Build.Step,

pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist {
    // The name prefix used for all paths in the archive.
    const name = if (cfg.emit_lib_vt) "libghostty-vt" else "ghostty";

    // Get the resources we're going to inject into the source tarball.
    // lib-vt doesn't need GTK resources or frame data.
    const alloc = b.allocator;
    var resources: std.ArrayListUnmanaged(Resource) = .empty;
    if (!cfg.emit_lib_vt) {
        {
            const gtk = SharedDeps.gtkNgDistResources(b);
            try resources.append(alloc, gtk.resources_c);
            try resources.append(alloc, gtk.resources_h);
        }
        {
            const framedata = GhosttyFrameData.distResources(b);
            try resources.append(alloc, framedata.framedata);
        }
    }

    // git archive to create the final tarball. "git archive" is the
    // easiest way I can find to create a tarball that ignores stuff
    // from gitignore and also supports adding files as well as removing
    // dist-only files (the "export-ignore" git attribute).
    const git_archive = b.addSystemCommand(&.{
        "git",
        "archive",
        "--format=tgz",
    });

    // embed the Ghostty version in the tarball
    {
        const version = b.addWriteFiles().add("VERSION", b.fmt("{f}", .{cfg.version}));
        // --add-file uses the most recent --prefix to determine the path
        // in the archive to copy the file (the directory only).
        git_archive.addArg(b.fmt("--prefix={s}-{f}/", .{
            name, cfg.version,
        }));
        git_archive.addPrefixedFileArg("--add-file=", version);
    }

    // Add all of our resources into the tarball.
    for (resources.items) |resource| {
        // Our dist path basename may not match our generated file basename,
        // and git archive requires this. To be safe, we copy the file once
        // to ensure the basename matches and then use that as the final
        // generated file.
        const copied = b.addWriteFiles().addCopyFile(
            resource.generated,
            std.fs.path.basename(resource.dist),
        );

        // --add-file uses the most recent --prefix to determine the path
        // in the archive to copy the file (the directory only).
        git_archive.addArg(b.fmt("--prefix={s}-{f}/{s}/", .{
            name,                                 cfg.version,
            std.fs.path.dirname(resource.dist).?,
        }));
        git_archive.addPrefixedFileArg("--add-file=", copied);
    }

    // Add our output
    git_archive.addArgs(&.{
        // This is important. Standard source tarballs extract into
        // a directory named `project-version`. This is expected by
        // standard tooling such as debhelper and rpmbuild.
        b.fmt("--prefix={s}-{f}/", .{ name, cfg.version }),
        "-o",
    });
    const output = git_archive.addOutputFileArg(b.fmt(
        "{s}-{f}.tar.gz",
        .{ name, cfg.version },
    ));
    git_archive.addArg("HEAD");

    // When building for lib-vt only, exclude large directories that
    // are not needed to build libghostty-vt. This significantly reduces
    // the size of the resulting archive.
    if (cfg.emit_lib_vt) {
        for (lib_vt_excludes) |exclude| {
            git_archive.addArg(b.fmt(":(exclude){s}", .{exclude}));
        }
    }

    // The install step to put the dist into the build directory.
    const install = b.addInstallFile(
        output,
        b.fmt("dist/{s}-{f}.tar.gz", .{ name, cfg.version }),
    );

    // The check step to ensure the archive works.
    const check = b.addSystemCommand(&.{ "tar", "xvzf" });
    check.addFileArg(output);
    check.addArg("-C");

    // This is the root Ghostty source dir of the extracted source tarball.
    // i.e. this is way `build.zig` is.
    const extract_dir = check
        .addOutputDirectoryArg(name)
        .path(b, b.fmt("{s}-{f}", .{ name, cfg.version }));

    // Check that tests pass within the extracted directory. This isn't
    // a fully hermetic test because we're sharing the Zig cache. In
    // the future we could add an option to use a totally new cache but
    // in the interest of speed we don't do that for now and hope other
    // CI catches any issues.
    const check_test = step: {
        // For lib-vt, we run the lib-vt tests instead of the full test suite.
        const check_cmd = if (cfg.emit_lib_vt)
            &[_][]const u8{ "zig", "build", "test-lib-vt", "-Demit-lib-vt=true" }
        else
            &[_][]const u8{ "zig", "build", "test" };
        const step = b.addSystemCommand(check_cmd);
        step.setCwd(extract_dir);

        // Must be set so that Zig knows that this command doesn't
        // have side effects and is being run for its exit code check.
        // Zig will cache depending on its extract dir.
        step.expectExitCode(0);

        // Capture stderr so it doesn't spew into the parent build.
        // On the flip side, if the test fails we won't know why so
        // that sucks but we should have already ran tests at this point.
        // NOTE(mitchellh): temporarily disabled to diagnose heisenbug
        //_ = step.captureStdErr();

        break :step step;
    };

    // Check that all our dist resources are at the proper path.
    for (resources.items) |resource| {
        const path = extract_dir.path(b, resource.dist);
        const check_path = b.addCheckFile(path, .{});
        check_test.step.dependOn(&check_path.step);
    }

    // For lib-vt, also verify the CMake build works from the tarball.
    if (cfg.emit_lib_vt) {
        const cmake_build_dir = extract_dir.path(b, "cmake-build");
        const cmake_configure = b.addSystemCommand(&.{ "cmake", "-B" });
        cmake_configure.addDirectoryArg(cmake_build_dir);
        cmake_configure.setCwd(extract_dir);
        cmake_configure.expectExitCode(0);
        cmake_configure.step.dependOn(&check.step);

        const cmake_build = b.addSystemCommand(&.{ "cmake", "--build" });
        cmake_build.addDirectoryArg(cmake_build_dir);
        cmake_build.expectExitCode(0);
        cmake_build.step.dependOn(&cmake_configure.step);

        check_test.step.dependOn(&cmake_build.step);
    }

    return .{
        .archive = output,
        .install_step = &install.step,
        .archive_step = &git_archive.step,
        .check_step = &check_test.step,
    };
}

/// Paths to exclude from the dist archive when building for lib-vt only.
/// These are large files and directories that are not needed to build or
/// test libghostty-vt, specified as git pathspec exclude patterns.
const lib_vt_excludes = &[_][]const u8{
    // App and platform resources
    "images",
    "macos",
    "dist/doxygen",
    "dist/linux",
    "dist/macos",
    "dist/windows",
    "flatpak",
    "snap",
    "po",
    "example",

    // Test corpus (lib-vt tests use embedded testdata within src/terminal/)
    "test",

    // Large binary assets
    "src/font/res",
    "src/crash/testdata",
    "pkg/wuffs/src/too_big.jpg",
    "pkg/wuffs/src/too_big.png",
    "pkg/breakpad/vendor",

    // Vendored libraries not used by lib-vt
    "vendor",
};

/// A dist resource is a resource that is built and distributed as part
/// of the source tarball with Ghostty. These aren't committed to the Git
/// repository but are built as part of the `zig build dist` command.
/// The purpose is to limit the number of build-time dependencies required
/// for downstream users and packagers.
pub const Resource = struct {
    /// The relative path in the source tree where the resource will be
    /// if it was pre-built. These are not checksummed or anything because the
    /// assumption is that the source tarball itself is checksummed and signed.
    dist: []const u8,

    /// The path to the generated resource in the build system. By depending
    /// on this you'll force it to regenerate. This does NOT point to the
    /// "path" above.
    generated: std.Build.LazyPath,

    /// Returns the path to use for this resource.
    pub fn path(self: *const Resource, b: *std.Build) std.Build.LazyPath {
        // If the dist path exists at build compile time then we use it.
        if (self.exists(b)) {
            return b.path(self.dist);
        }

        // Otherwise we use the generated path.
        return self.generated;
    }

    /// Returns true if the dist path exists at build time.
    pub fn exists(self: *const Resource, b: *std.Build) bool {
        if (b.build_root.handle.access(self.dist, .{})) {
            // If we have a ".git" directory then we're a git checkout
            // and we never want to use the dist path. This shouldn't happen
            // so show a warning to the user.
            if (b.build_root.handle.access(".git", .{})) {
                std.log.warn(
                    "dist resource '{s}' should not be in a git checkout",
                    .{self.dist},
                );
                return false;
            } else |_| {}

            return true;
        } else |_| {
            return false;
        }
    }
};
