const GhosttyLibVt = @This();

const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const RunStep = std.Build.Step.Run;
const CombineArchivesStep = @import("CombineArchivesStep.zig");
const LibsystemOverrideStep = @import("LibsystemOverrideStep.zig");
const Config = @import("Config.zig");
const GhosttyZig = @import("GhosttyZig.zig");
const LipoStep = @import("LipoStep.zig");
const SharedDeps = @import("SharedDeps.zig");
const XCFrameworkStep = @import("XCFrameworkStep.zig");

/// The step that generates the file.
step: *std.Build.Step,

/// The install step for the library output.
artifact: *std.Build.Step,

/// The kind of library
kind: Kind,

/// The final library file
output: std.Build.LazyPath,
dsym: ?std.Build.LazyPath,
pkg_config: ?std.Build.LazyPath,
pkg_config_static: ?std.Build.LazyPath,

/// The kind of library being built. This is similar to LinkMode but
/// also includes wasm which is an executable, not a library.
const Kind = enum {
    wasm,
    shared,
    static,
};

pub fn initWasm(
    b: *std.Build,
    zig: *const GhosttyZig,
) !GhosttyLibVt {
    const target = zig.vt.resolved_target.?;
    assert(target.result.cpu.arch.isWasm());

    const exe = b.addExecutable(.{
        .name = "ghostty-vt",
        .root_module = zig.vt_c,
        .version = zig.version,
    });

    // Allow exported symbols to actually be exported.
    exe.rdynamic = true;

    // Export the indirect function table so that embedders (e.g. JS in
    // a browser) can insert callback entries for terminal effects.
    exe.export_table = true;

    // There is no entrypoint for this wasm module.
    exe.entry = .disabled;

    // Default Zig stack size in Zig 0.16 is 1MB. Lower to 128 KB since
    // this has to be preallocated up front in Wasm linear memory. Peak
    // stack under various artificial loads is 17 KB at the time of this
    // comment but lets do 128KB to be safe. We can lower later.
    exe.stack_size = 128 * 1024;

    // Zig's WASM linker doesn't support --growable-table, so the table
    // is emitted with max == min and can't be grown from JS. Run a
    // small Zig build tool that patches the binary's table section to
    // remove the max limit.
    const patch_run = patch: {
        const patcher = b.addExecutable(.{
            .name = "wasm_patch_growable_table",
            .root_module = b.createModule(.{
                .root_source_file = b.path("src/build/wasm_patch_growable_table.zig"),
                .target = b.graph.host,
            }),
        });
        break :patch b.addRunArtifact(patcher);
    };
    patch_run.addFileArg(exe.getEmittedBin());
    const output = patch_run.addOutputFileArg("ghostty-vt.wasm");
    const artifact_install = b.addInstallFileWithDir(
        output,
        .bin,
        "ghostty-vt.wasm",
    );

    return .{
        .step = &patch_run.step,
        .artifact = &artifact_install.step,
        .kind = .wasm,
        .output = output,
        .dsym = null,
        .pkg_config = null,
        .pkg_config_static = null,
    };
}

pub fn initStatic(
    b: *std.Build,
    zig: *const GhosttyZig,
) !GhosttyLibVt {
    return initLib(b, zig, .static);
}

pub fn initShared(
    b: *std.Build,
    zig: *const GhosttyZig,
) !GhosttyLibVt {
    const target = zig.vt.resolved_target.?;

    // Prefer Apple's linker for final Darwin dylibs when its toolchain is
    // available. Besides producing the platform load commands expected by
    // Apple distribution, this lets the static archive's libSystem symbol
    // override take effect. Cross-compiled macOS dylibs continue through Zig.
    if (@import("apple_sdk").nativeLink.available(target)) {
        return initLibApple(b, zig);
    }

    return initLib(b, zig, .dynamic);
}

/// Apple platform targets for xcframework slices.
pub const ApplePlatform = enum {
    macos_universal,
    ios,
    ios_simulator,
    // tvOS, watchOS, and visionOS are not yet supported by Zig's
    // standard library (missing PATH_MAX, mcontext fields, etc.).

    /// Platforms that have device + simulator pairs, gated on SDK detection.
    const sdk_platforms = [_]struct {
        os_tag: std.Target.Os.Tag,
        device: ApplePlatform,
        simulator: ApplePlatform,
    }{
        .{ .os_tag = .ios, .device = .ios, .simulator = .ios_simulator },
    };
};

/// Static libraries for each Apple platform, keyed by `ApplePlatform`.
pub const AppleLibs = std.EnumMap(ApplePlatform, GhosttyLibVt);

/// Build static libraries for all available Apple platforms.
/// Always builds a macOS universal (arm64 + x86_64) fat binary.
/// Additional platforms are included if their SDK is detected.
pub fn initStaticAppleUniversal(
    b: *std.Build,
    cfg: *const Config,
    deps: *const SharedDeps,
    zig: *const GhosttyZig,
) !AppleLibs {
    var result: AppleLibs = .{};

    // macOS universal (arm64 + x86_64)
    const aarch64_zig = try zig.retarget(
        b,
        cfg,
        deps,
        Config.genericMacOSTarget(b, .aarch64),
    );
    const x86_64_zig = try zig.retarget(
        b,
        cfg,
        deps,
        Config.genericMacOSTarget(b, .x86_64),
    );
    const aarch64 = try initStatic(b, &aarch64_zig);
    const x86_64 = try initStatic(b, &x86_64_zig);
    const universal = LipoStep.create(b, .{
        .name = "ghostty-vt",
        .out_name = "libghostty-vt.a",
        .input_a = aarch64.output,
        .input_b = x86_64.output,
    });
    result.put(.macos_universal, .{
        .step = universal.step,
        .artifact = universal.step,
        .kind = .static,
        .output = universal.output,
        .dsym = null,
        .pkg_config = null,
        .pkg_config_static = null,
    });

    // Additional Apple platforms, each gated on SDK availability.
    for (ApplePlatform.sdk_platforms) |p| {
        const target_query: std.Target.Query = .{
            .cpu_arch = .aarch64,
            .os_tag = p.os_tag,
            .os_version_min = Config.osVersionMinLibVt(p.os_tag),
        };
        if (detectAppleSDK(
            b.graph.io,
            b.allocator,
            &b.graph.environ_map,
            b.resolveTargetQuery(target_query).result,
        )) {
            const dev_zig = try zig.retarget(b, cfg, deps, b.resolveTargetQuery(target_query));
            result.put(p.device, try initStatic(b, &dev_zig));

            const sim_zig = try zig.retarget(b, cfg, deps, b.resolveTargetQuery(.{
                .cpu_arch = .aarch64,
                .os_tag = p.os_tag,
                .os_version_min = Config.osVersionMinLibVt(p.os_tag),
                .abi = .simulator,
                .cpu_model = .{ .explicit = &std.Target.aarch64.cpu.apple_a17 },
            }));
            result.put(p.simulator, try initStatic(b, &sim_zig));
        }
    }

    return result;
}

fn initLib(
    b: *std.Build,
    zig: *const GhosttyZig,
    linkage: std.builtin.LinkMode,
) !GhosttyLibVt {
    const kind: Kind = switch (linkage) {
        .static => .static,
        .dynamic => .shared,
    };
    const target = zig.vt.resolved_target.?;
    const lib = b.addLibrary(.{
        .name = if (kind == .static) "ghostty-vt-static" else "ghostty-vt",
        .linkage = linkage,
        .root_module = zig.vt_c,
        .version = zig.version,
    });
    lib.installHeadersDirectory(
        b.path("include/ghostty"),
        "ghostty",
        .{ .include_extensions = &.{".h"} },
    );

    if (kind == .static) {
        // These must be bundled since we're compiling into a static lib.
        // Otherwise, you get undefined symbol errors. This could cause
        // problems if you're linking multiple static Zig libraries but
        // we'll cross that bridge when we get to it.
        lib.bundle_compiler_rt = true;
        lib.bundle_ubsan_rt = true;

        // Enable PIC so the static library can be linked into PIE
        // executables, which is the default on most Linux distributions.
        lib.root_module.pic = true;
    }

    if (target.result.os.tag == .windows) {
        // Zig's ubsan emits /exclude-symbols linker directives that
        // are incompatible with the MSVC linker (LNK4229).
        lib.bundle_ubsan_rt = false;

        if (kind == .static) {
            if (target.result.abi == .msvc) {
                // Zig's compiler runtime doesn't provide MSVC's security
                // cookie symbols when libc is linked. Disable stack-protector
                // generation so static consumers don't need BufferOverflowU.
                lib.root_module.stack_protector = false;
            }

            // The Zig standard library uses NT and kernel32 symbols.
            lib.root_module.linkSystemLibrary("ntdll", .{});
            lib.root_module.linkSystemLibrary("kernel32", .{});
        }
    }

    if (lib.rootModuleTarget().abi.isAndroid()) {
        // Support 16kb page sizes, required for Android 15+.
        lib.link_z_max_page_size = 16384; // 16kb

        try @import("android_ndk").addPaths(b, lib);
    }

    if (lib.rootModuleTarget().os.tag.isDarwin()) {
        // Self-hosted x86_64 doesn't work for darwin. It may not work
        // for other platforms too but definitely darwin.
        lib.use_llvm = true;

        // This is required for codesign and dynamic linking to work.
        lib.headerpad_max_install_names = true;

        // If we're not cross compiling then we try to find the Apple
        // SDK using standard Apple tooling.
        if (builtin.os.tag.isDarwin()) try @import("apple_sdk").addPaths(b, lib);
    }

    // Get our debug symbols (only for shared libs; static libs aren't linked)
    const dsymutil: ?std.Build.LazyPath = dsymutil: {
        if (kind != .shared) break :dsymutil null;
        if (!target.result.os.tag.isDarwin()) break :dsymutil null;

        const dsymutil = RunStep.create(b, "dsymutil");
        dsymutil.addArgs(&.{"dsymutil"});
        dsymutil.addFileArg(lib.getEmittedBin());
        dsymutil.addArgs(&.{"-o"});
        const output = dsymutil.addOutputFileArg("libghostty-vt.dSYM");
        break :dsymutil output;
    };

    // pkg-config
    //
    // pkg-config's --static only expands Libs.private / Requires.private;
    // it doesn't change -lghostty-vt into an archive-only reference when
    // both shared and static libraries are installed. Install a dedicated
    // static module so consumers can request the archive explicitly.
    const pcs: ?PkgConfigFiles = if (kind == .shared)
        pkgConfigFiles(b, zig, target.result.os.tag)
    else
        null;

    // For static libraries with vendored SIMD dependencies, combine
    // all archives into a single fat archive so consumers only need
    // to link one file.
    if (kind == .static and
        zig.simd_libs.items.len > 0)
    {
        var sources: SharedDeps.LazyPathList = .empty;
        try sources.append(b.allocator, lib.getEmittedBin());
        try sources.appendSlice(b.allocator, zig.simd_libs.items);

        const combined = CombineArchivesStep.create(b, target, "ghostty-vt", sources.items);
        combined.step.dependOn(&lib.step);

        // On Darwin, prefer libSystem's libc/libm over the bundled
        // compiler-rt for consumers of this archive. See GhosttyLib
        // and libsystem_override.sh for details.
        const override = LibsystemOverrideStep.create(
            b,
            target,
            combined.output,
            "libghostty-vt-fat.a",
        );

        return .{
            .step = override.step orelse combined.step,
            .artifact = &b.addInstallArtifact(lib, .{}).step,
            .kind = kind,
            .output = override.output,
            .dsym = dsymutil,
            .pkg_config = if (pcs) |v| v.shared else null,
            .pkg_config_static = if (pcs) |v| v.static else null,
        };
    }

    // Same libSystem preference for the plain (no vendored SIMD)
    // static archive; a no-op off Darwin.
    const override: LibsystemOverrideStep.Result = if (kind == .static) LibsystemOverrideStep.create(
        b,
        target,
        lib.getEmittedBin(),
        "libghostty-vt-static.a",
    ) else .{
        .step = null,
        .output = lib.getEmittedBin(),
    };

    return .{
        .step = override.step orelse &lib.step,
        .artifact = &b.addInstallArtifact(lib, .{}).step,
        .kind = kind,
        .output = override.output,
        .dsym = dsymutil,
        .pkg_config = if (pcs) |v| v.shared else null,
        .pkg_config_static = if (pcs) |v| v.static else null,
    };
}

/// Builds a shared Darwin library with Apple's linker.
fn initLibApple(
    b: *std.Build,
    zig: *const GhosttyZig,
) !GhosttyLibVt {
    // Sorry, this function has a lot of comments because there are
    // a lot of flags that aren't obvious.

    const target = zig.vt.resolved_target.?;
    assert(target.result.os.tag.isDarwin());

    // Build the combined archive through the normal static path first.
    const static = try initStatic(b, zig);

    // Zig's module-level export metadata does not carry into this separate
    // native link, so explicitly expose only libghostty-vt's public C ABI.
    const exports = b.addWriteFiles().add(
        "libghostty-vt.exports",
        "_ghostty_*\n",
    );

    // Match Zig's versioned dylib naming so existing consumers and packaging
    // continue to receive the same real name and compatibility symlinks.
    const real_name = b.fmt("libghostty-vt.{d}.{d}.{d}.dylib", .{
        zig.version.major,
        zig.version.minor,
        zig.version.patch,
    });
    const soname = b.fmt("libghostty-vt.{d}.dylib", .{zig.version.major});

    const native_link = try @import("apple_sdk").nativeLink.addCommand(
        b,
        "link libghostty-vt with Apple ld",
        target,
    );

    native_link.addArgs(&.{
        // Emit a Mach-O dynamic library instead of an executable.
        "-dynamiclib",
        // Pull every object from the combined static archive.
        "-Wl,-all_load",
        // Remove unreachable private code pulled in by all_load.
        "-Wl,-dead_strip",
        // Leave room for packaging tools to rewrite install names.
        "-Wl,-headerpad_max_install_names",
    });
    // Restrict exports to libghostty-vt's public C ABI.
    native_link.addPrefixedFileArg("-Wl,-exported_symbols_list,", exports);
    // Link the archive produced by the normal static-library path.
    native_link.addFileArg(static.output);
    native_link.addArgs(&.{
        // Give framework consumers a stable runtime-relative identity.
        "-install_name",
        "@rpath/libghostty-vt.dylib",
        // Record the exact package version for runtime inspection.
        "-current_version",
        b.fmt("{d}.{d}.{d}", .{
            zig.version.major,
            zig.version.minor,
            zig.version.patch,
        }),
        // Preserve compatibility with the initial public ABI.
        "-compatibility_version",
        "1.0.0",
        // Write the fully versioned dylib used by the install symlinks.
        "-o",
    });
    const output = native_link.addOutputFileArg(real_name);

    // Recreate addInstallArtifact's dylib layout: install the fully versioned
    // file, then point the major-version and unversioned names at it.
    const artifact_install = b.addInstallFileWithDir(
        output,
        .lib,
        real_name,
    );
    const soname_install = b.addSystemCommand(&.{
        "/bin/ln",
        "-sf",
        real_name,
        b.getInstallPath(.lib, soname),
    });
    soname_install.step.dependOn(&artifact_install.step);
    const unversioned_install = b.addSystemCommand(&.{
        "/bin/ln",
        "-sf",
        soname,
        b.getInstallPath(.lib, "libghostty-vt.dylib"),
    });
    unversioned_install.step.dependOn(&soname_install.step);

    // The native link is a Run step rather than a Compile step, so install the
    // public headers explicitly instead of relying on addInstallArtifact.
    const headers_install = b.addInstallDirectory(.{
        .source_dir = b.path("include/ghostty"),
        .install_dir = .header,
        .install_subdir = "ghostty",
        .include_extensions = &.{".h"},
    });
    unversioned_install.step.dependOn(&headers_install.step);

    // Preserve the debug-symbol output exposed by the normal shared-library
    // path for framework and release packaging.
    const dsymutil = RunStep.create(b, "dsymutil");
    dsymutil.addArgs(&.{"dsymutil"});
    dsymutil.addFileArg(output);
    dsymutil.addArgs(&.{"-o"});
    const dsym = dsymutil.addOutputFileArg("libghostty-vt.dSYM");

    const pcs = pkgConfigFiles(b, zig, target.result.os.tag);
    return .{
        .step = &native_link.step,
        .artifact = &unversioned_install.step,
        .kind = .shared,
        .output = output,
        .dsym = dsym,
        .pkg_config = pcs.shared,
        .pkg_config_static = pcs.static,
    };
}

/// Returns the Libs.private value for the pkg-config file.
/// Vendored C++ dependencies are built in no-libcxx mode so consumers
/// don't need libc++.  System-provided simdutf still requires it.
fn libsPrivate(
    zig: *const GhosttyZig,
) []const u8 {
    return if (zig.vt_c.link_libcpp orelse false) "-lc++" else "";
}

const PkgConfigFiles = struct {
    shared: std.Build.LazyPath,
    static: std.Build.LazyPath,
};

fn pkgConfigFiles(
    b: *std.Build,
    zig: *const GhosttyZig,
    os_tag: std.Target.Os.Tag,
) PkgConfigFiles {
    const wf = b.addWriteFiles();
    const libs_private = libsPrivate(zig);
    const requires_private = requiresPrivate(b);

    return .{
        .shared = wf.add("libghostty-vt.pc", b.fmt(
            \\prefix={s}
            \\includedir=${{prefix}}/include
            \\libdir=${{prefix}}/lib
            \\
            \\Name: libghostty-vt
            \\URL: https://github.com/ghostty-org/ghostty
            \\Description: Ghostty VT library
            \\Version: {f}
            \\Cflags: -I${{includedir}}
            \\Libs: -L${{libdir}} -lghostty-vt
            \\Libs.private: {s}
            \\Requires.private: {s}
        , .{ b.install_prefix, zig.version, libs_private, requires_private })),
        .static = wf.add("libghostty-vt-static.pc", b.fmt(
            \\prefix={s}
            \\includedir=${{prefix}}/include
            \\libdir=${{prefix}}/lib
            \\
            \\Name: libghostty-vt-static
            \\URL: https://github.com/ghostty-org/ghostty
            \\Description: Ghostty VT library (static)
            \\Version: {f}
            \\Cflags: -I${{includedir}}
            \\Libs: ${{libdir}}/{s}
            \\Libs.private: {s}
            \\Requires.private: {s}
        , .{
            b.install_prefix,
            zig.version,
            staticLibraryName(os_tag),
            libs_private,
            requires_private,
        })),
    };
}

fn staticLibraryName(os_tag: std.Target.Os.Tag) []const u8 {
    return if (os_tag == .windows)
        "ghostty-vt-static.lib"
    else
        "libghostty-vt.a";
}

/// Returns the Requires.private value for the pkg-config file.
/// When SIMD dependencies are provided by the system (via
/// -Dsystem-integration), we reference their pkg-config names so
/// that downstream consumers pick them up transitively.
fn requiresPrivate(b: *std.Build) []const u8 {
    const system_simdutf = b.systemIntegrationOption("simdutf", .{});
    const system_highway = b.systemIntegrationOption("highway", .{ .default = false });

    if (system_simdutf and system_highway) return "simdutf, libhwy";
    if (system_simdutf) return "simdutf";
    if (system_highway) return "libhwy";
    return "";
}

/// Create an XCFramework bundle from Apple platform static libraries.
pub fn xcframework(
    apple_libs: *const AppleLibs,
    b: *std.Build,
) *XCFrameworkStep {
    // Generate a headers directory with a module map for Swift PM.
    // We can't use include/ directly because it contains a module map
    // for GhosttyKit (the macOS app library).
    const wf = b.addWriteFiles();
    _ = wf.addCopyDirectory(
        b.path("include/ghostty"),
        "ghostty",
        .{ .include_extensions = &.{".h"} },
    );
    _ = wf.add("module.modulemap",
        \\module GhosttyVt {
        \\    umbrella header "ghostty/vt.h"
        \\    export *
        \\}
        \\
    );
    const headers = wf.getDirectory();

    var libraries: [AppleLibs.len]XCFrameworkStep.Library = undefined;
    var lib_count: usize = 0;
    for (std.enums.values(ApplePlatform)) |platform| {
        if (apple_libs.get(platform)) |lib| {
            libraries[lib_count] = .{
                .library = lib.output,
                .headers = headers,
                .dsym = null,
            };
            lib_count += 1;
        }
    }

    return XCFrameworkStep.create(b, .{
        .name = "ghostty-vt",
        .out_path = b.pathJoin(&.{ b.install_prefix, "lib/ghostty-vt.xcframework" }),
        .libraries = libraries[0..lib_count],
    });
}

/// Returns true if the Apple SDK for the given target is installed.
fn detectAppleSDK(
    io: std.Io,
    alloc: std.mem.Allocator,
    environ_map: *const std.process.Environ.Map,
    target: std.Target,
) bool {
    _ = std.zig.LibCInstallation.findNative(
        alloc,
        io,
        .{
            .environ_map = environ_map,
            .target = &target,
            .verbose = false,
        },
    ) catch return false;
    return true;
}

pub fn install(
    self: *const GhosttyLibVt,
    step: *std.Build.Step,
) void {
    const b = step.owner;
    step.dependOn(self.artifact);
    if (self.pkg_config) |pkg_config| {
        step.dependOn(&b.addInstallFileWithDir(
            pkg_config,
            .prefix,
            "share/pkgconfig/libghostty-vt.pc",
        ).step);
    }
    if (self.pkg_config_static) |pkg_config_static| {
        step.dependOn(&b.addInstallFileWithDir(
            pkg_config_static,
            .prefix,
            "share/pkgconfig/libghostty-vt-static.pc",
        ).step);
    }
}
