/*
 * Description: I/O traits with special support for Linux.
 *
 * Copyright (C) 2025 d@nny mc² <dmc2@amass.energy>
 * SPDX-License-Identifier: AGPL-3.0-or-later
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published
 * by the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

//! I/O traits with special support for Linux.

use core::{mem, ops, ptr};
#[cfg(all(unix, feature = "std-io"))]
use std::{
  fs::File,
  io::{self, PipeReader, PipeWriter},
  os::fd::AsRawFd,
};

#[cfg(all(unix, feature = "std-io"))]
use libc;

use super::BufError;
#[cfg(all(unix, feature = "std-io"))]
use super::{BlockingPipe, NonBlockingPipe};

#[cfg(all(unix, feature = "std-io"))]
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct FilePos(libc::off64_t);
#[cfg(all(unix, feature = "std-io"))]
impl ops::Deref for FilePos {
  type Target = libc::off64_t;
  #[inline(always)]
  fn deref(&self) -> &Self::Target { &self.0 }
}
#[cfg(all(unix, feature = "std-io"))]
impl ops::DerefMut for FilePos {
  #[inline(always)]
  fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
}
#[cfg(all(unix, feature = "std-io"))]
impl From<u64> for FilePos {
  #[inline(always)]
  fn from(x: u64) -> Self {
    if x > libc::off64_t::MAX as u64 {
      unreachable!("could not convert {x} to file offset! (too large)");
    }
    Self(x as libc::off64_t)
  }
}
#[cfg(all(unix, feature = "std-io"))]
impl ops::AddAssign<usize> for FilePos {
  #[inline]
  fn add_assign(&mut self, rhs: usize) {
    if rhs > libc::off64_t::MAX as usize {
      unreachable!("could not convert {rhs} to file offset in +=! (too large)");
    }
    self.0 = match self.0.checked_add(rhs as libc::off64_t) {
      None => unreachable!("could not add {rhs} to file offset in +=! (would become too large)"),
      Some(n) => n,
    };
  }
}

pub trait PReadBuf<T> {
  type Pos: ops::AddAssign<usize>+Copy;
  type E: BufError;
  fn pread_buf<'buf>(
    &self,
    pos: Self::Pos,
    buf: &'buf mut [mem::MaybeUninit<T>],
  ) -> (usize, Option<Self::E>);
  #[inline]
  fn pread_buf_exact<'buf>(
    &self,
    mut pos: Self::Pos,
    mut buf: &'buf mut [mem::MaybeUninit<T>],
  ) -> Result<(), (usize, Self::E)> {
    let mut all_pread: usize = 0;
    while !buf.is_empty() {
      let (cur_pread, err) = self.pread_buf(pos, buf);
      all_pread += cur_pread;
      if let Some(err) = err {
        if err.would_block() {
          unreachable!("pread_buf() is allowed to block!");
        }
        if !err.was_interrupted() {
          return Err((all_pread, err));
        }
      }
      buf = &mut buf[cur_pread..];
      pos += cur_pread;
    }
    Ok(())
  }
}

#[cfg(all(unix, feature = "std-io"))]
impl PReadBuf<u8> for File {
  type Pos = FilePos;
  type E = io::Error;
  #[inline]
  fn pread_buf<'buf>(
    &self,
    pos: Self::Pos,
    buf: &'buf mut [mem::MaybeUninit<u8>],
  ) -> (usize, Option<Self::E>) {
    let fd = self.as_raw_fd();
    let FilePos(offset) = pos;
    let count = buf.len();
    let buf: *mut libc::c_void = buf.as_mut_ptr().cast();
    match unsafe { libc::pread64(fd, buf, count, offset) } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
}

pub trait PWriteData<T> {
  type Pos: ops::AddAssign<usize>+Copy;
  type E: BufError;
  fn pwrite_data<'data>(&mut self, pos: Self::Pos, data: &'data [T]) -> (usize, Option<Self::E>);
  #[inline]
  fn pwrite_data_exact<'data>(
    &mut self,
    mut pos: Self::Pos,
    mut data: &'data [T],
  ) -> Result<(), (usize, Self::E)> {
    let mut all_pwritten: usize = 0;
    while !data.is_empty() {
      let (cur_pwritten, err) = self.pwrite_data(pos, data);
      all_pwritten += cur_pwritten;
      if let Some(err) = err {
        if err.would_block() {
          unreachable!("pwrite_data() is allowed to block!");
        }
        if !err.was_interrupted() {
          return Err((all_pwritten, err));
        }
      }
      data = &data[cur_pwritten..];
      pos += cur_pwritten;
    }
    Ok(())
  }
}

#[cfg(all(unix, feature = "std-io"))]
impl PWriteData<u8> for File {
  type Pos = FilePos;
  type E = io::Error;
  #[inline]
  fn pwrite_data<'data>(&mut self, pos: Self::Pos, data: &'data [u8]) -> (usize, Option<Self::E>) {
    let fd = self.as_raw_fd();
    let FilePos(offset) = pos;
    let count = data.len();
    let buf: *const libc::c_void = data.as_ptr().cast();
    match unsafe { libc::pwrite64(fd, buf, count, offset) } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
}

pub trait Splice<P> {
  type E: BufError;
  fn splice(&mut self, to: &mut P, n: usize) -> (usize, Option<Self::E>);
  #[inline]
  fn splice_exact(&mut self, to: &mut P, mut n: usize) -> Result<(), (usize, Self::E)> {
    let mut all_spliced: usize = 0;
    while n > 0 {
      let (cur_spliced, err) = self.splice(to, n);
      all_spliced += cur_spliced;
      if let Some(err) = err {
        if err.would_block() {
          unreachable!("splice() is allowed to block!");
        }
        if !err.was_interrupted() {
          return Err((all_spliced, err));
        }
      }
      n -= cur_spliced;
    }
    Ok(())
  }
}
#[cfg(all(target_os = "linux", feature = "std-io"))]
impl Splice<BlockingPipe<PipeWriter>> for BlockingPipe<PipeReader> {
  type E = io::Error;
  #[inline]
  fn splice(&mut self, to: &mut BlockingPipe<PipeWriter>, n: usize) -> (usize, Option<Self::E>) {
    let in_fd = self.as_raw_fd();
    let out_fd = to.as_raw_fd();
    match unsafe { libc::splice(in_fd, ptr::null_mut(), out_fd, ptr::null_mut(), n, 0) } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
}
#[cfg(all(target_os = "linux", feature = "std-io"))]
impl Splice<BlockingPipe<PipeWriter>> for File {
  type E = io::Error;
  #[inline]
  fn splice(&mut self, to: &mut BlockingPipe<PipeWriter>, n: usize) -> (usize, Option<Self::E>) {
    let in_fd = self.as_raw_fd();
    let out_fd = to.as_raw_fd();
    match unsafe { libc::splice(in_fd, ptr::null_mut(), out_fd, ptr::null_mut(), n, 0) } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
}
#[cfg(all(target_os = "linux", feature = "std-io"))]
impl Splice<File> for BlockingPipe<PipeReader> {
  type E = io::Error;
  #[inline]
  fn splice(&mut self, to: &mut File, n: usize) -> (usize, Option<Self::E>) {
    let in_fd = self.as_raw_fd();
    let out_fd = to.as_raw_fd();
    match unsafe { libc::splice(in_fd, ptr::null_mut(), out_fd, ptr::null_mut(), n, 0) } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
}

pub trait SpliceNonBlocking<P> {
  type E: BufError;
  fn splice_nonblocking(&mut self, to: &mut P, n: usize) -> (usize, Option<Self::E>);
}
#[cfg(all(target_os = "linux", feature = "std-io"))]
impl SpliceNonBlocking<NonBlockingPipe<PipeWriter>> for NonBlockingPipe<PipeReader> {
  type E = io::Error;
  #[inline]
  fn splice_nonblocking(
    &mut self,
    to: &mut NonBlockingPipe<PipeWriter>,
    n: usize,
  ) -> (usize, Option<Self::E>) {
    let in_fd = self.as_raw_fd();
    let out_fd = to.as_raw_fd();
    match unsafe {
      libc::splice(
        in_fd,
        ptr::null_mut(),
        out_fd,
        ptr::null_mut(),
        n,
        libc::SPLICE_F_NONBLOCK,
      )
    } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
}

pub trait SpliceAt<P> {
  type E: BufError;
  type Pos: ops::AddAssign<usize>+Copy;
  fn splice_at(&self, start: &mut Self::Pos, n: usize, to: &mut P) -> (usize, Option<Self::E>);
  #[inline]
  fn splice_at_exact(
    &self,
    start: &mut Self::Pos,
    mut n: usize,
    to: &mut P,
  ) -> Result<(), (usize, Self::E)> {
    let mut all_spliced: usize = 0;
    while n > 0 {
      let (cur_spliced, err) = self.splice_at(start, n, to);
      all_spliced += cur_spliced;
      if let Some(err) = err {
        if err.would_block() {
          unreachable!("splice() is allowed to block!");
        }
        if !err.was_interrupted() {
          return Err((all_spliced, err));
        }
      }
      n -= cur_spliced;
    }
    Ok(())
  }
}
#[cfg(all(target_os = "linux", feature = "std-io"))]
impl SpliceAt<BlockingPipe<PipeWriter>> for File {
  type E = io::Error;
  type Pos = FilePos;
  #[inline]
  fn splice_at(
    &self,
    start: &mut Self::Pos,
    n: usize,
    to: &mut BlockingPipe<PipeWriter>,
  ) -> (usize, Option<Self::E>) {
    let in_fd = self.as_raw_fd();
    let out_fd = to.as_raw_fd();
    let &mut FilePos(mut in_off) = start;
    let in_off: *mut libc::off64_t = ptr::from_mut(&mut in_off);
    match unsafe { libc::splice(in_fd, in_off, out_fd, ptr::null_mut(), n, 0) } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
}

pub trait SpliceToAt<P> {
  type E: BufError;
  type Pos: ops::AddAssign<usize>+Copy;
  fn splice_to_at(&mut self, n: usize, to: &mut P, at: &mut Self::Pos) -> (usize, Option<Self::E>);
  #[inline]
  fn splice_to_at_exact(
    &mut self,
    mut n: usize,
    to: &mut P,
    at: &mut Self::Pos,
  ) -> Result<(), (usize, Self::E)> {
    let mut all_spliced: usize = 0;
    while n > 0 {
      let (cur_spliced, err) = self.splice_to_at(n, to, at);
      all_spliced += cur_spliced;
      if let Some(err) = err {
        if err.would_block() {
          unreachable!("splice() is allowed to block!");
        }
        if !err.was_interrupted() {
          return Err((all_spliced, err));
        }
      }
      n -= cur_spliced;
    }
    Ok(())
  }
}
#[cfg(all(target_os = "linux", feature = "std-io"))]
impl SpliceToAt<File> for BlockingPipe<PipeReader> {
  type E = io::Error;
  type Pos = FilePos;
  #[inline]
  fn splice_to_at(
    &mut self,
    n: usize,
    to: &mut File,
    at: &mut Self::Pos,
  ) -> (usize, Option<Self::E>) {
    let in_fd = self.as_raw_fd();
    let out_fd = to.as_raw_fd();
    let &mut FilePos(mut out_off) = at;
    let out_off: *mut libc::off64_t = ptr::from_mut(&mut out_off);
    match unsafe { libc::splice(in_fd, ptr::null_mut(), out_fd, out_off, n, 0) } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
}

pub trait Tee<P> {
  type E: BufError;
  fn tee(&self, to: &mut P, n: usize) -> (usize, Option<Self::E>);
  #[inline]
  fn tee_exact(&self, to: &mut P, mut n: usize) -> Result<(), (usize, Self::E)> {
    let mut all_teed: usize = 0;
    while n > 0 {
      let (cur_teed, err) = self.tee(to, n);
      all_teed += cur_teed;
      if let Some(err) = err {
        if err.would_block() {
          unreachable!("tee() is allowed to block!");
        }
        if !err.was_interrupted() {
          return Err((all_teed, err));
        }
      }
      n -= cur_teed;
    }
    Ok(())
  }
}
#[cfg(all(target_os = "linux", feature = "std-io"))]
impl Tee<BlockingPipe<PipeWriter>> for BlockingPipe<PipeReader> {
  type E = io::Error;
  #[inline]
  fn tee(&self, to: &mut BlockingPipe<PipeWriter>, n: usize) -> (usize, Option<Self::E>) {
    let in_fd = self.as_raw_fd();
    let out_fd = to.as_raw_fd();
    match unsafe { libc::tee(in_fd, out_fd, n, 0) } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
}

pub trait TeeNonBlocking<P> {
  type E: BufError;
  fn tee_nonblocking(&self, to: &mut P, n: usize) -> (usize, Option<Self::E>);
}
#[cfg(all(target_os = "linux", feature = "std-io"))]
impl TeeNonBlocking<NonBlockingPipe<PipeWriter>> for NonBlockingPipe<PipeReader> {
  type E = io::Error;
  #[inline]
  fn tee_nonblocking(
    &self,
    to: &mut NonBlockingPipe<PipeWriter>,
    n: usize,
  ) -> (usize, Option<Self::E>) {
    let in_fd = self.as_raw_fd();
    let out_fd = to.as_raw_fd();
    match unsafe { libc::tee(in_fd, out_fd, n, libc::SPLICE_F_NONBLOCK) } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
}

pub trait CopyRange<P> {
  type Pos: ops::AddAssign<usize>+Copy;
  type E: BufError;
  fn copy_range(
    &self,
    start: &mut Self::Pos,
    n: usize,
    to: &mut P,
    pos: Option<&mut Self::Pos>,
  ) -> (usize, Option<Self::E>);
  #[inline]
  fn copy_range_exact(
    &self,
    start: &mut Self::Pos,
    mut n: usize,
    to: &mut P,
    pos: Option<&mut Self::Pos>,
  ) -> Result<(), (usize, Self::E)> {
    let mut all_copied: usize = 0;
    let pos_ptr: Option<ptr::NonNull<Self::Pos>> = pos.map(ptr::NonNull::from_mut);
    while n > 0 {
      let (cur_copied, err) =
        self.copy_range(start, n, to, pos_ptr.map(|mut pos| unsafe { pos.as_mut() }));
      all_copied += cur_copied;
      if let Some(err) = err {
        if err.would_block() {
          unreachable!("copy_range() is allowed to block!");
        }
        if !err.was_interrupted() {
          return Err((all_copied, err));
        }
      }
      n -= cur_copied;
    }
    Ok(())
  }
  fn copy_range_mut(
    &mut self,
    n: usize,
    to: &mut P,
    pos: Option<&mut Self::Pos>,
  ) -> (usize, Option<Self::E>);
  #[inline]
  fn copy_range_exact_mut(
    &mut self,
    mut n: usize,
    to: &mut P,
    pos: Option<&mut Self::Pos>,
  ) -> Result<(), (usize, Self::E)> {
    let mut all_copied: usize = 0;
    let pos_ptr: Option<ptr::NonNull<Self::Pos>> = pos.map(ptr::NonNull::from_mut);
    while n > 0 {
      let (cur_copied, err) =
        self.copy_range_mut(n, to, pos_ptr.map(|mut pos| unsafe { pos.as_mut() }));
      all_copied += cur_copied;
      if let Some(err) = err {
        if err.would_block() {
          unreachable!("copy_range() is allowed to block!");
        }
        if !err.was_interrupted() {
          return Err((all_copied, err));
        }
      }
      n -= cur_copied;
    }
    Ok(())
  }
}

#[cfg(all(target_os = "linux", feature = "std-io"))]
impl CopyRange<File> for File {
  type Pos = FilePos;
  type E = io::Error;
  #[inline]
  fn copy_range(
    &self,
    start: &mut Self::Pos,
    n: usize,
    to: &mut File,
    mut pos: Option<&mut Self::Pos>,
  ) -> (usize, Option<Self::E>) {
    let in_fd = self.as_raw_fd();
    let out_fd = to.as_raw_fd();
    let &mut FilePos(mut in_off) = start;
    let in_off: *mut libc::off64_t = ptr::from_mut(&mut in_off);
    let out_off: *mut libc::off64_t = match pos.as_mut() {
      None => ptr::null_mut(),
      Some(off) => ptr::from_mut(off),
    };
    match unsafe { libc::copy_file_range(in_fd, in_off, out_fd, out_off, n, 0) } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
  #[inline]
  fn copy_range_mut(
    &mut self,
    n: usize,
    to: &mut File,
    mut pos: Option<&mut Self::Pos>,
  ) -> (usize, Option<Self::E>) {
    let in_fd = self.as_raw_fd();
    let out_fd = to.as_raw_fd();
    let out_off: *mut libc::off64_t = match pos.as_mut() {
      None => ptr::null_mut(),
      Some(off) => ptr::from_mut(off),
    };
    match unsafe { libc::copy_file_range(in_fd, ptr::null_mut(), out_fd, out_off, n, 0) } {
      -1 => (0, Some(io::Error::last_os_error())),
      n => (n as usize, None),
    }
  }
}
