use core::mem::MaybeUninit;

use libk_util::ext::OptionExt;
use yggdrasil_abi::{
    error::Error,
    io::{DirectoryEntry, FileMetadataUpdate, FileMode, GroupId, OpenOptions, UserId},
};

use crate::vfs::{
    file::{File, FileRef},
    path::OwnedFilename,
    Filename,
};

use super::{AccessToken, CreateInfo, DirectoryData, Metadata, Node, NodeFlags, NodeImpl, NodeRef};

impl Node {
    // Devices

    /// Performs a device-specific function on a the device node
    pub fn device_request(
        self: &NodeRef,
        option: u32,
        buffer: &mut [u8],
        len: usize,
    ) -> Result<usize, Error> {
        match &self.data {
            NodeImpl::Block(dev) => dev.device_request(option, buffer, len),
            NodeImpl::Char(dev) => dev.device_request(option, buffer, len),
            _ => Err(Error::InvalidOperation),
        }
    }

    // Devices + files

    /// Opens the node with given [OpenOptions]. Only works for regular files and devices. For
    /// directories, use [Node::open_directory].
    pub fn open(self: &NodeRef, opts: OpenOptions, _check: AccessToken) -> Result<FileRef, Error> {
        match &self.data {
            NodeImpl::Regular(imp) => {
                let (pos, instance) = imp.open(self, opts)?;
                if opts.contains(OpenOptions::TRUNCATE) {
                    imp.truncate(self, 0)?;
                }
                Ok(File::regular(self.clone(), pos, instance, opts))
            }
            NodeImpl::Block(dev) => File::block(dev.clone(), self.clone(), opts),
            NodeImpl::Char(dev) => File::char(dev.clone(), self.clone(), opts),
            NodeImpl::Hardlink(_) => unreachable!("BUG: Hard links cannot be referenced directly"),
            // TODO: maybe merge open_directory and open?
            NodeImpl::Directory(_) => Err(Error::IsADirectory),
            NodeImpl::Symlink(_) => todo!(),
            NodeImpl::PseudoTerminalSlave(pty) => {
                let pty = pty.upgrade().ok_or(Error::DoesNotExist)?;
                Ok(File::open_pty_slave(pty, self.clone()))
            }
            NodeImpl::PseudoTerminalMaster(pty) => {
                let pty = pty.upgrade().ok_or(Error::DoesNotExist)?;
                Ok(File::open_pty_master(pty, self.clone()))
            }
        }
    }

    // Directory

    /// Opens the node as a directory for reading its entries
    pub fn open_directory(self: &NodeRef, _check: AccessToken) -> Result<FileRef, Error> {
        let dir = self.as_directory()?;
        let pos = dir.imp.open(self)?;
        Ok(File::directory(self.clone(), pos))
    }

    /// Reads entries from the directory
    pub fn read_directory(
        self: &NodeRef,
        pos: u64,
        entries: &mut [MaybeUninit<DirectoryEntry>],
    ) -> Result<(usize, u64), Error> {
        self.as_directory()?.imp.read_entries(self, pos, entries)
    }

    /// Attempts to look up a child node with given name inside the directory node in the tree
    /// cache. If no such node is present there, will attempt to fetch it from the underlying
    /// filesystem.
    pub fn lookup_or_load(
        self: &NodeRef,
        name: &Filename,
        _check: AccessToken,
    ) -> Result<NodeRef, Error> {
        let dir = self.as_directory()?;

        {
            let children = dir.children.lock();

            if let Some((_, node)) = children.iter().find(|(name_, _)| name_ == name) {
                return Ok(node.clone());
            }
        }

        let node = match dir.imp.lookup(self, name) {
            Err(Error::NotImplemented) => Err(Error::DoesNotExist),
            res => res,
        }?;

        self.add_child(name, node.clone())?;

        Ok(node)
    }

    /// Creates an entry within a directory with given [CreateInfo].
    pub fn create(self: &NodeRef, info: CreateInfo, check: AccessToken) -> Result<NodeRef, Error> {
        let directory = self.as_directory()?;
        if directory.contains(self, info.name) {
            return Err(Error::AlreadyExists);
        }
        let node = directory.imp.create_node(self, &info)?;

        self.create_node(node.clone(), info.name, check.clone())?;

        // Fill out the node info
        node.set_access(Some(info.uid), Some(info.gid), Some(info.mode), check)?;

        Ok(node)
    }

    /// Attaches a pre-created node to its parent
    pub fn create_node<F: Into<OwnedFilename>>(
        self: &NodeRef,
        node: NodeRef,
        name: F,
        _check: AccessToken,
    ) -> Result<NodeRef, Error> {
        let filename = name.into();
        let directory = self.as_directory()?;

        match directory.imp.attach_node(self, &node, filename.as_ref()) {
            Ok(_) | Err(Error::NotImplemented) => (),
            Err(err) => return Err(err),
        }

        // Attach the created node to the directory in memory cache
        self.add_child(filename, node.clone())?;

        Ok(node)
    }

    /// Removes a regular file, device or symlink from the directory
    pub fn remove_file(self: &NodeRef, name: &Filename, check: AccessToken) -> Result<(), Error> {
        let directory = self.as_directory()?;
        let child = self.lookup_or_load(name, check)?;

        // Detach the node in the real filesystem
        // If the filesystem implements unlink_node(), then checking if it's okay to
        // remove a directory node is done by the filesystem itself, if not, check the
        // cache.
        let need_nonempty_check = match directory.imp.unlink_node(self, &child, name) {
            Ok(_) => false,
            Err(Error::NotImplemented) => true,
            Err(err) => return Err(err),
        };

        if need_nonempty_check && let Ok(child_dir) = child.as_directory() {
            let child_children = child_dir.children.lock();
            if !child_children.is_empty() {
                return Err(Error::IsADirectory);
            }
            directory.remove_child(name);
        } else {
            // Just unlink the node from the cache tree
            directory.remove_child(name);
        }

        Ok(())
    }

    pub fn move_node(
        source: &NodeRef,
        destination: &NodeRef,
        source_name: &Filename,
        destination_name: &Filename,
        access: AccessToken,
    ) -> Result<(), Error> {
        if !Self::is_same_filesystem(source, destination) {
            return Err(Error::CrossDeviceLink);
        }

        let source_dir = source.as_directory()?;
        let destination_dir = destination.as_directory()?;

        let node = source.lookup_or_load(source_name, access)?;

        match source_dir.imp.unlink_node(source, &node, source_name) {
            Ok(()) | Err(Error::NotImplemented) => (),
            Err(error) => {
                log::warn!("Rename: unlink {source_name}: {error:?}");
                return Err(error);
            }
        }

        match destination_dir
            .imp
            .attach_node(destination, &node, destination_name)
        {
            Ok(()) | Err(Error::NotImplemented) => (),
            Err(error) => {
                log::warn!("Rename: attach {destination_name}: {error:?}");
                return Err(error);
            }
        }

        source_dir.remove_child(source_name);
        destination.add_child(destination_name, node)?;

        Ok(())
    }

    // Common

    /// Changes user/group ID or access mode of the node
    pub fn set_access(
        self: &NodeRef,
        uid: Option<UserId>,
        gid: Option<GroupId>,
        mode: Option<FileMode>,
        _check: AccessToken,
    ) -> Result<(), Error> {
        if uid.is_none() && gid.is_none() && mode.is_none() {
            return Err(Error::InvalidOperation);
        }

        let common = self.data_as_common();
        if self.flags.contains(NodeFlags::IN_MEMORY_PROPS) {
            let mut cache = self.props.lock();

            let metadata = cache
                .metadata
                .get_or_try_insert_with(|| common.metadata(self))?;

            if let Some(uid) = uid {
                metadata.uid = uid;
            }
            if let Some(gid) = gid {
                metadata.gid = gid;
            }
            if let Some(mode) = mode {
                metadata.mode = mode;
            }

            Ok(())
        } else {
            // Update permissions in the real node
            let mut metadata = common.metadata(self)?;
            if let Some(uid) = uid {
                metadata.uid = uid;
            }
            if let Some(gid) = gid {
                metadata.gid = gid;
            }
            if let Some(mode) = mode {
                metadata.mode &= !Metadata::MODE_MASK;
                metadata.mode |= mode & Metadata::MODE_MASK;
            }
            common.set_metadata(self, &metadata)
        }
    }

    /// Returns the "metadata" of the file: uid, gid, access mode
    pub fn metadata(self: &NodeRef) -> Result<Metadata, Error> {
        if self.flags.contains(NodeFlags::IN_MEMORY_PROPS) {
            let mut cache = self.props.lock();

            let metadata = cache
                .metadata
                .get_or_try_insert_with(|| self.data_as_common().metadata(self))?;

            Ok(*metadata)
        } else {
            self.data_as_common().metadata(self)
        }
    }

    pub fn update_metadata(self: &NodeRef, update: &FileMetadataUpdate) -> Result<(), Error> {
        let common = self.data_as_common();

        if self.flags.contains(NodeFlags::IN_MEMORY_PROPS) {
            let mut cache = self.props.lock();

            let metadata = cache
                .metadata
                .get_or_try_insert_with(|| common.metadata(self))?;
            metadata.update(update);
            Ok(())
        } else {
            let mut metadata = common.metadata(self)?;
            metadata.update(update);
            // Update permissions in the real node
            common.set_metadata(self, &metadata)
        }
    }

    // TODO clarify directory size
    /// Returns the size in bytes of the node
    pub fn size(self: &NodeRef) -> Result<u64, Error> {
        // Try to fetch the size from the cache
        let mut props = self.props.lock();

        if self.flags.contains(NodeFlags::IN_MEMORY_SIZE) {
            if let Some(size) = props.size {
                return Ok(size);
            }

            if let Ok(dir) = self.as_directory() {
                return Ok(dir.children.lock().len() as _);
            }

            log::warn!(
                "Possible bug: node has IN_MEMORY_SIZE, but is not a directory: {:?}",
                self.ty()
            );
            Ok(0)
            // Err(Error::NotImplemented)
        } else {
            // Fetch the size from the node
            let size = self.data_as_common().size(self)?;
            props.size = Some(size);

            Ok(size)
        }
    }

    pub fn read_link(self: &NodeRef, buffer: &mut [u8]) -> Result<usize, Error> {
        let symlink = self.as_symlink()?;
        symlink.imp.read_link(buffer)
    }
}

impl DirectoryData {
    fn remove_child(&self, name: &Filename) {
        let mut children = self.children.lock();

        children.retain(|(t, child)| {
            if t == name {
                *child.parent.lock() = None;
                false
            } else {
                true
            }
        });
    }
}
