const std = @import("std");
const builtin = @import("builtin");
const event = std.event;
const Target = @import("target.zig").Target;
const c = @import("c.zig");

/// See the render function implementation for documentation of the fields.
pub const LibCInstallation = struct {
    include_dir: []const u8,
    lib_dir: ?[]const u8,
    static_lib_dir: ?[]const u8,
    msvc_lib_dir: ?[]const u8,
    kernel32_lib_dir: ?[]const u8,
    dynamic_linker_path: ?[]const u8,

    pub const FindError = error{
        OutOfMemory,
        FileSystem,
        UnableToSpawnCCompiler,
        CCompilerExitCode,
        CCompilerCrashed,
        CCompilerCannotFindHeaders,
        LibCRuntimeNotFound,
        LibCStdLibHeaderNotFound,
        LibCKernel32LibNotFound,
        UnsupportedArchitecture,
    };

    pub fn parse(
        self: *LibCInstallation,
        allocator: *std.mem.Allocator,
        libc_file: []const u8,
        stderr: *std.io.OutStream(std.os.File.WriteError),
    ) !void {
        self.initEmpty();

        const keys = []const []const u8{
            "include_dir",
            "lib_dir",
            "static_lib_dir",
            "msvc_lib_dir",
            "kernel32_lib_dir",
            "dynamic_linker_path",
        };
        const FoundKey = struct {
            found: bool,
            allocated: ?[]u8,
        };
        var found_keys = [1]FoundKey{FoundKey{ .found = false, .allocated = null }} ** keys.len;
        errdefer {
            self.initEmpty();
            for (found_keys) |found_key| {
                if (found_key.allocated) |s| allocator.free(s);
            }
        }

        const contents = try std.io.readFileAlloc(allocator, libc_file);
        defer allocator.free(contents);

        var it = std.mem.tokenize(contents, "\n");
        while (it.next()) |line| {
            if (line.len == 0 or line[0] == '#') continue;
            var line_it = std.mem.separate(line, "=");
            const name = line_it.next() orelse {
                try stderr.print("missing equal sign after field name\n");
                return error.ParseError;
            };
            const value = line_it.rest();
            inline for (keys) |key, i| {
                if (std.mem.eql(u8, name, key)) {
                    found_keys[i].found = true;
                    switch (@typeInfo(@typeOf(@field(self, key)))) {
                        builtin.TypeId.Optional => {
                            if (value.len == 0) {
                                @field(self, key) = null;
                            } else {
                                found_keys[i].allocated = try std.mem.dupe(allocator, u8, value);
                                @field(self, key) = found_keys[i].allocated;
                            }
                        },
                        else => {
                            if (value.len == 0) {
                                try stderr.print("field cannot be empty: {}\n", key);
                                return error.ParseError;
                            }
                            const dupe = try std.mem.dupe(allocator, u8, value);
                            found_keys[i].allocated = dupe;
                            @field(self, key) = dupe;
                        },
                    }
                    break;
                }
            }
        }
        for (found_keys) |found_key, i| {
            if (!found_key.found) {
                try stderr.print("missing field: {}\n", keys[i]);
                return error.ParseError;
            }
        }
    }

    pub fn render(self: *const LibCInstallation, out: *std.io.OutStream(std.os.File.WriteError)) !void {
        @setEvalBranchQuota(4000);
        try out.print(
            \\# The directory that contains `stdlib.h`.
            \\# On Linux, can be found with: `cc -E -Wp,-v -xc /dev/null`
            \\include_dir={}
            \\
            \\# The directory that contains `crt1.o`.
            \\# On Linux, can be found with `cc -print-file-name=crt1.o`.
            \\# Not needed when targeting MacOS.
            \\lib_dir={}
            \\
            \\# The directory that contains `crtbegin.o`.
            \\# On Linux, can be found with `cc -print-file-name=crtbegin.o`.
            \\# Not needed when targeting MacOS or Windows.
            \\static_lib_dir={}
            \\
            \\# The directory that contains `vcruntime.lib`.
            \\# Only needed when targeting Windows.
            \\msvc_lib_dir={}
            \\
            \\# The directory that contains `kernel32.lib`.
            \\# Only needed when targeting Windows.
            \\kernel32_lib_dir={}
            \\
            \\# The full path to the dynamic linker, on the target system.
            \\# Only needed when targeting Linux.
            \\dynamic_linker_path={}
            \\
        ,
            self.include_dir,
            self.lib_dir orelse "",
            self.static_lib_dir orelse "",
            self.msvc_lib_dir orelse "",
            self.kernel32_lib_dir orelse "",
            self.dynamic_linker_path orelse Target(Target.Native).getDynamicLinkerPath(),
        );
    }

    /// Finds the default, native libc.
    pub async fn findNative(self: *LibCInstallation, loop: *event.Loop) !void {
        self.initEmpty();
        var group = event.Group(FindError!void).init(loop);
        errdefer group.deinit();
        var windows_sdk: ?*c.ZigWindowsSDK = null;
        errdefer if (windows_sdk) |sdk| c.zig_free_windows_sdk(@ptrCast(?[*]c.ZigWindowsSDK, sdk));

        switch (builtin.os) {
            builtin.Os.windows => {
                var sdk: *c.ZigWindowsSDK = undefined;
                switch (c.zig_find_windows_sdk(@ptrCast(?[*]?[*]c.ZigWindowsSDK, &sdk))) {
                    c.ZigFindWindowsSdkError.None => {
                        windows_sdk = sdk;

                        if (sdk.msvc_lib_dir_ptr != 0) {
                            self.msvc_lib_dir = try std.mem.dupe(loop.allocator, u8, sdk.msvc_lib_dir_ptr[0..sdk.msvc_lib_dir_len]);
                        }
                        try group.call(findNativeKernel32LibDir, self, loop, sdk);
                        try group.call(findNativeIncludeDirWindows, self, loop, sdk);
                        try group.call(findNativeLibDirWindows, self, loop, sdk);
                    },
                    c.ZigFindWindowsSdkError.OutOfMemory => return error.OutOfMemory,
                    c.ZigFindWindowsSdkError.NotFound => return error.NotFound,
                    c.ZigFindWindowsSdkError.PathTooLong => return error.NotFound,
                }
            },
            builtin.Os.linux => {
                try group.call(findNativeIncludeDirLinux, self, loop);
                try group.call(findNativeLibDirLinux, self, loop);
                try group.call(findNativeStaticLibDir, self, loop);
                try group.call(findNativeDynamicLinker, self, loop);
            },
            builtin.Os.macosx, builtin.Os.freebsd, builtin.Os.netbsd => {
                self.include_dir = try std.mem.dupe(loop.allocator, u8, "/usr/include");
            },
            else => @compileError("unimplemented: find libc for this OS"),
        }
        return await (async group.wait() catch unreachable);
    }

    async fn findNativeIncludeDirLinux(self: *LibCInstallation, loop: *event.Loop) !void {
        const cc_exe = std.os.getEnvPosix("CC") orelse "cc";
        const argv = []const []const u8{
            cc_exe,
            "-E",
            "-Wp,-v",
            "-xc",
            "/dev/null",
        };
        // TODO make this use event loop
        const errorable_result = std.os.ChildProcess.exec(loop.allocator, argv, null, null, 1024 * 1024);
        const exec_result = if (std.debug.runtime_safety) blk: {
            break :blk errorable_result catch unreachable;
        } else blk: {
            break :blk errorable_result catch |err| switch (err) {
                error.OutOfMemory => return error.OutOfMemory,
                else => return error.UnableToSpawnCCompiler,
            };
        };
        defer {
            loop.allocator.free(exec_result.stdout);
            loop.allocator.free(exec_result.stderr);
        }

        switch (exec_result.term) {
            std.os.ChildProcess.Term.Exited => |code| {
                if (code != 0) return error.CCompilerExitCode;
            },
            else => {
                return error.CCompilerCrashed;
            },
        }

        var it = std.mem.tokenize(exec_result.stderr, "\n\r");
        var search_paths = std.ArrayList([]const u8).init(loop.allocator);
        defer search_paths.deinit();
        while (it.next()) |line| {
            if (line.len != 0 and line[0] == ' ') {
                try search_paths.append(line);
            }
        }
        if (search_paths.len == 0) {
            return error.CCompilerCannotFindHeaders;
        }

        // search in reverse order
        var path_i: usize = 0;
        while (path_i < search_paths.len) : (path_i += 1) {
            const search_path_untrimmed = search_paths.at(search_paths.len - path_i - 1);
            const search_path = std.mem.trimLeft(u8, search_path_untrimmed, " ");
            const stdlib_path = try std.os.path.join(loop.allocator, [][]const u8{ search_path, "stdlib.h" });
            defer loop.allocator.free(stdlib_path);

            if (try fileExists(stdlib_path)) {
                self.include_dir = try std.mem.dupe(loop.allocator, u8, search_path);
                return;
            }
        }

        return error.LibCStdLibHeaderNotFound;
    }

    async fn findNativeIncludeDirWindows(self: *LibCInstallation, loop: *event.Loop, sdk: *c.ZigWindowsSDK) !void {
        var search_buf: [2]Search = undefined;
        const searches = fillSearch(&search_buf, sdk);

        var result_buf = try std.Buffer.initSize(loop.allocator, 0);
        defer result_buf.deinit();

        for (searches) |search| {
            result_buf.shrink(0);
            const stream = &std.io.BufferOutStream.init(&result_buf).stream;
            try stream.print("{}\\Include\\{}\\ucrt", search.path, search.version);

            const stdlib_path = try std.os.path.join(
                loop.allocator,
                [][]const u8{ result_buf.toSliceConst(), "stdlib.h" },
            );
            defer loop.allocator.free(stdlib_path);

            if (try fileExists(stdlib_path)) {
                self.include_dir = result_buf.toOwnedSlice();
                return;
            }
        }

        return error.LibCStdLibHeaderNotFound;
    }

    async fn findNativeLibDirWindows(self: *LibCInstallation, loop: *event.Loop, sdk: *c.ZigWindowsSDK) FindError!void {
        var search_buf: [2]Search = undefined;
        const searches = fillSearch(&search_buf, sdk);

        var result_buf = try std.Buffer.initSize(loop.allocator, 0);
        defer result_buf.deinit();

        for (searches) |search| {
            result_buf.shrink(0);
            const stream = &std.io.BufferOutStream.init(&result_buf).stream;
            try stream.print("{}\\Lib\\{}\\ucrt\\", search.path, search.version);
            switch (builtin.arch) {
                builtin.Arch.i386 => try stream.write("x86"),
                builtin.Arch.x86_64 => try stream.write("x64"),
                builtin.Arch.aarch64 => try stream.write("arm"),
                else => return error.UnsupportedArchitecture,
            }
            const ucrt_lib_path = try std.os.path.join(
                loop.allocator,
                [][]const u8{ result_buf.toSliceConst(), "ucrt.lib" },
            );
            defer loop.allocator.free(ucrt_lib_path);
            if (try fileExists(ucrt_lib_path)) {
                self.lib_dir = result_buf.toOwnedSlice();
                return;
            }
        }
        return error.LibCRuntimeNotFound;
    }

    async fn findNativeLibDirLinux(self: *LibCInstallation, loop: *event.Loop) FindError!void {
        self.lib_dir = try await (async ccPrintFileName(loop, "crt1.o", true) catch unreachable);
    }

    async fn findNativeStaticLibDir(self: *LibCInstallation, loop: *event.Loop) FindError!void {
        self.static_lib_dir = try await (async ccPrintFileName(loop, "crtbegin.o", true) catch unreachable);
    }

    async fn findNativeDynamicLinker(self: *LibCInstallation, loop: *event.Loop) FindError!void {
        var dyn_tests = []DynTest{
            DynTest{
                .name = "ld-linux-x86-64.so.2",
                .result = null,
            },
            DynTest{
                .name = "ld-musl-x86_64.so.1",
                .result = null,
            },
        };
        var group = event.Group(FindError!void).init(loop);
        errdefer group.deinit();
        for (dyn_tests) |*dyn_test| {
            try group.call(testNativeDynamicLinker, self, loop, dyn_test);
        }
        try await (async group.wait() catch unreachable);
        for (dyn_tests) |*dyn_test| {
            if (dyn_test.result) |result| {
                self.dynamic_linker_path = result;
                return;
            }
        }
    }

    const DynTest = struct {
        name: []const u8,
        result: ?[]const u8,
    };

    async fn testNativeDynamicLinker(self: *LibCInstallation, loop: *event.Loop, dyn_test: *DynTest) FindError!void {
        if (await (async ccPrintFileName(loop, dyn_test.name, false) catch unreachable)) |result| {
            dyn_test.result = result;
            return;
        } else |err| switch (err) {
            error.LibCRuntimeNotFound => return,
            else => return err,
        }
    }

    async fn findNativeKernel32LibDir(self: *LibCInstallation, loop: *event.Loop, sdk: *c.ZigWindowsSDK) FindError!void {
        var search_buf: [2]Search = undefined;
        const searches = fillSearch(&search_buf, sdk);

        var result_buf = try std.Buffer.initSize(loop.allocator, 0);
        defer result_buf.deinit();

        for (searches) |search| {
            result_buf.shrink(0);
            const stream = &std.io.BufferOutStream.init(&result_buf).stream;
            try stream.print("{}\\Lib\\{}\\um\\", search.path, search.version);
            switch (builtin.arch) {
                builtin.Arch.i386 => try stream.write("x86\\"),
                builtin.Arch.x86_64 => try stream.write("x64\\"),
                builtin.Arch.aarch64 => try stream.write("arm\\"),
                else => return error.UnsupportedArchitecture,
            }
            const kernel32_path = try std.os.path.join(
                loop.allocator,
                [][]const u8{ result_buf.toSliceConst(), "kernel32.lib" },
            );
            defer loop.allocator.free(kernel32_path);
            if (try fileExists(kernel32_path)) {
                self.kernel32_lib_dir = result_buf.toOwnedSlice();
                return;
            }
        }
        return error.LibCKernel32LibNotFound;
    }

    fn initEmpty(self: *LibCInstallation) void {
        self.* = LibCInstallation{
            .include_dir = ([*]const u8)(undefined)[0..0],
            .lib_dir = null,
            .static_lib_dir = null,
            .msvc_lib_dir = null,
            .kernel32_lib_dir = null,
            .dynamic_linker_path = null,
        };
    }
};

/// caller owns returned memory
async fn ccPrintFileName(loop: *event.Loop, o_file: []const u8, want_dirname: bool) ![]u8 {
    const cc_exe = std.os.getEnvPosix("CC") orelse "cc";
    const arg1 = try std.fmt.allocPrint(loop.allocator, "-print-file-name={}", o_file);
    defer loop.allocator.free(arg1);
    const argv = []const []const u8{ cc_exe, arg1 };

    // TODO This simulates evented I/O for the child process exec
    await (async loop.yield() catch unreachable);
    const errorable_result = std.os.ChildProcess.exec(loop.allocator, argv, null, null, 1024 * 1024);
    const exec_result = if (std.debug.runtime_safety) blk: {
        break :blk errorable_result catch unreachable;
    } else blk: {
        break :blk errorable_result catch |err| switch (err) {
            error.OutOfMemory => return error.OutOfMemory,
            else => return error.UnableToSpawnCCompiler,
        };
    };
    defer {
        loop.allocator.free(exec_result.stdout);
        loop.allocator.free(exec_result.stderr);
    }
    switch (exec_result.term) {
        std.os.ChildProcess.Term.Exited => |code| {
            if (code != 0) return error.CCompilerExitCode;
        },
        else => {
            return error.CCompilerCrashed;
        },
    }
    var it = std.mem.tokenize(exec_result.stdout, "\n\r");
    const line = it.next() orelse return error.LibCRuntimeNotFound;
    const dirname = std.os.path.dirname(line) orelse return error.LibCRuntimeNotFound;

    if (want_dirname) {
        return std.mem.dupe(loop.allocator, u8, dirname);
    } else {
        return std.mem.dupe(loop.allocator, u8, line);
    }
}

const Search = struct {
    path: []const u8,
    version: []const u8,
};

fn fillSearch(search_buf: *[2]Search, sdk: *c.ZigWindowsSDK) []Search {
    var search_end: usize = 0;
    if (sdk.path10_ptr != 0) {
        if (sdk.version10_ptr != 0) {
            search_buf[search_end] = Search{
                .path = sdk.path10_ptr[0..sdk.path10_len],
                .version = sdk.version10_ptr[0..sdk.version10_len],
            };
            search_end += 1;
        }
    }
    if (sdk.path81_ptr != 0) {
        if (sdk.version81_ptr != 0) {
            search_buf[search_end] = Search{
                .path = sdk.path81_ptr[0..sdk.path81_len],
                .version = sdk.version81_ptr[0..sdk.version81_len],
            };
            search_end += 1;
        }
    }
    return search_buf[0..search_end];
}

fn fileExists(path: []const u8) !bool {
    if (std.os.File.access(path)) |_| {
        return true;
    } else |err| switch (err) {
        error.FileNotFound, error.PermissionDenied => return false,
        else => return error.FileSystem,
    }
}
