/*
 * Copyright (c) 2022, Umut İnan Erdoğan <umutinanerdogan@pm.me>
 *
 * SPDX-License-Identifier: MPL-2.0
 */

use alloc::sync::{Arc, Weak};
use ext2::fs::{sync::Synced, Ext2};
use ext2::sector::SectorSize;
use ext2::sys::inode::TypePerm;
use ext2::volume::Volume;

use crate::handle::{Handle, HandleResource};

use super::errors::FsError;
use super::vfs::{DirectoryEntry, FsFlags, VFS};
use super::{FsNode, FsResult as Result, StorageDevice};

pub struct Ext2StorageDevice<S, V>
where
    S: SectorSize,
    V: Volume<u8, S>,
{
    fs: Synced<Ext2<S, V>>,
    device_handle: Handle,
    root: Arc<FsNode>,
}

impl<S, V> Ext2StorageDevice<S, V>
where
    S: SectorSize,
    V: Volume<u8, S>,
{
    pub fn new(volume: V) -> Result<Self> {
        let fs = Synced::new(volume).map_err(|e| e.into())?;
        let root_inode = fs.root_inode();
        let device_handle = Handle::new(HandleResource::StorageDevice);
        let root = Arc::new(FsNode::new(
            FsFlags::DIRECTORY,
            root_inode.size(),
            2,
            device_handle,
            Weak::new(),
        ));

        let mut vfs = VFS.lock();
        vfs.add_fs_node(root.clone());

        Ok(Self {
            fs,
            device_handle,
            root,
        })
    }
}

impl<S, V> StorageDevice for Ext2StorageDevice<S, V>
where
    S: SectorSize + Send,
    V: Volume<u8, S> + Send,
{
    fn read(&self, node: &FsNode, offset: usize, size: usize, buffer: &mut Vec<u8>) -> Result<()> {
        let inode = self
            .fs
            .inode_nth(node.inode() as usize)
            .ok_or_else(|| FsError::InodeNotFound)?;

        // FIXME: I don't really know how Ext2 works, so for now we don't
        //        support non-zero offsets and buffer sizes that don't match
        //        the file length. We always read the whole file.
        if offset > 0 || size != inode.size() {
            Err(FsError::UnsupportedOperation)?;
        }

        inode.read_to_end(buffer).map_err(|e| e.into())?;
        Ok(())
    }

    fn write(&self, _node: &FsNode, _offset: usize, _buffer: &[u8]) -> Result<()> {
        todo!()
    }

    fn read_dir(&self, node: &FsNode, index: usize) -> Result<DirectoryEntry> {
        let inode = self
            .fs
            .inode_nth(node.inode() as usize)
            .ok_or_else(|| FsError::InodeNotFound)?;
        let mut dir = inode.directory().ok_or_else(|| FsError::NotADirectory)?;
        let entry = dir
            .nth(index)
            .ok_or_else(|| FsError::InodeNotFound)?
            .map_err(|e| e.into())?;
        let entry_inode = self
            .fs
            .inode_nth(entry.inode)
            .ok_or_else(|| FsError::InodeNotFound)?;
        let mut vfs = VFS.lock();
        let entry_node_handle = vfs
            .find_fs_node(entry.inode, self.device_handle)
            .unwrap_or_else(|| {
                vfs.add_fs_node(Arc::new(FsNode::new(
                    ext2_type_to_fs_flags(entry_inode.type_perm()),
                    inode.size(),
                    entry.inode,
                    self.device_handle,
                    Weak::new(),
                )))
            });

        Ok(DirectoryEntry::new(
            entry.file_name_string(),
            entry_node_handle,
        ))
    }

    fn find_dir(&self, node: &FsNode, name: &str) -> Result<Handle> {
        let inode = self
            .fs
            .inode_nth(node.inode() as usize)
            .ok_or_else(|| FsError::InodeNotFound)?;
        let dir = inode.directory().ok_or_else(|| FsError::NotADirectory)?;
        let mut found_node = Err(FsError::NotFound);
        let mut vfs = VFS.lock();
        for entry in dir {
            if entry.is_err() {
                continue;
            }

            let entry = entry.unwrap();
            if entry.file_name_string() == name {
                found_node = Ok(vfs
                    .find_fs_node(entry.inode, self.device_handle)
                    .unwrap_or_else(|| {
                        vfs.add_fs_node(Arc::new(FsNode::new(
                            ext2_type_to_fs_flags(inode.type_perm()),
                            inode.size(),
                            entry.inode,
                            self.device_handle,
                            Weak::new(),
                        )))
                    }));
            }
        }

        found_node
    }

    fn root_node(&self) -> Arc<FsNode> {
        self.root.clone()
    }

    fn device_handle(&self) -> Handle {
        self.device_handle
    }
}

fn ext2_type_to_fs_flags(type_perm: TypePerm) -> FsFlags {
    let is_directory = type_perm & TypePerm::DIRECTORY == TypePerm::DIRECTORY;
    let is_symlink = type_perm & TypePerm::SYMLINK == TypePerm::SYMLINK;
    let t = if is_directory {
        FsFlags::DIRECTORY
    } else {
        FsFlags::FILE
    };
    let s = if is_symlink {
        FsFlags::SYMBOLIC_LINK
    } else {
        FsFlags::empty()
    };

    t | s
}
