//! Filesystem implementations

use core::ptr::NonNull;

use abi::{io::filesystem::FilesystemRequestVariant, path::Path};
use alloc::sync::Arc;
use ext2::Ext2Fs;
use libk::{
    block,
    fs::{devfs, sysfs},
    vfs::{self, register_root, Filesystem, FilesystemMountOption, IoContext, NodeRef},
};
use libk_mm::{
    address::{PhysicalAddress, Virtualize},
    phys,
};
use libk_util::OneTimeInit;
use memfs::block::{self, BlockAllocator};
// use memfs::block::{self, BlockAllocator};
use static_assertions::const_assert_eq;
use ygg_driver_fat32::Fat32Fs;
use yggdrasil_abi::{error::Error, io::MountOptions};

pub use pseudo::add_pseudo_devices;

pub mod pseudo;
// pub mod sysfs;

/// Describes in-memory filesystem image used as initial root
pub struct Initrd {
    /// Page-aligned start address of the initrd
    pub phys_page_start: PhysicalAddress,
    /// Page-aligned length
    pub phys_page_len: usize,
    /// Safe reference to the initrd data slice
    pub data: &'static [u8],
}

/// Holds reference to the data of initrd as well as its page-aligned physical memory region
pub static INITRD_DATA: OneTimeInit<Initrd> = OneTimeInit::new();

/// Implementation of [memfs::block::BlockAllocator] for the kernel
pub struct FileBlockAllocator;

const_assert_eq!(block::SIZE, 4096);

unsafe impl BlockAllocator for FileBlockAllocator {
    fn alloc() -> Result<NonNull<u8>, Error> {
        let page = phys::alloc_page()?;
        Ok(unsafe { NonNull::new_unchecked(page.virtualize() as *mut _) })
    }

    unsafe fn dealloc(block: NonNull<u8>) {
        let page = block.as_ptr() as usize;
        let physical = PhysicalAddress::from_virtualized(page);
        phys::free_page(physical);
    }
}

/// Mounts an instance of a filesystem for given set of [MountOptions].
pub fn mount_filesystem(ioctx: &mut IoContext, options: &MountOptions) -> Result<(), Error> {
    let Some(fs_name) = options.filesystem else {
        log::warn!("TODO: mount without filesystem type/fs probing not yet implemented");
        return Err(Error::NotImplemented);
    };

    let source = options.source.map(|path| {
        let path = Path::from_str(path);
        if !path.is_absolute() {
            todo!();
        }
        ioctx.find(None, path, true)
    });
    let target = options.target;
    let options = vfs::parse_mount_options(options.options);

    ioctx.mount(target, move || create_filesystem(fs_name, source, options))
}

fn create_filesystem<'a, I: IntoIterator<Item = FilesystemMountOption<'a>>>(
    fs_name: &str,
    source: Option<Result<NodeRef, Error>>,
    options: I,
) -> Result<NodeRef, Error> {
    let root = match fs_name {
        "devfs" => devfs::root().clone(),
        "sysfs" => sysfs::root().clone(),
        "ext2" if let Some(source) = source => {
            let source = source?;
            let device = source.as_block_device()?;
            block!(Ext2Fs::create(device.clone(), options).await)??
        }
        "fat32" if let Some(source) = source => {
            let source = source?;
            let device = source.as_block_device()?;
            block!(Fat32Fs::create(device.clone(), options).await)??
        }
        _ => return Err(Error::InvalidArgument),
    };

    register_root(root.clone());

    Ok(root)
}

/// Global filesystem control request
pub fn global_control(option: u32, buffer: &mut [u8], len: usize) -> Result<usize, Error> {
    let _ = buffer;
    let _ = len;

    let option = FilesystemRequestVariant::try_from(option)?;
    match option {
        FilesystemRequestVariant::FlushCache => {
            block! {
                let roots = vfs::filesystem_roots();
                for root in roots.read().iter() {
                    if let Some(fs) = root.filesystem() {
                        if let Err(error) = fs.flush().await {
                            log::error!("{:?} flush: {error:?}", fs.display_name());
                        }
                    }
                }
            }?;

            Ok(0)
        }
    }
}

/// Specific filesystem control request
pub fn filesystem_control(
    fs: Arc<dyn Filesystem>,
    option: u32,
    buffer: &mut [u8],
    len: usize,
) -> Result<usize, Error> {
    let _ = buffer;
    let _ = len;

    if let Ok(option) = FilesystemRequestVariant::try_from(option) {
        match option {
            FilesystemRequestVariant::FlushCache => {
                block!(fs.flush().await)??;
                return Ok(0);
            }
        }
    }

    todo!()
}
