use core::{any::Any, fmt};

use alloc::{
    boxed::Box,
    string::String,
    sync::{Arc, Weak},
    vec::Vec,
};
use libk_util::{
    ext::OptionExt,
    sync::{spin_rwlock::IrqSafeRwLock, IrqSafeSpinlock},
};
use traits::HardlinkImpl;
use yggdrasil_abi::{
    bitflags,
    error::Error,
    io::{FileMetadataUpdate, FileMetadataUpdateMode, FileMode, FileType, GroupId, UserId},
    path::Path,
    time::SystemTime,
};

mod access;
pub mod impls;
mod traits;

// Node is implemented in the following modules
mod ops;
mod tree;

pub use access::AccessToken;
pub use traits::{CommonImpl, DirectoryImpl, RegularImpl, SymlinkImpl};

use crate::{
    device::{
        block::{BlockDevice, BlockDeviceFile},
        char::{CharDevice, CharDeviceFile},
    },
    time::real_time,
};

use super::{
    path::OwnedFilename,
    pty::{PseudoTerminalMaster, PseudoTerminalSlave},
    Filename, Filesystem,
};

/// Wrapper type for a [Node] shared reference
pub type NodeRef = Arc<Node>;

bitflags! {
    #[doc = "Describes additional flags for the node"]
    pub struct NodeFlags: u32 {
        #[doc = "Node's metadata only exists within the VFS cache and should not be fetched"]
        const IN_MEMORY_PROPS: bit 0;
        #[doc = "Node's size only exists within the VFS cache"]
        const IN_MEMORY_SIZE: bit 1;
        #[doc = "Don't cache symlink targets"]
        const NO_LINK_CACHE: bit 2;
    }
}

#[derive(Debug, Clone)]
pub enum CreateFileType<'a> {
    Symlink(&'a Path),
    Directory,
    File,
}

/// Information used when creating an entry within a directory
#[derive(Debug, Clone)]
pub struct CreateInfo<'a> {
    /// New entry name
    pub name: &'a Filename,
    /// User ID of the entry
    pub uid: UserId,
    /// Group ID of the entry
    pub gid: GroupId,
    /// Access mode of the entry
    pub mode: FileMode,
    /// Node type to be created
    pub ty: CreateFileType<'a>,
}

pub(crate) struct DirectoryData {
    pub(crate) imp: Box<dyn DirectoryImpl>,
    pub(crate) mountpoint: IrqSafeSpinlock<Option<NodeRef>>,
    pub(crate) children: IrqSafeSpinlock<Vec<(OwnedFilename, NodeRef)>>,
}

pub(crate) struct SymlinkData {
    // Cached symlink target with the literal path
    pub(crate) target: IrqSafeRwLock<Option<String>>,
    pub(crate) imp: Box<dyn SymlinkImpl>,
}

pub(crate) struct HardlinkData {
    pub(crate) imp: Box<dyn HardlinkImpl>,
}

enum NodeImpl {
    Regular(Box<dyn RegularImpl>),
    Directory(DirectoryData),
    Block(BlockDeviceFile),
    Char(CharDeviceFile),
    Symlink(SymlinkData),
    Hardlink(HardlinkData),
    // These map transparently to other types of nodes
    // TODO: implement open operation on these
    #[allow(unused)]
    PseudoTerminalSlave(Weak<PseudoTerminalSlave>),
    #[allow(unused)]
    PseudoTerminalMaster(Weak<PseudoTerminalMaster>),
}

/// Metadata of the node
#[derive(Clone, Copy)]
pub struct Metadata {
    /// User ID of the node
    pub uid: UserId,
    /// Group ID of the node
    pub gid: GroupId,
    /// Access mode of the node
    pub mode: FileMode,
    /// Filesystem-specific node number
    pub inode: Option<u32>,
    /// Filesystem-specific block size
    pub block_size: u64,
    /// Size of the node (without metadata) in units of `block_size`
    pub block_count: u64,
    /// Creation time
    pub ctime: SystemTime,
    /// Modification time
    pub mtime: SystemTime,
    /// Last access time
    pub atime: SystemTime,
}

struct PropertyCache {
    metadata: Option<Metadata>,
    size: Option<u64>,
}

/// Decribes a single entry within a filesytem
pub struct Node {
    data: NodeImpl,
    flags: NodeFlags,
    filesystem: Option<Arc<dyn Filesystem>>,
    props: IrqSafeSpinlock<PropertyCache>,
    parent: IrqSafeSpinlock<Option<NodeRef>>,
}

impl Metadata {
    pub const MODE_MASK: FileMode = FileMode::new(0o777);

    pub fn now(uid: UserId, gid: GroupId, mode: FileMode, ino: u32) -> Metadata {
        let now = real_time();
        Metadata {
            mode,
            uid,
            gid,
            ctime: now,
            mtime: now,
            atime: now,
            inode: Some(ino),
            block_size: 4096,
            block_count: 0,
        }
    }

    pub fn now_root(mode: FileMode, ino: u32) -> Metadata {
        Self::now(UserId::root(), GroupId::root(), mode, ino)
    }

    pub fn set_atime_now(&mut self) {
        self.atime = real_time();
    }

    pub fn set_mtime_now(&mut self) {
        let t = real_time();
        self.mtime = t;
        self.atime = t;
    }

    pub fn update(&mut self, update: &FileMetadataUpdate) {
        match update {
            FileMetadataUpdate::Times(_) => todo!(),
            FileMetadataUpdate::Permissions(mode) => match *mode {
                FileMetadataUpdateMode::Set(mode) => {
                    self.mode &= !Self::MODE_MASK;
                    self.mode |= mode & Self::MODE_MASK;
                }
                FileMetadataUpdateMode::Modify { set, clear } => {
                    self.mode &= !(clear & Self::MODE_MASK);
                    self.mode |= set & Self::MODE_MASK;
                }
            },
        }
    }
}

impl Node {
    fn new(
        data: NodeImpl,
        flags: NodeFlags,
        metadata: Option<Metadata>,
        filesystem: Option<Arc<dyn Filesystem>>,
    ) -> NodeRef {
        Arc::new(Self {
            data,
            flags,
            props: IrqSafeSpinlock::new(PropertyCache {
                metadata,
                size: None,
            }),
            filesystem,
            parent: IrqSafeSpinlock::new(None),
        })
    }

    pub(crate) fn pseudo_terminal_nodes(
        master: &Arc<PseudoTerminalMaster>,
        slave: &Arc<PseudoTerminalSlave>,
        metadata: Metadata,
    ) -> (NodeRef, NodeRef) {
        let master = Self::new(
            NodeImpl::PseudoTerminalMaster(Arc::downgrade(master)),
            NodeFlags::IN_MEMORY_PROPS | NodeFlags::IN_MEMORY_SIZE,
            Some(metadata),
            None,
        );
        let slave = Self::new(
            NodeImpl::PseudoTerminalSlave(Arc::downgrade(slave)),
            NodeFlags::IN_MEMORY_PROPS | NodeFlags::IN_MEMORY_SIZE,
            Some(metadata),
            None,
        );

        master.props.lock().size = Some(0);
        slave.props.lock().size = Some(0);

        (master, slave)
    }

    /// Creates a new directory node with given [DirectoryImpl] and permissions
    pub fn directory<T: DirectoryImpl + 'static>(
        data: T,
        flags: NodeFlags,
        metadata: Option<Metadata>,
        filesystem: Option<Arc<dyn Filesystem>>,
    ) -> NodeRef {
        let data = NodeImpl::Directory(DirectoryData {
            imp: Box::new(data),
            mountpoint: IrqSafeSpinlock::new(None),
            children: IrqSafeSpinlock::new(Vec::new()),
        });
        Self::new(data, flags, metadata, filesystem)
    }

    // /// Creates a new directory node with given [DirectoryImpl]
    // pub fn directory<T: DirectoryImpl + 'static>(data: T, flags: NodeFlags) -> NodeRef {
    //     Self::directory_kernel(data, flags, FileMode::default_dir())
    // }

    /// Creates a new file node with given [RegularImpl] and permissions
    pub fn regular<T: RegularImpl + 'static>(
        data: T,
        flags: NodeFlags,
        metadata: Option<Metadata>,
        filesystem: Option<Arc<dyn Filesystem>>,
    ) -> NodeRef {
        Self::new(
            NodeImpl::Regular(Box::new(data)),
            flags,
            metadata,
            filesystem,
        )
    }

    // /// Creates a new file node with given [RegularImpl]
    // pub fn regular<T: RegularImpl + 'static>(data: T, flags: NodeFlags) -> NodeRef {
    //     Self::regular_kernel(data, flags, FileMode::default_file())
    // }

    /// Creates a new block device node with given [BlockDevice]
    pub fn block(
        device: Arc<dyn BlockDevice>,
        flags: NodeFlags,
        metadata: Metadata,
        filesystem: Option<Arc<dyn Filesystem>>,
    ) -> NodeRef {
        Self::new(
            NodeImpl::Block(BlockDeviceFile(device)),
            flags,
            Some(metadata),
            filesystem,
        )
    }

    /// Creates a new character device node with given [CharDevice]
    pub fn char(
        device: Arc<dyn CharDevice>,
        flags: NodeFlags,
        metadata: Metadata,
        filesystem: Option<Arc<dyn Filesystem>>,
    ) -> NodeRef {
        Self::new(
            NodeImpl::Char(CharDeviceFile(device)),
            flags,
            Some(metadata),
            filesystem,
        )
    }

    /// Creates a new symbolic link node with given [SymlinkImpl]
    pub fn symlink<T: SymlinkImpl + 'static>(
        data: T,
        flags: NodeFlags,
        metadata: Option<Metadata>,
        filesystem: Option<Arc<dyn Filesystem>>,
    ) -> NodeRef {
        Self::new(
            NodeImpl::Symlink(SymlinkData {
                target: IrqSafeRwLock::new(None),
                imp: Box::new(data),
            }),
            flags,
            metadata,
            filesystem,
        )
    }

    /// Creates a new hard link node pointing to another [Node]
    pub fn hardlink<T: HardlinkImpl + 'static>(data: T) -> NodeRef {
        Self::new(
            NodeImpl::Hardlink(HardlinkData {
                imp: Box::new(data),
            }),
            NodeFlags::IN_MEMORY_SIZE,
            None,
            None,
        )
    }

    /// Unwraps a hard link chain to its final target
    pub fn flatten_hardlink(self: &Arc<Self>) -> Result<Arc<Self>, Error> {
        let mut node = self.clone();
        while let Some(target) = node.read_hardlink()? {
            node = target;
        }
        Ok(node)
    }

    pub fn read_hardlink(self: &Arc<Self>) -> Result<Option<NodeRef>, Error> {
        match &self.data {
            NodeImpl::Hardlink(data) => data.target(self).map(Some),
            _ => Ok(None),
        }
    }

    /// Returns a [Filesystem] this node belongs to.
    pub fn filesystem(&self) -> Option<Arc<dyn Filesystem>> {
        self.filesystem.clone()
    }

    /// Returns the impl data of the node as `dyn Any`
    pub fn data_as_any(&self) -> &dyn Any {
        match &self.data {
            NodeImpl::Directory(dir) => dir.imp.as_any(),
            NodeImpl::Regular(imp) => imp.as_any(),
            NodeImpl::Block(w) => w.as_any(),
            NodeImpl::Char(w) => w.as_any(),
            NodeImpl::Symlink(w) => w.imp.as_any(),
            NodeImpl::Hardlink(_) => unreachable!("BUG: Hard links cannot be referenced directly"),
            NodeImpl::PseudoTerminalSlave(_) | NodeImpl::PseudoTerminalMaster(_) => todo!(),
        }
    }

    /// Returns the impl data of the node as `dyn CommonImpl`
    pub fn data_as_common(&self) -> &dyn CommonImpl {
        match &self.data {
            NodeImpl::Directory(dir) => dir.imp.as_ref(),
            NodeImpl::Regular(imp) => imp.as_ref(),
            NodeImpl::Block(w) => w,
            NodeImpl::Char(w) => w,
            NodeImpl::Symlink(w) => w.imp.as_ref(),
            NodeImpl::Hardlink(_) => unreachable!("BUG: Hard links cannot be referenced directly"),
            NodeImpl::PseudoTerminalSlave(_) | NodeImpl::PseudoTerminalMaster(_) => todo!(),
        }
    }

    /// Attempts to cast the impl data of the node to `&T`
    pub fn data_as_ref<T: 'static>(&self) -> &T {
        self.data_as_any().downcast_ref().unwrap()
    }

    /// Returns the type of the node
    pub fn ty(&self) -> FileType {
        match &self.data {
            NodeImpl::Regular(_) => FileType::File,
            NodeImpl::Directory(_) => FileType::Directory,
            NodeImpl::Block(_) => FileType::Block,
            NodeImpl::Char(_) => FileType::Char,
            NodeImpl::Symlink(_) => FileType::Symlink,
            NodeImpl::Hardlink(_) => unreachable!("BUG: Hard links cannot be referenced directly"),
            NodeImpl::PseudoTerminalSlave(_) | NodeImpl::PseudoTerminalMaster(_) => FileType::Char,
        }
    }

    /// Returns `true` if the node represents a directory
    pub fn is_directory(&self) -> bool {
        matches!(&self.data, NodeImpl::Directory(_))
    }

    /// Returns `true` if the node represents a hard link to another node
    pub fn is_hard_link(&self) -> bool {
        matches!(&self.data, NodeImpl::Hardlink(_))
    }

    /// Returns `true` if the node represents a character device and the character device is a
    /// terminal
    pub fn is_terminal(&self) -> bool {
        match &self.data {
            NodeImpl::Char(dev) => dev.is_terminal(),
            NodeImpl::PseudoTerminalSlave(_) => true,
            NodeImpl::PseudoTerminalMaster(_) => true,
            _ => false,
        }
    }

    pub(crate) fn is_same_filesystem(a: &Self, b: &Self) -> bool {
        if let (Some(fs_a), Some(fs_b)) = (a.filesystem.as_ref(), b.filesystem.as_ref()) {
            Arc::ptr_eq(fs_a, fs_b)
        } else {
            false
        }
    }

    pub(crate) fn mountpoint_target(&self) -> Option<NodeRef> {
        match &self.data {
            NodeImpl::Directory(dir) => dir.mountpoint.lock().clone(),
            _ => None,
        }
    }

    pub(crate) fn mount<F: FnOnce() -> Result<NodeRef, Error>>(
        self: &NodeRef,
        create_fs: F,
    ) -> Result<(), Error> {
        let directory = self.as_directory()?;
        let mut mountpoint = directory.mountpoint.lock();
        if mountpoint.is_some() {
            // TODO Busy
            todo!();
        }

        let fs_root = create_fs()?;
        assert!(fs_root.is_directory());
        let mut parent = fs_root.parent.lock();
        if parent.is_some() {
            return Err(Error::InvalidOperation);
        }

        mountpoint.replace(fs_root.clone());
        parent.replace(self.clone());

        Ok(())
    }

    pub(crate) fn as_directory(&self) -> Result<&DirectoryData, Error> {
        match &self.data {
            NodeImpl::Directory(dir) => Ok(dir),
            _ => Err(Error::NotADirectory),
        }
    }

    pub(crate) fn as_regular(&self) -> Result<&dyn RegularImpl, Error> {
        match &self.data {
            NodeImpl::Regular(imp) => Ok(imp.as_ref()),
            _ => Err(Error::InvalidFile),
        }
    }

    pub(crate) fn as_symlink(&self) -> Result<&SymlinkData, Error> {
        match &self.data {
            NodeImpl::Symlink(imp) => Ok(imp),
            _ => Err(Error::InvalidFile),
        }
    }

    pub(crate) fn read_symlink_path(self: &NodeRef, _token: AccessToken) -> Result<String, Error> {
        let symlink = self.as_symlink()?;
        let mut target = symlink.target.write();
        target
            .get_or_try_insert_with(|| symlink.imp.read_to_string())
            .cloned()
    }

    pub fn as_block_device(&self) -> Result<&Arc<dyn BlockDevice>, Error> {
        match &self.data {
            NodeImpl::Block(dev) => Ok(&dev.0),
            _ => Err(Error::InvalidFile),
        }
    }
}

impl DirectoryData {
    fn contains(&self, node: &NodeRef, name: &Filename) -> bool {
        {
            let cache = self.children.lock();
            if cache.iter().any(|(n, _)| n == name) {
                return true;
            }
        }
        self.imp.contains(node, name)
    }
}

impl HardlinkData {
    fn target(&self, link: &NodeRef) -> Result<NodeRef, Error> {
        let target = self.imp.target(link)?;
        // Not allowed to point to other links or directories
        debug_assert!(!target.is_hard_link());
        debug_assert!(!matches!(
            target.ty(),
            FileType::Directory | FileType::Symlink
        ));
        Ok(target)
    }
}

impl fmt::Debug for Node {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self.data {
            NodeImpl::Directory(_) => f.debug_struct("DirectoryNode").finish_non_exhaustive(),
            NodeImpl::Regular(_) => f.debug_struct("RegularNode").finish_non_exhaustive(),
            NodeImpl::Char(_) => f.debug_struct("CharNode").finish_non_exhaustive(),
            NodeImpl::Block(_) => f.debug_struct("BlockNode").finish_non_exhaustive(),
            NodeImpl::Symlink(_) => f.debug_struct("SymlinkNode").finish_non_exhaustive(),
            NodeImpl::Hardlink(_) => f.debug_struct("HardlinkNode").finish_non_exhaustive(),
            NodeImpl::PseudoTerminalSlave(_) => f
                .debug_struct("PseudoTerminalSlave")
                .finish_non_exhaustive(),
            NodeImpl::PseudoTerminalMaster(_) => f
                .debug_struct("PseudoTerminalMaster")
                .finish_non_exhaustive(),
        }
    }
}

#[cfg(test)]
mod tests {
    use core::any::Any;
    use std::sync::Arc;

    use crate::vfs::{node::NodeFlags, Filename};

    use super::{CommonImpl, DirectoryImpl, Node, RegularImpl};

    struct DummyDirectory;
    struct DummyFile;

    impl CommonImpl for DummyDirectory {}
    impl DirectoryImpl for DummyDirectory {}

    impl CommonImpl for DummyFile {}
    impl RegularImpl for DummyFile {}

    #[test]
    fn node_sync_send() {
        fn node_send<T: Send>(_n: &T) {}

        let node = Node::regular(DummyFile, NodeFlags::empty(), None, None);

        node_send(&node);
    }

    #[test]
    fn dir_cache_add() {
        let d1 = Node::directory(DummyDirectory, NodeFlags::empty(), None, None);
        let d2 = Node::directory(DummyDirectory, NodeFlags::empty(), None, None);
        let f1 = Node::regular(DummyFile, NodeFlags::empty(), None, None);

        assert!(Arc::ptr_eq(&f1.parent(), &f1));
        assert_eq!(d1.children_len().unwrap(), 0);

        d1.add_child(Filename::new("f1").unwrap(), f1.clone())
            .unwrap();
        assert!(Arc::ptr_eq(&f1.parent(), &d1));
        assert_eq!(d1.children_len().unwrap(), 1);

        assert!(Arc::ptr_eq(&d2.parent(), &d2));
        d2.add_child(Filename::new("d1").unwrap(), d1.clone())
            .unwrap();
        assert!(Arc::ptr_eq(&f1.parent(), &d1));
        assert!(Arc::ptr_eq(&d1.parent(), &d2));
        assert_eq!(d1.children_len().unwrap(), 1);
        assert_eq!(d2.children_len().unwrap(), 1);
    }

    #[test]
    fn in_mem_dir_size_coherence() {
        let d = Node::directory(DummyDirectory, NodeFlags::IN_MEMORY_SIZE, None, None);

        for i in 0..10 {
            let name = format!("f{}", i);
            let node = Node::regular(DummyFile, NodeFlags::empty(), None, None);

            d.add_child(Filename::new(&name).unwrap(), node).unwrap();
            assert_eq!(d.size().unwrap(), d.children_len().unwrap() as u64);
        }
    }

    #[test]
    fn data_any() {
        struct AnyData {
            value: u32,
        }

        impl CommonImpl for AnyData {
            fn as_any(&self) -> &dyn Any {
                self
            }
        }
        impl DirectoryImpl for AnyData {}

        let d = Node::directory(AnyData { value: 1234 }, NodeFlags::empty(), None, None);
        let r = d.data_as_ref::<AnyData>();

        assert_eq!(r.value, 1234);
    }
}
