use core::str::FromStr;

use alloc::ffi::CString;

use crate::{
    io::{IoResult, cvt, sys_read, sys_write},
    sys::{
        SYS_CLOSE, SYS_DUP, SYS_DUP2, SYS_FSTAT, SYS_FTRUNCATE, SYS_IOCTL, SYS_ISATTY, SYS_LSEEK,
        SYS_OPEN, SYS_POLL, SYS_STAT, syscall1, syscall2, syscall3, syscall5,
    },
};

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct OpenFlags(pub u64);

impl OpenFlags {
    pub const NONE: Self = Self(0);
    /// Create file if it does not exist
    pub const CREATE: Self = Self(0x40);
    /// Append writes to end of file
    pub const APPEND: Self = Self(0x400);
    /// Truncate file to zero length on open
    pub const TRUNCATE: Self = Self(0x200);
}

impl core::ops::BitOr for OpenFlags {
    type Output = Self;
    fn bitor(self, rhs: Self) -> Self {
        Self(self.0 | rhs.0)
    }
}

impl core::ops::BitOrAssign for OpenFlags {
    fn bitor_assign(&mut self, rhs: Self) {
        self.0 |= rhs.0;
    }
}

#[derive(Debug)]
pub struct FileDesc {
    fd: u64,
    flags: OpenFlags,
}

impl FileDesc {
    #[inline]
    pub fn new(path: &str, flags: OpenFlags) -> IoResult<Self> {
        let path = CString::from_str(path).unwrap();
        let fd = cvt(unsafe { sys_open(path.as_ptr().cast(), flags.0) as isize })?;

        Ok(Self {
            fd: fd as u64,
            flags,
        })
    }

    pub fn from_raw_fd(fd: u64, flags: OpenFlags) -> Self {
        Self { fd, flags }
    }

    #[inline]
    pub fn close(self) {
        sys_close(self.fd);
    }

    /// Issue a device-specific ioctl on a file descriptor.
    #[inline]
    pub fn ioctl(&self, request: u64, arg: u64, arg_len: usize, flags: u64) -> IoResult<u64> {
        let result = cvt(unsafe {
            syscall5(SYS_IOCTL, self.fd, request, arg, arg_len as u64, flags) as isize
        })?;
        Ok(result as u64)
    }

    #[inline]
    pub fn read(&self, buf: &mut [u8]) -> IoResult<usize> {
        let result = cvt(unsafe { sys_read(self.fd, buf.as_mut_ptr(), buf.len()) })?;
        Ok(result as usize)
    }

    #[inline]
    pub fn write(&self, buf: &[u8]) -> IoResult<usize> {
        let result = cvt(unsafe { sys_write(self.fd, buf.as_ptr(), buf.len()) })?;
        Ok(result as usize)
    }

    #[inline]
    pub fn lseek(&self, offset: i64, whence: u32) -> IoResult<u64> {
        let result =
            cvt(unsafe { syscall3(SYS_LSEEK, self.fd, offset as u64, whence as u64) as isize })?;
        Ok(result as u64)
    }

    #[inline]
    pub fn ftruncate(&self, size: u64) -> IoResult<()> {
        let result = cvt(unsafe { syscall2(SYS_FTRUNCATE, self.fd, size) as isize })?;
        let _ = result;
        Ok(())
    }

    #[inline]
    pub fn raw_fd(&self) -> u64 {
        self.fd
    }

    /// Get file metadata for this file descriptor
    #[inline]
    pub fn fstat(&self) -> IoResult<FstatEntry> {
        sys_fstat(self.fd)
    }
}

/// Duplicate file descriptor oldfd to newfd
/// Returns newfd on success, or u64::MAX on error
#[inline]
/// Returns true if the file descriptor refers to a terminal.
pub fn isatty(fd: u64) -> bool {
    unsafe { syscall1(SYS_ISATTY, fd) == 1 }
}

pub fn sys_dup2(oldfd: u64, newfd: u64) -> u64 {
    unsafe { syscall2(SYS_DUP2, oldfd, newfd) }
}

/// Duplicate file descriptor to the lowest available descriptor number
#[inline]
pub fn sys_dup(oldfd: u64) -> u64 {
    unsafe { syscall1(SYS_DUP, oldfd) }
}

#[inline]
pub fn sys_close(fd: u64) -> i32 {
    unsafe { syscall1(SYS_CLOSE, fd) as i32 }
}

#[inline]
pub unsafe fn sys_open(path: *const u8, flags: u64) -> i64 {
    unsafe { syscall2(SYS_OPEN, path as u64, flags) as i64 }
}

#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct PollState {
    pub readable: bool,
    pub writable: bool,
    pub error: bool,
    pub hangup: bool,
    pub invalid: bool,
}

#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct PollFd {
    pub fd: u64,
    pub interests: PollState,
    pub result: PollState,
}

#[inline]
pub fn poll(entries: &mut [PollFd], timeout_ms: u64) -> u64 {
    let (ptr, len) = if entries.is_empty() {
        (core::ptr::null_mut::<PollFd>(), 0)
    } else {
        (entries.as_mut_ptr(), entries.len())
    };

    unsafe { syscall3(SYS_POLL, ptr as u64, len as u64, timeout_ms) }
}

/// File metadata returned by fstat
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub struct FstatEntry {
    pub size: u64,
    pub created: u64,
    pub accessed: u64,
    pub modified: u64,
    pub attrs: u16,
    pub kind: u8,
}

impl FstatEntry {
    /// Check if this is a regular file
    #[inline]
    pub fn is_file(&self) -> bool {
        self.kind == 0
    }

    /// Check if this is a directory
    #[inline]
    pub fn is_directory(&self) -> bool {
        self.kind == 1
    }

    /// Check if file is read-only
    #[inline]
    pub fn is_readonly(&self) -> bool {
        self.attrs & 1 != 0
    }

    /// Check if file is hidden
    #[inline]
    pub fn is_hidden(&self) -> bool {
        self.attrs & 2 != 0
    }
}

/// Get file metadata for a file descriptor
#[inline]
pub fn sys_fstat(fd: u64) -> IoResult<FstatEntry> {
    let mut entry = FstatEntry {
        size: 0,
        created: 0,
        accessed: 0,
        modified: 0,
        attrs: 0,
        kind: 0,
    };

    let _result =
        cvt(unsafe { syscall2(SYS_FSTAT, fd, &mut entry as *mut FstatEntry as u64) as isize })?;

    Ok(entry)
}

/// Get file metadata for a file path
#[inline]
pub fn stat_path(path: &str) -> IoResult<FstatEntry> {
    let mut entry = FstatEntry {
        size: 0,
        created: 0,
        accessed: 0,
        modified: 0,
        attrs: 0,
        kind: 0,
    };

    let path_bytes = path.as_bytes();
    let _result = cvt(unsafe {
        syscall3(
            SYS_STAT,
            path_bytes.as_ptr() as u64,
            path_bytes.len() as u64,
            &mut entry as *mut FstatEntry as u64,
        ) as isize
    })?;

    Ok(entry)
}
