//! In-memory filesystem driver
#![no_std]
#![deny(missing_docs)]
#![allow(clippy::new_without_default, clippy::new_ret_no_self)]
#![feature(maybe_uninit_array_assume_init)]

use core::{
    marker::PhantomData,
    sync::atomic::{AtomicU32, Ordering},
};

use alloc::sync::Arc;
use block::BlockAllocator;
use dir::DirectoryNode;
use file::FileNode;
use libk::vfs::{impls::fixed_path_symlink, AccessToken, Filename, Filesystem, Metadata, NodeRef};
use libk_util::sync::IrqSafeSpinlock;
use tar::TarEntry;
use yggdrasil_abi::{
    error::Error,
    io::{FileMode, FileType, GroupId, UserId},
    path::Path,
    time::SystemTime,
};

use crate::tar::TarIterator;

#[cfg(test)]
extern crate std;

extern crate alloc;

#[cfg(test)]
macro_rules! test_allocator_with_counter {
    ($counter:ident, $allocator:ident) => {
        static $counter: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);

        struct $allocator;

        unsafe impl $crate::block::BlockAllocator for $allocator {
            fn alloc() -> Result<core::ptr::NonNull<u8>, yggdrasil_abi::error::Error> {
                let b = std::boxed::Box::into_raw(std::boxed::Box::new([0; $crate::block::SIZE]));
                $counter.fetch_add(1, core::sync::atomic::Ordering::Release);
                Ok(unsafe { core::ptr::NonNull::new_unchecked(b as _) })
            }

            unsafe fn dealloc(block: core::ptr::NonNull<u8>) {
                $counter.fetch_sub(1, core::sync::atomic::Ordering::Release);
                drop(std::boxed::Box::from_raw(
                    block.as_ptr() as *mut [u8; $crate::block::SIZE]
                ));
            }
        }
    };
}

pub mod block;
pub mod bvec;

mod dir;
mod file;
mod tar;

static INO_COUNTER: AtomicU32 = AtomicU32::new(1);

/// In-memory read/write filesystem
pub struct MemoryFilesystem<A: BlockAllocator> {
    root: IrqSafeSpinlock<Option<NodeRef>>,
    _pd: PhantomData<A>,
}

impl<A: BlockAllocator> MemoryFilesystem<A> {
    fn make_path(
        self: &Arc<Self>,
        at: &NodeRef,
        path: &Path,
        create: bool,
        mode: FileMode,
        mtime: SystemTime,
    ) -> Result<NodeRef, Error> {
        let access = unsafe { AccessToken::authorized() };
        if path.is_empty() {
            return Ok(at.clone());
        }
        let (element, rest) = path.split_left();
        // let (element, rest) = path::split_left(path);
        assert!(!element.is_empty());
        assert!(!element.contains('/'));

        let filename = Filename::new(element)?;

        // let node = at.lookup(element);
        let node = at.lookup_or_load(filename, access);
        let node = match node {
            Ok(node) => node,
            Err(Error::DoesNotExist) => {
                if !create {
                    return Err(Error::DoesNotExist);
                }

                let ino = INO_COUNTER.fetch_add(1, Ordering::Relaxed);
                let metadata = Metadata {
                    uid: UserId::root(),
                    gid: GroupId::root(),
                    atime: mtime,
                    ctime: mtime,
                    mode,
                    mtime,
                    inode: Some(ino),
                    block_size: 512,
                    block_count: 0,
                };
                let node = DirectoryNode::<A>::new(self.clone(), metadata);
                at.add_child(filename, node.clone())?;

                node
            }
            Err(err) => {
                log::warn!("{:?}: lookup failed: {:?}", path, err);
                return Err(err);
            }
        };

        if rest.is_empty() {
            Ok(node)
        } else {
            assert!(node.is_directory());
            self.make_path(&node, rest, create, mode, mtime)
        }
    }

    fn create_node_initial(self: &Arc<Self>, hdr: &TarEntry) -> Result<NodeRef, Error> {
        let kind = hdr.node_kind();
        let mode = usize::from(&hdr.mode);
        let mode = FileMode::new(0o777 & (mode as u32));
        let mtime = SystemTime::new(usize::from(&hdr.mtime) as u64, 0);
        let ino = INO_COUNTER.fetch_add(1, Ordering::Relaxed);
        let metadata = Metadata {
            uid: UserId::root(),
            gid: GroupId::root(),
            atime: mtime,
            ctime: mtime,
            mode,
            mtime,
            inode: Some(ino),
            block_size: 512,
            block_count: usize::from(&hdr.size).div_ceil(512) as u64,
        };
        match kind {
            FileType::File => Ok(FileNode::<A>::new(self.clone(), metadata)),
            FileType::Directory => Ok(DirectoryNode::<A>::new(self.clone(), metadata)),
            FileType::Symlink => {
                let target = hdr.symlink_target()?;
                Ok(fixed_path_symlink(target))
            }
            _ => todo!(),
        }
    }

    fn from_slice_internal(self: &Arc<Self>, tar_data: &'static [u8]) -> Result<NodeRef, Error> {
        let root =
            DirectoryNode::<A>::new(self.clone(), Metadata::now_root(FileMode::new(0o755), 0));

        // 1. Create paths in tar
        for item in TarIterator::new(tar_data) {
            let Ok((hdr, _)) = item else {
                return Err(Error::InvalidArgument);
            };

            let path = Path::from_str(hdr.name.as_str()?.trim_matches('/'));
            let mtime = SystemTime::new(usize::from(&hdr.mtime) as u64, 0);

            let (dirname, filename) = path.split_right();
            let parent = self.make_path(&root, dirname, true, FileMode::new(0o755), mtime)?;
            let node = self.create_node_initial(hdr)?;
            let filename = Filename::new(filename)?;

            parent.add_child(filename, node)?;
        }

        // 2. Associate files with their data
        for item in TarIterator::new(tar_data) {
            let Ok((hdr, data)) = item else {
                panic!("Unreachable");
            };

            let mtime = SystemTime::new(usize::from(&hdr.mtime) as u64, 0);
            let path = Path::from_str(hdr.name.as_str()?.trim_matches('/'));
            let node = self.make_path(&root, path, false, FileMode::empty(), mtime)?;
            assert_eq!(node.ty(), hdr.node_kind());

            let uid = unsafe { UserId::from_raw(usize::from(&hdr.uid) as u32) };
            let gid = unsafe { GroupId::from_raw(usize::from(&hdr.gid) as u32) };
            let mode = convert_mode(usize::from(&hdr.mode))?;

            let access = unsafe { AccessToken::authorized() };
            node.set_access(Some(uid), Some(gid), Some(mode), access)?;

            if hdr.node_kind() == FileType::File {
                let data = data.unwrap();
                let node_data = node.data_as_ref::<FileNode<A>>();
                let mut bvec = node_data.data.lock();

                bvec.init_with_cow(data)?;
                assert_eq!(bvec.size(), data.len());
            }
        }

        Ok(root)
    }

    /// Constructs a filesystem tree from a tar image in memory
    pub fn from_slice(tar_data: &'static [u8]) -> Result<Arc<Self>, Error> {
        let fs = Arc::new(Self {
            root: IrqSafeSpinlock::new(None),
            _pd: PhantomData,
        });
        let root = fs.from_slice_internal(tar_data)?;
        fs.root.replace(Some(root));

        Ok(fs)
    }

    // TODO Filesystem trait?
    /// Returns the root node of the memory filesystem
    pub fn root(&self) -> Result<NodeRef, Error> {
        Ok(self.root.lock().clone().unwrap())
    }
}

impl<A: BlockAllocator> Filesystem for MemoryFilesystem<A> {
    fn display_name(&self) -> &'static str {
        "memfs"
    }
}

fn convert_mode(mode: usize) -> Result<FileMode, Error> {
    Ok(FileMode::new(mode as u32 & 0o777))
}
