// SPDX-License-Identifier: MPL-2.0

#![allow(dead_code)]

use super::{
    block_group::{BlockGroup, RawGroupDescriptor},
    block_ptr::Ext2Bid,
    inode::{FilePerm, Inode, InodeDesc, RawInode},
    prelude::*,
    super_block::{RawSuperBlock, SuperBlock, SUPER_BLOCK_OFFSET},
};

/// The root inode number.
const ROOT_INO: u32 = 2;

/// The Ext2 filesystem.
#[derive(Debug)]
pub struct Ext2 {
    block_device: Arc<dyn BlockDevice>,
    super_block: RwMutex<Dirty<SuperBlock>>,
    block_groups: Vec<BlockGroup>,
    inodes_per_group: u32,
    blocks_per_group: Ext2Bid,
    inode_size: usize,
    block_size: usize,
    group_descriptors_segment: USegment,
    self_ref: Weak<Self>,
}

impl Ext2 {
    /// Opens and loads an Ext2 from the `block_device`.
    pub fn open(block_device: Arc<dyn BlockDevice>) -> Result<Arc<Self>> {
        // Load the superblock
        // TODO: if the main superblock is corrupted, should we load the backup?
        let super_block = {
            let raw_super_block = block_device.read_val::<RawSuperBlock>(SUPER_BLOCK_OFFSET)?;
            SuperBlock::try_from(raw_super_block)?
        };
        assert_eq!(
            super_block.block_size(),
            BLOCK_SIZE,
            "currently only support 4096-byte block size"
        );

        let group_descriptors_segment: USegment = {
            let npages = ((super_block.block_groups_count() as usize)
                * core::mem::size_of::<RawGroupDescriptor>())
            .div_ceil(BLOCK_SIZE);
            let segment = FrameAllocOptions::new()
                .zeroed(false)
                .alloc_segment(npages)?;
            let bio_segment =
                BioSegment::new_from_segment(segment.clone().into(), BioDirection::FromDevice);
            match block_device.read_blocks(super_block.group_descriptors_bid(0), bio_segment)? {
                BioStatus::Complete => (),
                err_status => {
                    return Err(Error::from(err_status));
                }
            }
            segment.into()
        };

        // Load the block groups information
        let load_block_groups = |fs: Weak<Ext2>,
                                 block_device: &dyn BlockDevice,
                                 group_descriptors_segment: &USegment|
         -> Result<Vec<BlockGroup>> {
            let block_groups_count = super_block.block_groups_count() as usize;
            let mut block_groups = Vec::with_capacity(block_groups_count);
            for idx in 0..block_groups_count {
                let block_group = BlockGroup::load(
                    group_descriptors_segment,
                    idx,
                    block_device,
                    &super_block,
                    fs.clone(),
                )?;
                block_groups.push(block_group);
            }
            Ok(block_groups)
        };

        let ext2 = Arc::new_cyclic(|weak_ref| Self {
            inodes_per_group: super_block.inodes_per_group(),
            blocks_per_group: super_block.blocks_per_group(),
            inode_size: super_block.inode_size(),
            block_size: super_block.block_size(),
            block_groups: load_block_groups(
                weak_ref.clone(),
                block_device.as_ref(),
                &group_descriptors_segment,
            )
            .unwrap(),
            block_device,
            super_block: RwMutex::new(Dirty::new(super_block)),
            group_descriptors_segment,
            self_ref: weak_ref.clone(),
        });
        Ok(ext2)
    }

    /// Returns the block device.
    pub fn block_device(&self) -> &dyn BlockDevice {
        self.block_device.as_ref()
    }

    /// Returns the size of block.
    pub fn block_size(&self) -> usize {
        self.block_size
    }

    /// Returns the size of inode.
    pub fn inode_size(&self) -> usize {
        self.inode_size
    }

    /// Returns the number of inodes in each block group.
    pub fn inodes_per_group(&self) -> u32 {
        self.inodes_per_group
    }

    /// Returns the number of blocks in each block group.
    pub fn blocks_per_group(&self) -> Ext2Bid {
        self.blocks_per_group
    }

    /// Returns the super block.
    pub fn super_block(&self) -> RwMutexReadGuard<Dirty<SuperBlock>> {
        self.super_block.read()
    }

    /// Returns the root inode.
    pub fn root_inode(&self) -> Result<Arc<Inode>> {
        self.lookup_inode(ROOT_INO)
    }

    /// Finds and returns the inode by `ino`.
    pub(super) fn lookup_inode(&self, ino: u32) -> Result<Arc<Inode>> {
        let (_, block_group) = self.block_group_of_ino(ino)?;
        let inode_idx = self.inode_idx(ino);
        block_group.lookup_inode(inode_idx)
    }

    /// Creates a new inode.
    pub(super) fn create_inode(
        &self,
        dir_block_group_idx: usize,
        inode_type: InodeType,
        file_perm: FilePerm,
    ) -> Result<Arc<Inode>> {
        let (block_group_idx, ino) =
            self.alloc_ino(dir_block_group_idx, inode_type == InodeType::Dir)?;
        let inode = {
            let inode_desc = InodeDesc::new(inode_type, file_perm);
            Inode::new(ino, block_group_idx, inode_desc, self.self_ref.clone())
        };
        let block_group = &self.block_groups[block_group_idx];
        block_group.insert_cache(self.inode_idx(ino), inode.clone());
        Ok(inode)
    }

    /// Allocates a new inode number, internally used by `new_inode`.
    ///
    /// Attempts to allocate from the `dir_block_group_idx` group first.
    /// If allocation is not possible from this group, then search the remaining groups.
    fn alloc_ino(&self, dir_block_group_idx: usize, is_dir: bool) -> Result<(usize, u32)> {
        let mut block_group_idx = dir_block_group_idx;
        if block_group_idx >= self.block_groups.len() {
            return_errno_with_message!(Errno::EINVAL, "invalid block group idx");
        }

        for _ in 0..self.block_groups.len() {
            if block_group_idx >= self.block_groups.len() {
                block_group_idx = 0;
            }
            let block_group = &self.block_groups[block_group_idx];
            if let Some(inode_idx) = block_group.alloc_inode(is_dir) {
                let ino = block_group_idx as u32 * self.inodes_per_group + inode_idx + 1;
                self.super_block.write().dec_free_inodes();
                return Ok((block_group_idx, ino));
            }
            block_group_idx += 1;
        }

        return_errno_with_message!(Errno::ENOSPC, "no space on device");
    }

    /// Frees an inode.
    pub(super) fn free_inode(&self, ino: u32, is_dir: bool) -> Result<()> {
        let (_, block_group) = self.block_group_of_ino(ino)?;
        let inode_idx = self.inode_idx(ino);
        // In order to prevent value underflow, it is necessary to increment
        // the free inode counter prior to freeing the inode.
        self.super_block.write().inc_free_inodes();
        block_group.free_inode(inode_idx, is_dir);
        Ok(())
    }

    /// Writes back the metadata of inode.
    pub(super) fn sync_inode(&self, ino: u32, inode: &InodeDesc) -> Result<()> {
        let (_, block_group) = self.block_group_of_ino(ino)?;
        let inode_idx = self.inode_idx(ino);
        block_group.sync_raw_inode(inode_idx, &RawInode::from(inode));
        Ok(())
    }

    /// Writes back the block group descriptor to the descriptors table.
    pub(super) fn sync_group_descriptor(
        &self,
        block_group_idx: usize,
        raw_descriptor: &RawGroupDescriptor,
    ) -> Result<()> {
        let offset = block_group_idx * core::mem::size_of::<RawGroupDescriptor>();
        self.group_descriptors_segment
            .write_val(offset, raw_descriptor)?;
        Ok(())
    }

    /// Allocates a consecutive range of blocks.
    ///
    /// The returned allocated range size may be smaller than the requested `count` if
    /// insufficient consecutive blocks are available.
    ///
    /// Attempts to allocate blocks from the `block_group_idx` group first.
    /// If allocation is not possible from this group, then search the remaining groups.
    pub(super) fn alloc_blocks(
        &self,
        mut block_group_idx: usize,
        count: Ext2Bid,
    ) -> Option<Range<Ext2Bid>> {
        if count > self.super_block.read().free_blocks_count() {
            return None;
        }

        let mut remaining_count = count;
        let mut allocated_range: Option<Range<Ext2Bid>> = None;
        for _ in 0..self.block_groups.len() {
            if remaining_count == 0 {
                break;
            }

            if block_group_idx >= self.block_groups.len() {
                block_group_idx = 0;
            }
            let block_group = &self.block_groups[block_group_idx];
            if let Some(range_in_group) = block_group.alloc_blocks(remaining_count) {
                let device_range = {
                    let start =
                        (block_group_idx as Ext2Bid) * self.blocks_per_group + range_in_group.start;
                    start..start + (range_in_group.len() as Ext2Bid)
                };
                match allocated_range {
                    Some(ref mut range) => {
                        if range.end == device_range.start {
                            // Accumulate consecutive bids
                            range.end = device_range.end;
                            remaining_count -= range_in_group.len() as Ext2Bid;
                        } else {
                            block_group.free_blocks(range_in_group);
                            break;
                        }
                    }
                    None => {
                        allocated_range = Some(device_range);
                    }
                }
            }
            block_group_idx += 1;
        }

        if let Some(range) = allocated_range.as_ref() {
            self.super_block
                .write()
                .dec_free_blocks(range.len() as Ext2Bid);
        }
        allocated_range
    }

    /// Frees a range of blocks.
    pub(super) fn free_blocks(&self, range: Range<Ext2Bid>) -> Result<()> {
        let mut current_range = range.clone();
        while !current_range.is_empty() {
            let (_, block_group) = self.block_group_of_bid(current_range.start)?;
            let range_in_group = {
                let start = self.block_idx(current_range.start);
                let len = (current_range.len() as Ext2Bid).min(self.blocks_per_group - start);
                start..start + len
            };
            // In order to prevent value underflow, it is necessary to increment
            // the free block counter prior to freeing the block.
            self.super_block
                .write()
                .inc_free_blocks(range_in_group.len() as Ext2Bid);
            block_group.free_blocks(range_in_group.clone());
            current_range.start += range_in_group.len() as Ext2Bid
        }

        Ok(())
    }

    /// Reads contiguous blocks starting from the `bid` synchronously.
    pub(super) fn read_blocks(&self, bid: Ext2Bid, bio_segment: BioSegment) -> Result<()> {
        let status = self
            .block_device
            .read_blocks(Bid::new(bid as u64), bio_segment)?;
        match status {
            BioStatus::Complete => Ok(()),
            err_status => Err(Error::from(err_status)),
        }
    }

    /// Reads contiguous blocks starting from the `bid` asynchronously.
    pub(super) fn read_blocks_async(
        &self,
        bid: Ext2Bid,
        bio_segment: BioSegment,
    ) -> Result<BioWaiter> {
        let waiter = self
            .block_device
            .read_blocks_async(Bid::new(bid as u64), bio_segment)?;
        Ok(waiter)
    }

    /// Writes contiguous blocks starting from the `bid` synchronously.
    pub(super) fn write_blocks(&self, bid: Ext2Bid, bio_segment: BioSegment) -> Result<()> {
        let status = self
            .block_device
            .write_blocks(Bid::new(bid as u64), bio_segment)?;
        match status {
            BioStatus::Complete => Ok(()),
            err_status => Err(Error::from(err_status)),
        }
    }

    /// Writes contiguous blocks starting from the `bid` asynchronously.
    pub(super) fn write_blocks_async(
        &self,
        bid: Ext2Bid,
        bio_segment: BioSegment,
    ) -> Result<BioWaiter> {
        let waiter = self
            .block_device
            .write_blocks_async(Bid::new(bid as u64), bio_segment)?;
        Ok(waiter)
    }

    /// Writes back the metadata to the block device.
    pub fn sync_metadata(&self) -> Result<()> {
        // If the superblock is clean, the block groups must be clean.
        if !self.super_block.read().is_dirty() {
            return Ok(());
        }

        let mut super_block = self.super_block.write();
        // Writes back the metadata of block groups
        for block_group in &self.block_groups {
            block_group.sync_metadata()?;
        }

        // Writes back the main superblock and group descriptor table.
        let mut bio_waiter = BioWaiter::new();
        let raw_super_block = RawSuperBlock::from((*super_block).deref());
        bio_waiter.concat(
            self.block_device
                .write_bytes_async(SUPER_BLOCK_OFFSET, raw_super_block.as_bytes())?,
        );
        let group_descriptors_bio_segment = BioSegment::new_from_segment(
            self.group_descriptors_segment.clone(),
            BioDirection::ToDevice,
        );
        bio_waiter.concat(self.block_device.write_blocks_async(
            super_block.group_descriptors_bid(0),
            group_descriptors_bio_segment.clone(),
        )?);
        bio_waiter
            .wait()
            .ok_or_else(|| Error::with_message(Errno::EIO, "failed to sync main metadata"))?;
        drop(bio_waiter);

        // Writes back the backups of superblock and group descriptor table.
        let mut raw_super_block_backup = raw_super_block;
        for idx in 1..super_block.block_groups_count() {
            if super_block.is_backup_group(idx as usize) {
                let mut bio_waiter = BioWaiter::new();
                raw_super_block_backup.block_group_idx = idx as u16;
                bio_waiter.concat(self.block_device.write_bytes_async(
                    super_block.bid(idx as usize).to_offset(),
                    raw_super_block_backup.as_bytes(),
                )?);
                bio_waiter.concat(self.block_device.write_blocks_async(
                    super_block.group_descriptors_bid(idx as usize),
                    group_descriptors_bio_segment.clone(),
                )?);
                bio_waiter.wait().ok_or_else(|| {
                    Error::with_message(Errno::EIO, "failed to sync backup metadata")
                })?;
            }
        }

        // Reset to clean.
        super_block.clear_dirty();
        Ok(())
    }

    /// Writes back all the cached inodes to the block device.
    pub fn sync_all_inodes(&self) -> Result<()> {
        for block_group in &self.block_groups {
            block_group.sync_all_inodes()?;
        }
        Ok(())
    }

    #[inline]
    fn block_group_of_bid(&self, bid: Ext2Bid) -> Result<(usize, &BlockGroup)> {
        let block_group_idx = (bid / self.blocks_per_group) as usize;
        if block_group_idx >= self.block_groups.len() {
            return_errno_with_message!(Errno::EINVAL, "invalid bid");
        }
        Ok((block_group_idx, &self.block_groups[block_group_idx]))
    }

    #[inline]
    fn block_group_of_ino(&self, ino: u32) -> Result<(usize, &BlockGroup)> {
        let block_group_idx = ((ino - 1) / self.inodes_per_group) as usize;
        if block_group_idx >= self.block_groups.len() {
            return_errno!(Errno::ENOENT);
        }
        Ok((block_group_idx, &self.block_groups[block_group_idx]))
    }

    #[inline]
    fn inode_idx(&self, ino: u32) -> u32 {
        (ino - 1) % self.inodes_per_group
    }

    #[inline]
    fn block_idx(&self, bid: Ext2Bid) -> Ext2Bid {
        bid % self.blocks_per_group
    }
}
