//
// Syd: rock-solid application kernel
// src/io.rs: I/O utilities
//
// Copyright (c) 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

use std::{
    fs::File,
    io::{Read, Stdin, Write},
    os::fd::AsFd,
};

use nix::{
    errno::Errno,
    sys::sendfile::sendfile64,
    unistd::{read, write},
};

use crate::{
    compat::{fstatx, STATX_SIZE},
    err2no,
    retry::retry_on_eintr,
};

/// Read until EOF or `buf` is full from the given file.
///
/// Returns number of bytes read. NO-OP if `buf` is empty.
pub fn read_buf<Fd: AsFd>(fd: Fd, buf: &mut [u8]) -> Result<usize, Errno> {
    let mut nread = 0;

    while nread < buf.len() {
        match retry_on_eintr(|| read(&fd, &mut buf[nread..]))? {
            0 => break,
            n => nread = nread.checked_add(n).ok_or(Errno::EOVERFLOW)?,
        }
    }

    Ok(nread)
}

/// Read until EOF from the given file.
///
/// Returns number of bytes read.
pub fn read_all<Fd: AsFd>(fd: Fd) -> Result<Vec<u8>, Errno> {
    let mut buf = Vec::new();

    let size = fstatx(&fd, STATX_SIZE)
        .map(|stx| stx.stx_size)
        .and_then(|size| usize::try_from(size).or(Err(Errno::EOVERFLOW)))?;
    if size == 0 {
        return Ok(buf);
    }

    buf.try_reserve(size).or(Err(Errno::ENOMEM))?;
    buf.resize(size, 0);

    let n = read_buf(fd, &mut buf)?;
    buf.truncate(n);

    Ok(buf)
}

/// Write all the data to the given file.
///
/// Returns `Errno::EPIPE` on EOF. NO-OP if data is empty.
pub fn write_all<Fd: AsFd>(fd: Fd, data: &[u8]) -> Result<(), Errno> {
    let mut nwrite = 0;

    while nwrite < data.len() {
        match retry_on_eintr(|| write(&fd, &data[nwrite..]))? {
            0 => return Err(Errno::EPIPE),
            n => nwrite = nwrite.checked_add(n).ok_or(Errno::EOVERFLOW)?,
        }
    }

    Ok(())
}

/// Super trait: AsFd + Read.
pub trait ReadFd: AsFd + Read {}

/// Super trait: AsFd + Write.
pub trait WriteFd: AsFd + Write {}

impl ReadFd for File {}
impl ReadFd for Stdin {}

impl WriteFd for File {}

/// Copy all available data from one file to another.
///
/// Uses `nix::fcntl::sendfile64` and falls back to `std::io::copy`
/// on errors `Err(Errno::EINVAL)` and `Err(Errno::ENOSYS)`.
pub fn copy<Fd1, Fd2>(src: &mut Fd1, dst: &mut Fd2) -> Result<u64, Errno>
where
    Fd1: ReadFd,
    Fd2: WriteFd,
{
    // sendfile() will transfer at most 0x7ffff000 (2,147,479,552) bytes,
    // returning the number of bytes actually transferred. (This is true on
    // both 32-bit and 64-bit systems.)
    const MAX: usize = 0x7ffff000;

    let mut ncopy = 0;
    loop {
        return match sendfile64(&dst, &src, None, MAX) {
            Ok(0) => Ok(ncopy),
            Ok(n) => {
                let n = n.try_into().or(Err(Errno::EOVERFLOW))?;
                ncopy = ncopy.checked_add(n).ok_or(Errno::EOVERFLOW)?;
                continue;
            }
            Err(Errno::EINTR) => continue,
            Err(Errno::EINVAL | Errno::ENOSYS) =>
            {
                #[expect(clippy::disallowed_methods)]
                std::io::copy(src, dst).map_err(|err| err2no(&err))
            }
            Err(errno) => Err(errno),
        };
    }
}
