// SPDX-License-Identifier: MPL-2.0

use super::SyscallReturn;
use crate::{
    fs::{
        file_handle::FileLike,
        file_table::{get_file_fast, FdFlags, FileDesc, WithFileTable},
        utils::{
            FileRange, RangeLockItem, RangeLockItemBuilder, RangeLockType, StatusFlags, OFFSET_MAX,
        },
    },
    prelude::*,
    process::{process_table, Pid},
};

pub fn sys_fcntl(fd: FileDesc, cmd: i32, arg: u64, ctx: &Context) -> Result<SyscallReturn> {
    let fcntl_cmd = FcntlCmd::try_from(cmd)?;
    debug!("fd = {}, cmd = {:?}, arg = {}", fd, fcntl_cmd, arg);
    match fcntl_cmd {
        FcntlCmd::F_DUPFD => handle_dupfd(fd, arg, FdFlags::empty(), ctx),
        FcntlCmd::F_DUPFD_CLOEXEC => handle_dupfd(fd, arg, FdFlags::CLOEXEC, ctx),
        FcntlCmd::F_GETFD => handle_getfd(fd, ctx),
        FcntlCmd::F_SETFD => handle_setfd(fd, arg, ctx),
        FcntlCmd::F_GETFL => handle_getfl(fd, ctx),
        FcntlCmd::F_SETFL => handle_setfl(fd, arg, ctx),
        FcntlCmd::F_GETLK => handle_getlk(fd, arg, ctx),
        FcntlCmd::F_SETLK => handle_setlk(fd, arg, true, ctx),
        FcntlCmd::F_SETLKW => handle_setlk(fd, arg, false, ctx).map_err(|err| match err.error() {
            Errno::EINTR => Error::new(Errno::ERESTARTSYS),
            _ => err,
        }),
        FcntlCmd::F_GETOWN => handle_getown(fd, ctx),
        FcntlCmd::F_SETOWN => handle_setown(fd, arg, ctx),
    }
}

fn handle_dupfd(fd: FileDesc, arg: u64, flags: FdFlags, ctx: &Context) -> Result<SyscallReturn> {
    let file_table = ctx.thread_local.file_table().borrow();
    let new_fd = file_table.write().dup(fd, arg as FileDesc, flags)?;
    Ok(SyscallReturn::Return(new_fd as _))
}

fn handle_getfd(fd: FileDesc, ctx: &Context) -> Result<SyscallReturn> {
    let mut file_table = ctx.thread_local.file_table().borrow_mut();
    file_table.read_with(|inner| {
        let fd_flags = inner.get_entry(fd)?.flags();
        Ok(SyscallReturn::Return(fd_flags.bits() as _))
    })
}

fn handle_setfd(fd: FileDesc, arg: u64, ctx: &Context) -> Result<SyscallReturn> {
    let flags = if arg > u64::from(u8::MAX) {
        return_errno_with_message!(Errno::EINVAL, "invalid fd flags");
    } else {
        FdFlags::from_bits(arg as u8).ok_or(Error::with_message(Errno::EINVAL, "invalid flags"))?
    };
    let mut file_table = ctx.thread_local.file_table().borrow_mut();
    file_table.read_with(|inner| {
        inner.get_entry(fd)?.set_flags(flags);
        Ok(SyscallReturn::Return(0))
    })
}

fn handle_getfl(fd: FileDesc, ctx: &Context) -> Result<SyscallReturn> {
    let mut file_table = ctx.thread_local.file_table().borrow_mut();
    let file = get_file_fast!(&mut file_table, fd);
    let status_flags = file.status_flags();
    let access_mode = file.access_mode();
    Ok(SyscallReturn::Return(
        (status_flags.bits() | access_mode as u32) as _,
    ))
}

fn handle_setfl(fd: FileDesc, arg: u64, ctx: &Context) -> Result<SyscallReturn> {
    let mut file_table = ctx.thread_local.file_table().borrow_mut();
    let file = get_file_fast!(&mut file_table, fd);
    let valid_flags_mask = StatusFlags::O_APPEND
        | StatusFlags::O_ASYNC
        | StatusFlags::O_DIRECT
        | StatusFlags::O_NOATIME
        | StatusFlags::O_NONBLOCK;
    let mut status_flags = file.status_flags();
    status_flags.remove(valid_flags_mask);
    status_flags.insert(StatusFlags::from_bits_truncate(arg as _) & valid_flags_mask);
    file.set_status_flags(status_flags)?;
    Ok(SyscallReturn::Return(0))
}

fn handle_getlk(fd: FileDesc, arg: u64, ctx: &Context) -> Result<SyscallReturn> {
    let mut file_table = ctx.thread_local.file_table().borrow_mut();
    let file = get_file_fast!(&mut file_table, fd);
    let lock_mut_ptr = arg as Vaddr;
    let mut lock_mut_c = ctx.user_space().read_val::<c_flock>(lock_mut_ptr)?;
    let lock_type = RangeLockType::try_from(lock_mut_c.l_type)?;
    if lock_type == RangeLockType::Unlock {
        return_errno_with_message!(Errno::EINVAL, "invalid flock type for getlk");
    }
    let mut lock = RangeLockItemBuilder::new()
        .type_(lock_type)
        .range(from_c_flock_and_file(&lock_mut_c, &**file)?)
        .build()?;
    let inode_file = file.as_inode_or_err()?;
    lock = inode_file.test_range_lock(lock)?;
    lock_mut_c.copy_from_range_lock(&lock);
    ctx.user_space().write_val(lock_mut_ptr, &lock_mut_c)?;
    Ok(SyscallReturn::Return(0))
}

fn handle_setlk(
    fd: FileDesc,
    arg: u64,
    is_nonblocking: bool,
    ctx: &Context,
) -> Result<SyscallReturn> {
    let mut file_table = ctx.thread_local.file_table().borrow_mut();
    let file = get_file_fast!(&mut file_table, fd);
    let lock_mut_ptr = arg as Vaddr;
    let lock_mut_c = ctx.user_space().read_val::<c_flock>(lock_mut_ptr)?;
    let lock_type = RangeLockType::try_from(lock_mut_c.l_type)?;
    let lock = RangeLockItemBuilder::new()
        .type_(lock_type)
        .range(from_c_flock_and_file(&lock_mut_c, &**file)?)
        .build()?;
    let inode_file = file.as_inode_or_err()?;
    inode_file.set_range_lock(&lock, is_nonblocking)?;
    Ok(SyscallReturn::Return(0))
}

fn handle_getown(fd: FileDesc, ctx: &Context) -> Result<SyscallReturn> {
    let mut file_table = ctx.thread_local.file_table().borrow_mut();
    file_table.read_with(|inner| {
        let pid = inner.get_entry(fd)?.owner().unwrap_or(0);
        Ok(SyscallReturn::Return(pid as _))
    })
}

fn handle_setown(fd: FileDesc, arg: u64, ctx: &Context) -> Result<SyscallReturn> {
    // A process ID is specified as a positive value; a process group ID is specified as a negative value.
    let abs_arg = (arg as i32).unsigned_abs();
    if abs_arg > i32::MAX as u32 {
        return_errno_with_message!(Errno::EINVAL, "process (group) id overflowed");
    }
    let pid = Pid::try_from(abs_arg)
        .map_err(|_| Error::with_message(Errno::EINVAL, "invalid process (group) id"))?;

    let owner_process = if pid == 0 {
        None
    } else {
        Some(process_table::get_process(pid).ok_or(Error::with_message(
            Errno::ESRCH,
            "cannot set_owner with an invalid pid",
        ))?)
    };

    let file_table = ctx.thread_local.file_table().borrow();
    let mut file_table_locked = file_table.write();
    let file_entry = file_table_locked.get_entry_mut(fd)?;
    file_entry.set_owner(owner_process.as_ref())?;
    Ok(SyscallReturn::Return(0))
}

#[repr(i32)]
#[derive(Debug, Clone, Copy, TryFromInt)]
#[allow(non_camel_case_types)]
enum FcntlCmd {
    F_DUPFD = 0,
    F_GETFD = 1,
    F_SETFD = 2,
    F_GETFL = 3,
    F_SETFL = 4,
    F_GETLK = 5,
    F_SETLK = 6,
    F_SETLKW = 7,
    F_SETOWN = 8,
    F_GETOWN = 9,
    F_DUPFD_CLOEXEC = 1030,
}

#[allow(non_camel_case_types)]
pub type off_t = i64;

#[allow(non_camel_case_types)]
#[derive(Debug, Copy, Clone, TryFromInt)]
#[repr(u16)]
pub enum RangeLockWhence {
    SEEK_SET = 0,
    SEEK_CUR = 1,
    SEEK_END = 2,
}

/// C struct for a file range lock in Libc
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod)]
pub struct c_flock {
    /// Type of lock: F_RDLCK, F_WRLCK, or F_UNLCK
    pub l_type: u16,
    /// Where `l_start' is relative to
    pub l_whence: u16,
    /// Offset where the lock begins
    pub l_start: off_t,
    /// Size of the locked area, 0 means until EOF
    pub l_len: off_t,
    /// Process holding the lock
    pub l_pid: Pid,
}

impl c_flock {
    pub fn copy_from_range_lock(&mut self, lock: &RangeLockItem) {
        self.l_type = lock.type_() as u16;
        if RangeLockType::Unlock != lock.type_() {
            self.l_whence = RangeLockWhence::SEEK_SET as u16;
            self.l_start = lock.start() as off_t;
            self.l_len = if lock.end() == OFFSET_MAX {
                0
            } else {
                lock.range().len() as off_t
            };
            self.l_pid = lock.owner();
        }
    }
}

/// Create the file range through C flock and opened file reference
fn from_c_flock_and_file(lock: &c_flock, file: &dyn FileLike) -> Result<FileRange> {
    let start = {
        let whence = RangeLockWhence::try_from(lock.l_whence)?;
        match whence {
            RangeLockWhence::SEEK_SET => lock.l_start,
            RangeLockWhence::SEEK_CUR => (file.as_inode_or_err()?.offset() as off_t)
                .checked_add(lock.l_start)
                .ok_or(Error::with_message(Errno::EOVERFLOW, "start overflow"))?,

            RangeLockWhence::SEEK_END => (file.metadata().size as off_t)
                .checked_add(lock.l_start)
                .ok_or(Error::with_message(Errno::EOVERFLOW, "start overflow"))?,
        }
    };

    let (start, end) = match lock.l_len {
        len if len > 0 => {
            let end = start
                .checked_add(len)
                .ok_or(Error::with_message(Errno::EOVERFLOW, "end overflow"))?;
            (start as usize, end as usize)
        }
        0 => (start as usize, OFFSET_MAX),
        len if len < 0 => {
            let end = start;
            let new_start = start + len;
            if new_start < 0 {
                return Err(Error::with_message(Errno::EINVAL, "invalid len"));
            }
            (new_start as usize, end as usize)
        }
        _ => unreachable!(),
    };

    FileRange::new(start, end)
}
