const builtin = @import("builtin");

const std = @import("std");
const linux = std.os.linux;

const symbol = @import("../c.zig").symbol;
const errno = @import("../c.zig").errno;

comptime {
    if (builtin.target.isMuslLibC()) {
        symbol(&_exit, "_exit");

        symbol(&accessLinux, "access");
        symbol(&acctLinux, "acct");
        symbol(&chdirLinux, "chdir");
        symbol(&chownLinux, "chown");
        symbol(&close, "close");
        symbol(&posix_close, "posix_close");
        symbol(&fchownatLinux, "fchownat");
        symbol(&lchownLinux, "lchown");
        symbol(&chrootLinux, "chroot");
        symbol(&ctermidLinux, "ctermid");
        symbol(&dupLinux, "dup");
        symbol(&dup2Linux, "dup2");
        symbol(&dup3Linux, "dup3");
        symbol(&dup3Linux, "__dup3");

        symbol(&getegidLinux, "getegid");
        symbol(&geteuidLinux, "geteuid");
        symbol(&getgidLinux, "getgid");
        symbol(&getgroupsLinux, "getgroups");
        symbol(&getpgidLinux, "getpgid");
        symbol(&getpgrpLinux, "getpgrp");
        symbol(&setpgidLinux, "setpgid");
        symbol(&setpgrpLinux, "setpgrp");
        symbol(&getsidLinux, "getsid");
        symbol(&getpidLinux, "getpid");
        symbol(&getppidLinux, "getppid");
        symbol(&getuidLinux, "getuid");

        symbol(&rmdirLinux, "rmdir");
        symbol(&linkLinux, "link");
        symbol(&linkatLinux, "linkat");
        symbol(&pipeLinux, "pipe");
        symbol(&renameatLinux, "renameat");
        symbol(&symlinkLinux, "symlink");
        symbol(&symlinkatLinux, "symlinkat");
        symbol(&syncLinux, "sync");
        symbol(&unlinkLinux, "unlink");
        symbol(&unlinkatLinux, "unlinkat");

        symbol(&execveLinux, "execve");
    }
    if (builtin.target.isMuslLibC() or builtin.target.isWasiLibC()) {
        symbol(&swab, "swab");
    }
    if (builtin.target.isWasiLibC()) {
        symbol(&closeWasi, "close");
    }
}

fn _exit(exit_code: c_int) callconv(.c) noreturn {
    std.c._Exit(exit_code);
}

fn accessLinux(path: [*:0]const c_char, amode: c_int) callconv(.c) c_int {
    return errno(linux.access(@ptrCast(path), @bitCast(amode)));
}

fn acctLinux(path: [*:0]const c_char) callconv(.c) c_int {
    return errno(linux.acct(@ptrCast(path)));
}

fn chdirLinux(path: [*:0]const c_char) callconv(.c) c_int {
    return errno(linux.chdir(@ptrCast(path)));
}

fn chownLinux(path: [*:0]const c_char, uid: linux.uid_t, gid: linux.gid_t) callconv(.c) c_int {
    return errno(linux.chown(@ptrCast(path), uid, gid));
}

fn fchownatLinux(fd: c_int, path: [*:0]const c_char, uid: linux.uid_t, gid: linux.gid_t, flags: c_int) callconv(.c) c_int {
    return errno(linux.fchownat(fd, @ptrCast(path), uid, gid, @bitCast(flags)));
}

fn lchownLinux(path: [*:0]const c_char, uid: linux.uid_t, gid: linux.gid_t) callconv(.c) c_int {
    return errno(linux.lchown(@ptrCast(path), uid, gid));
}

fn chrootLinux(path: [*:0]const c_char) callconv(.c) c_int {
    return errno(linux.chroot(@ptrCast(path)));
}

fn ctermidLinux(maybe_path: ?[*]c_char) callconv(.c) [*:0]c_char {
    const default_tty = "/dev/tty";

    return if (maybe_path) |path| blk: {
        path[0..(default_tty.len + 1)].* = @bitCast(default_tty.*);
        break :blk path[0..default_tty.len :0].ptr;
    } else @ptrCast(@constCast(default_tty));
}

fn dupLinux(fd: c_int) callconv(.c) c_int {
    return errno(linux.dup(fd));
}

fn dup2Linux(old: c_int, new: c_int) callconv(.c) c_int {
    const busy: usize = @bitCast(-@as(isize, @intFromEnum(linux.E.BUSY)));
    var res = busy;
    while (res == busy) res = linux.dup2(old, new);
    return errno(res);
}

fn dup3Linux(old: c_int, new: c_int, flags: c_int) callconv(.c) c_int {
    const busy: usize = @bitCast(-@as(isize, @intFromEnum(linux.E.BUSY)));
    var res = busy;

    if (@hasField(linux.SYS, "dup3")) {
        while (res == busy) res = linux.dup3(old, new, @intCast(flags));
    } else if (@hasField(linux.SYS, "dup2")) {
        const cloexec: c_int = @bitCast(linux.O{ .CLOEXEC = true });
        const inval: usize = @bitCast(-@as(isize, @intFromEnum(linux.E.INVAL)));
        if (old == new or (flags & ~cloexec != 0)) return errno(inval);
        while (res == busy) res = linux.dup2(old, new);
        _ = if (res >= 0 and (flags & cloexec == cloexec)) linux.fcntl(new, linux.F.SETFD, linux.FD_CLOEXEC);
    } else {
        return errno(@bitCast(-@as(isize, @intFromEnum(linux.E.NOSYS))));
    }
    return errno(res);
}

fn getegidLinux() callconv(.c) linux.gid_t {
    return linux.getegid();
}

fn geteuidLinux() callconv(.c) linux.uid_t {
    return linux.geteuid();
}

fn getgidLinux() callconv(.c) linux.gid_t {
    return linux.getgid();
}

fn getgroupsLinux(size: c_int, list: ?[*]linux.gid_t) callconv(.c) c_int {
    return errno(linux.getgroups(@intCast(size), list));
}

fn getpgidLinux(pid: linux.pid_t) callconv(.c) linux.pid_t {
    return errno(linux.getpgid(pid));
}

fn getpgrpLinux() callconv(.c) linux.pid_t {
    return @intCast(linux.getpgid(0)); // @intCast as it cannot fail
}

fn setpgidLinux(pid: linux.pid_t, pgid: linux.pid_t) callconv(.c) c_int {
    return errno(linux.setpgid(pid, pgid));
}

fn setpgrpLinux() callconv(.c) linux.pid_t {
    return @intCast(linux.setpgid(0, 0)); // @intCast as it cannot fail
}

fn getpidLinux() callconv(.c) linux.pid_t {
    return linux.getpid();
}

fn getppidLinux() callconv(.c) linux.pid_t {
    return linux.getppid();
}

fn getsidLinux(pid: linux.pid_t) callconv(.c) linux.pid_t {
    return errno(linux.getsid(pid));
}

fn getuidLinux() callconv(.c) linux.uid_t {
    return linux.getuid();
}

fn linkLinux(old: [*:0]const c_char, new: [*:0]const c_char) callconv(.c) c_int {
    return errno(linux.link(@ptrCast(old), @ptrCast(new)));
}

fn linkatLinux(old_fd: c_int, old: [*:0]const c_char, new_fd: c_int, new: [*:0]const c_char, flags: c_int) callconv(.c) c_int {
    return errno(linux.linkat(old_fd, @ptrCast(old), new_fd, @ptrCast(new), @bitCast(flags)));
}

fn pipeLinux(fd: *[2]c_int) callconv(.c) c_int {
    return errno(linux.pipe(@ptrCast(fd)));
}

fn renameatLinux(old_fd: c_int, old: [*:0]const c_char, new_fd: c_int, new: [*:0]const c_char) callconv(.c) c_int {
    return errno(linux.renameat(old_fd, @ptrCast(old), new_fd, @ptrCast(new)));
}

fn rmdirLinux(path: [*:0]const c_char) callconv(.c) c_int {
    return errno(linux.rmdir(@ptrCast(path)));
}

fn symlinkLinux(existing: [*:0]const c_char, new: [*:0]const c_char) callconv(.c) c_int {
    return errno(linux.symlink(@ptrCast(existing), @ptrCast(new)));
}

fn symlinkatLinux(existing: [*:0]const c_char, fd: c_int, new: [*:0]const c_char) callconv(.c) c_int {
    return errno(linux.symlinkat(@ptrCast(existing), fd, @ptrCast(new)));
}

fn syncLinux() callconv(.c) void {
    linux.sync();
}

fn unlinkLinux(path: [*:0]const c_char) callconv(.c) c_int {
    return errno(linux.unlink(@ptrCast(path)));
}

fn unlinkatLinux(fd: c_int, path: [*:0]const c_char, flags: c_int) callconv(.c) c_int {
    return errno(linux.unlinkat(fd, @ptrCast(path), @bitCast(flags)));
}

fn execveLinux(path: [*:0]const c_char, argv: [*:null]const ?[*:0]c_char, envp: [*:null]const ?[*:0]c_char) callconv(.c) c_int {
    return errno(linux.execve(@ptrCast(path), @ptrCast(argv), @ptrCast(envp)));
}

fn swab(noalias src_ptr: *const anyopaque, noalias dest_ptr: *anyopaque, n: isize) callconv(.c) void {
    var src: [*]const u8 = @ptrCast(src_ptr);
    var dest: [*]u8 = @ptrCast(dest_ptr);
    var i = n;

    while (i > 1) : (i -= 2) {
        dest[0] = src[1];
        dest[1] = src[0];
        dest += 2;
        src += 2;
    }
}

fn close(fd: std.c.fd_t) callconv(.c) c_int {
    const signed: isize = @bitCast(linux.close(fd));
    if (signed < 0) {
        @branchHint(.unlikely);
        if (-signed == @intFromEnum(linux.E.INTR)) return 0;
        std.c._errno().* = @intCast(-signed);
        return -1;
    }
    return 0;
}

fn posix_close(fd: std.c.fd_t, _: c_int) callconv(.c) c_int {
    return close(fd);
}

fn closeWasi(fd: std.c.fd_t) callconv(.c) c_int {
    switch (std.os.wasi.fd_close(fd)) {
        .SUCCESS => return 0,
        else => |e| {
            std.c._errno().* = @intFromEnum(e);
            return -1;
        },
    }
}
