//! Unix-specific system calls and utilities.
use std::intrinsics;

/// File access modes.
export record OpenFlags: Copy(i64);

/// Open file for reading only.
export constant O_RDONLY: OpenFlags = OpenFlags(0);

/// Open file for writing only.
export constant O_WRONLY: OpenFlags = OpenFlags(1);

/// Open file for reading and writing.
export constant O_RDWR: OpenFlags = OpenFlags(2);

/// Create file if it doesn't exist.
export constant O_CREAT: OpenFlags = OpenFlags(64);

/// Truncate file to zero length.
export constant O_TRUNC: OpenFlags = OpenFlags(512);

/// Standard file descriptor for stdin.
export constant STDIN: i64 = 0;

/// Standard file descriptor for stdout.
export constant STDOUT: i64 = 1;

/// Standard file descriptor for stderr.
export constant STDERR: i64 = 2;

/// Special value representing current working directory for `openat()`.
constant AT_FDCWD: i64 = -100;

/// Opens a file at the given path and returns a file descriptor.
/// Returns a negative value on error.
export fn open(path: &[u8], flags: OpenFlags) -> i64 {
    unsafe {
        return intrinsics::ecall(56, AT_FDCWD, path.ptr as i64, *flags, 0);
    }
}

/// Opens a file at the given path with mode, returns a file descriptor.
export fn openOpts(path: &[u8], flags: OpenFlags, mode: i64) -> i64 {
    unsafe {
        return intrinsics::ecall(56, AT_FDCWD, path.ptr as i64, *flags, mode);
    }
}

/// Reads from a file descriptor into the provided buffer.
/// Returns the number of bytes read, or a negative value on error.
export fn read(fd: i64, buf: &mut [u8]) -> i64 {
    unsafe {
        return intrinsics::ecall(63, fd, buf.ptr as i64, buf.len as i64, 0);
    }
}

/// Reads from a file descriptor until EOF or buffer is full.
/// Returns the total number of bytes read, or a negative value on error.
export fn readToEnd(fd: i64, buf: &mut [u8]) -> i64 {
    let mut total: u32 = 0;
    while total < buf.len {
        let n = read(fd, &mut buf[total..]);

        if n < 0 {
            return n;
        }
        if n == 0 {
            break;
        }
        set total += n as u32;
    }
    return total as i64;
}

/// Writes to a file descriptor from the provided buffer.
/// Returns the number of bytes written, or a negative value on error.
export fn write(fd: i64, buf: &[u8]) -> i64 {
    unsafe {
        return intrinsics::ecall(64, fd, buf.ptr as i64, buf.len as i64, 0);
    }
}

/// Writes the entire contents of a buffer to a file descriptor.
/// Returns `false` when the descriptor cannot accept the full buffer.
export fn writeAll(fd: i64, data: &[u8]) -> bool {
    let mut written: u32 = 0;
    while written < data.len {
        let n = write(fd, &data[written..]);
        if n <= 0 {
            return false;
        }
        set written += n as u32;
    }
    return true;
}

/// Closes a file descriptor.
/// Returns `0` on success, or a negative value on error.
export fn close(fd: i64) -> i64 {
    return intrinsics::ecall(57, fd, 0, 0, 0);
}

/// Reads the entire contents of a file at the given path into the provided buffer.
/// Return the number of bytes read, or `nil` on error.
export fn readFile(path: &[u8], buf: &mut [u8]) -> ?u32 {
    let fd = open(path, O_RDONLY);
    if fd < 0 {
        return nil;
    }
    let n = readToEnd(fd, buf);

    if close(fd) < 0 {
        return nil;
    }
    if n < 0 {
        return nil;
    }
    return n as u32;
}

/// Exit the current process with the given status code.
export fn exit(status: i64) {
    intrinsics::ecall(93, status, 0, 0, 0);
}

/// Writes the entire contents of a buffer to a file at the given path.
/// Creates the file if it doesn't exist, truncates if it does.
/// Returns `true` on success.
export fn writeFile(path: &[u8], data: &[u8]) -> bool {
    let flags = OpenFlags(*O_WRONLY | *O_CREAT | *O_TRUNC);
    let fd = openOpts(path, flags, 420);
    if fd < 0 { return false; }
    let written = writeAll(fd, data);
    let closed = close(fd) == 0;
    return written and closed;
}

/// Writes each buffer to a file at the given path.
/// Creates the file if it doesn't exist, truncates if it does.
/// Returns `true` only when every part and the final close succeed.
export fn writeFileParts(path: &[u8], parts: &[*[u8]]) -> bool {
    let flags = OpenFlags(*O_WRONLY | *O_CREAT | *O_TRUNC);
    let fd = openOpts(path, flags, 420); // 0644 in octal.
    if fd < 0 {
        return false;
    }

    for part in parts {
        if not writeAll(fd, part) {
            close(fd);
            return false;
        }
    }
    return close(fd) == 0;
}
