// SPDX-License-Identifier: MPL-2.0

use super::SyscallReturn;
use crate::{
    fs::{
        file_table::FileDesc,
        fs_resolver::{AT_FDCWD, FsPath, SplitPath},
        utils::InodeType,
    },
    prelude::*,
    syscall::constants::MAX_FILENAME_LEN,
};

pub fn sys_renameat2(
    old_dirfd: FileDesc,
    old_path_addr: Vaddr,
    new_dirfd: FileDesc,
    new_path_addr: Vaddr,
    flags: u32,
    ctx: &Context,
) -> Result<SyscallReturn> {
    let user_space = ctx.user_space();
    let old_path_name = user_space.read_cstring(old_path_addr, MAX_FILENAME_LEN)?;
    let new_path_name = user_space.read_cstring(new_path_addr, MAX_FILENAME_LEN)?;
    debug!(
        "old_dirfd = {}, old_path = {:?}, new_dirfd = {}, new_path = {:?}",
        old_dirfd, old_path_name, new_dirfd, new_path_name
    );
    let Some(flags) = Flags::from_bits(flags) else {
        return_errno_with_message!(Errno::EINVAL, "invalid flags");
    };
    // TODO: Add support for handling the `NOREPLACE`, `EXCHANGE`, and `WHITEOUT` flags.
    if !flags.is_empty() {
        warn!("unsupported flags: {:?}", flags);
        return_errno_with_message!(Errno::EINVAL, "unsupported flags");
    }

    let fs_ref = ctx.thread_local.borrow_fs();
    let fs = fs_ref.resolver().read();

    let old_path_name = old_path_name.to_string_lossy();
    let (old_dir_path, old_name) = {
        let (old_parent_path_name, old_name) = old_path_name.split_dirname_and_basename()?;
        let old_fs_path = FsPath::from_fd_and_path(old_dirfd, old_parent_path_name)?;
        (fs.lookup(&old_fs_path)?, old_name)
    };
    let old_path = old_dir_path.lookup(old_name)?;
    if old_path.type_() != InodeType::Dir && old_path_name.ends_with('/') {
        return_errno_with_message!(Errno::ENOTDIR, "the old path is not a directory");
    }

    let new_path_name = new_path_name.to_string_lossy();
    let (new_dir_path, new_name) = {
        if old_path.type_() != InodeType::Dir && new_path_name.ends_with('/') {
            return_errno_with_message!(Errno::EISDIR, "the new path is a directory");
        }
        let (new_parent_path_name, new_name) = new_path_name.split_dirname_and_basename()?;
        let new_fs_path = FsPath::from_fd_and_path(new_dirfd, new_parent_path_name)?;
        (fs.lookup(&new_fs_path)?, new_name)
    };

    // Check the absolute path
    let old_abs_path = old_path.abs_path();
    let new_abs_path = new_dir_path.abs_path() + "/" + new_name;
    if new_abs_path.starts_with(&old_abs_path) {
        if new_abs_path.len() == old_abs_path.len() {
            return Ok(SyscallReturn::Return(0));
        } else {
            return_errno_with_message!(
                Errno::EINVAL,
                "the new path contains a path prefix of the old path"
            );
        }
    }

    old_dir_path.rename(old_name, &new_dir_path, new_name)?;

    Ok(SyscallReturn::Return(0))
}

pub fn sys_renameat(
    old_dirfd: FileDesc,
    old_path_addr: Vaddr,
    new_dirfd: FileDesc,
    new_path_addr: Vaddr,
    ctx: &Context,
) -> Result<SyscallReturn> {
    self::sys_renameat2(old_dirfd, old_path_addr, new_dirfd, new_path_addr, 0, ctx)
}

pub fn sys_rename(
    old_path_addr: Vaddr,
    new_path_addr: Vaddr,
    ctx: &Context,
) -> Result<SyscallReturn> {
    self::sys_renameat2(AT_FDCWD, old_path_addr, AT_FDCWD, new_path_addr, 0, ctx)
}

bitflags! {
    /// Flags used in the `renameat2` system call.
    ///
    /// Reference: <https://elixir.bootlin.com/linux/v6.16.3/source/include/uapi/linux/fcntl.h#L140-L143>.
    struct Flags: u32 {
        const NOREPLACE = 1 << 0;
        const EXCHANGE  = 1 << 1;
        const WHITEOUT  = 1 << 2;
    }
}
