const LibCDirs = @This();
const builtin = @import("builtin");

const std = @import("../std.zig");
const Io = std.Io;
const LibCInstallation = std.zig.LibCInstallation;
const Allocator = std.mem.Allocator;

libc_include_dir_list: []const []const u8,
libc_installation: ?*const LibCInstallation,
libc_framework_dir_list: []const []const u8,
sysroot: ?[]const u8,
darwin_sdk_layout: ?DarwinSdkLayout,

/// The filesystem layout of darwin SDK elements.
pub const DarwinSdkLayout = enum {
    /// macOS SDK layout: TOP { /usr/include, /usr/lib, /System/Library/Frameworks }.
    sdk,
    /// Shipped libc layout: TOP { /lib/libc/include,  /lib/libc/darwin, <NONE> }.
    vendored,
};

pub fn detect(
    arena: Allocator,
    io: Io,
    zig_lib_dir: []const u8,
    target: *const std.Target,
    is_native_abi: bool,
    link_libc: bool,
    libc_installation: ?*const LibCInstallation,
    environ_map: *const std.process.Environ.Map,
) LibCInstallation.FindError!LibCDirs {
    if (!link_libc) {
        return .{
            .libc_include_dir_list = &[0][]u8{},
            .libc_installation = null,
            .libc_framework_dir_list = &.{},
            .sysroot = null,
            .darwin_sdk_layout = null,
        };
    }

    if (libc_installation) |lci| {
        return detectFromInstallation(arena, target, lci);
    }

    // If linking system libraries and targeting the native abi, default to
    // using the system libc installation.
    if (is_native_abi and !target.isMinGW()) {
        const libc = try arena.create(LibCInstallation);
        libc.* = LibCInstallation.findNative(arena, io, .{
            .target = target,
            .environ_map = environ_map,
        }) catch |err| switch (err) {
            error.CCompilerExitCode,
            error.CCompilerCrashed,
            error.CCompilerCannotFindHeaders,
            error.UnableToSpawnCCompiler,
            error.DarwinSdkNotFound,
            => |e| {
                // We tried to integrate with the native system C compiler,
                // however, it is not installed. So we must rely on our bundled
                // libc files.
                if (std.zig.target.canBuildLibC(target)) {
                    return detectFromBuilding(arena, zig_lib_dir, target);
                }
                return e;
            },
            else => |e| return e,
        };
        return detectFromInstallation(arena, target, libc);
    }

    // If not linking system libraries, build and provide our own libc by
    // default if possible.
    if (std.zig.target.canBuildLibC(target)) {
        return detectFromBuilding(arena, zig_lib_dir, target);
    }

    // If zig can't build the libc for the target and we are targeting the
    // native abi, fall back to using the system libc installation.
    // On windows, instead of the native (mingw) abi, we want to check
    // for the MSVC abi as a fallback.
    const use_system_abi = if (builtin.os.tag == .windows)
        target.abi == .msvc or target.abi == .itanium
    else
        is_native_abi;

    if (use_system_abi) {
        const libc = try arena.create(LibCInstallation);
        libc.* = try LibCInstallation.findNative(arena, io, .{
            .verbose = true,
            .target = target,
            .environ_map = environ_map,
        });
        return detectFromInstallation(arena, target, libc);
    }

    return .{
        .libc_include_dir_list = &.{},
        .libc_installation = null,
        .libc_framework_dir_list = &.{},
        .sysroot = null,
        .darwin_sdk_layout = null,
    };
}

fn detectFromInstallation(arena: Allocator, target: *const std.Target, lci: *const LibCInstallation) !LibCDirs {
    var list = try std.array_list.Managed([]const u8).initCapacity(arena, 5);
    var framework_list = std.array_list.Managed([]const u8).init(arena);

    list.appendAssumeCapacity(lci.include_dir.?);

    const is_redundant = std.mem.eql(u8, lci.sys_include_dir.?, lci.include_dir.?);
    if (!is_redundant) list.appendAssumeCapacity(lci.sys_include_dir.?);

    if (target.os.tag == .windows) {
        if (std.fs.path.dirname(lci.sys_include_dir.?)) |sys_include_dir_parent| {
            // This include path will only exist when the optional "Desktop development with C++"
            // is installed. It contains headers, .rc files, and resources. It is especially
            // necessary when working with Windows resources.
            const atlmfc_dir = try std.fs.path.join(arena, &[_][]const u8{ sys_include_dir_parent, "atlmfc", "include" });
            list.appendAssumeCapacity(atlmfc_dir);
        }
        if (std.fs.path.dirname(lci.include_dir.?)) |include_dir_parent| {
            const um_dir = try std.fs.path.join(arena, &[_][]const u8{ include_dir_parent, "um" });
            list.appendAssumeCapacity(um_dir);

            const shared_dir = try std.fs.path.join(arena, &[_][]const u8{ include_dir_parent, "shared" });
            list.appendAssumeCapacity(shared_dir);
        }
    }
    if (target.os.tag == .haiku) {
        const include_dir_path = lci.include_dir.?;
        const os_dir = try std.fs.path.join(arena, &[_][]const u8{ include_dir_path, "os" });
        list.appendAssumeCapacity(os_dir);
        // Errors.h
        const os_support_dir = try std.fs.path.join(arena, &[_][]const u8{ include_dir_path, "os/support" });
        list.appendAssumeCapacity(os_support_dir);

        const config_dir = try std.fs.path.join(arena, &[_][]const u8{ include_dir_path, "config" });
        list.appendAssumeCapacity(config_dir);
    }

    var sysroot: ?[]const u8 = null;

    if (target.os.tag.isDarwin()) d: {
        const down1 = std.fs.path.dirname(lci.sys_include_dir.?) orelse break :d;
        const down2 = std.fs.path.dirname(down1) orelse break :d;
        try framework_list.append(try std.fs.path.join(arena, &.{ down2, "System", "Library", "Frameworks" }));
        sysroot = down2;
    }

    return .{
        .libc_include_dir_list = list.items,
        .libc_installation = lci,
        .libc_framework_dir_list = framework_list.items,
        .sysroot = sysroot,
        .darwin_sdk_layout = if (sysroot == null) null else .sdk,
    };
}

pub fn detectFromBuilding(
    arena: Allocator,
    zig_lib_dir: []const u8,
    target: *const std.Target,
) !LibCDirs {
    const s = std.fs.path.sep_str;

    if (target.os.tag.isDarwin()) {
        const list = try arena.alloc([]const u8, 1);
        list[0] = try std.fmt.allocPrint(
            arena,
            "{s}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "any-darwin-any",
            .{zig_lib_dir},
        );
        return .{
            .libc_include_dir_list = list,
            .libc_installation = null,
            .libc_framework_dir_list = &.{},
            .sysroot = null,
            .darwin_sdk_layout = .vendored,
        };
    }

    const generic_name = libCGenericName(target);
    // Some architecture families are handled by the same set of headers.
    const arch_name = if (target.isMuslLibC() or target.isWasiLibC())
        std.zig.target.muslArchNameHeaders(target.cpu.arch)
    else if (target.isGnuLibC())
        std.zig.target.glibcArchNameHeaders(target.cpu.arch)
    else if (target.isFreeBSDLibC())
        std.zig.target.freebsdArchNameHeaders(target.cpu.arch)
    else if (target.isNetBSDLibC())
        std.zig.target.netbsdArchNameHeaders(target.cpu.arch)
    else if (target.isOpenBSDLibC())
        std.zig.target.openbsdArchNameHeaders(target.cpu.arch)
    else
        @tagName(target.cpu.arch);
    const os_name = @tagName(target.os.tag);
    const abi_name = if (target.isMuslLibC())
        std.zig.target.muslAbiNameHeaders(target.abi)
    else if (target.isGnuLibC())
        std.zig.target.glibcAbiNameHeaders(target.abi)
    else if (target.isNetBSDLibC())
        std.zig.target.netbsdAbiNameHeaders(target.abi)
    else
        @tagName(target.abi);
    const arch_include_dir = try std.fmt.allocPrint(
        arena,
        "{s}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "{s}-{s}-{s}",
        .{ zig_lib_dir, arch_name, os_name, abi_name },
    );
    const generic_include_dir = try std.fmt.allocPrint(
        arena,
        "{s}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "generic-{s}",
        .{ zig_lib_dir, generic_name },
    );
    const generic_arch_name = std.zig.target.osArchName(target);
    const arch_os_include_dir = try std.fmt.allocPrint(
        arena,
        "{s}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "{s}-{s}-any",
        .{ zig_lib_dir, generic_arch_name, os_name },
    );
    const generic_os_include_dir = try std.fmt.allocPrint(
        arena,
        "{s}" ++ s ++ "libc" ++ s ++ "include" ++ s ++ "any-{s}-any",
        .{ zig_lib_dir, os_name },
    );

    const list = try arena.alloc([]const u8, 4);
    list[0] = arch_include_dir;
    list[1] = generic_include_dir;
    list[2] = arch_os_include_dir;
    list[3] = generic_os_include_dir;

    return .{
        .libc_include_dir_list = list,
        .libc_installation = null,
        .libc_framework_dir_list = &.{},
        .sysroot = null,
        .darwin_sdk_layout = .vendored,
    };
}

fn libCGenericName(target: *const std.Target) [:0]const u8 {
    switch (target.os.tag) {
        .windows => return "mingw",
        .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => return "darwin",
        .freebsd => return "freebsd",
        .netbsd => return "netbsd",
        .openbsd => return "openbsd",
        else => {},
    }
    switch (target.abi) {
        .gnu,
        .gnuabin32,
        .gnuabi64,
        .gnueabi,
        .gnueabihf,
        .gnuf32,
        .gnusf,
        .gnux32,
        => return "glibc",
        .musl,
        .muslabin32,
        .muslabi64,
        .musleabi,
        .musleabihf,
        .muslf32,
        .muslsf,
        .muslx32,
        .none,
        .ohos,
        .ohoseabi,
        => return "musl",
        .eabi,
        .eabihf,
        .ilp32,
        .android,
        .androideabi,
        .msvc,
        .itanium,
        .simulator,
        => unreachable,
    }
}
