use core::{
    mem::{self, MaybeUninit},
    ops::{Deref, DerefMut},
    str::FromStr,
};

use yggdrasil_abi::{
    io::{DirectoryEntry, FileType},
    util::FixedString,
};

use crate::{access::InodeBlock, data::FsRequiredFeatures, Dirent, Ext2Fs};

use super::DirentName;

pub(super) struct DirentIter<'a> {
    fs: &'a Ext2Fs,
    block: &'a [u8],
    offset: usize,
}

pub(super) struct DirentIterMut<'a> {
    fs: &'a Ext2Fs,
    block: &'a mut [u8],
    offset: usize,
}

pub(super) struct Record<'a, D: 'a> {
    fs: &'a Ext2Fs,
    data: D,
    offset: usize,
}

impl<'a, D: Deref<Target = [u8]> + 'a> Record<'a, D> {
    pub fn dirent(&self) -> &Dirent {
        bytemuck::from_bytes(&self.data[..size_of::<Dirent>()])
    }

    pub fn is_empty(&self) -> bool {
        self.dirent().ino == 0
    }

    pub fn ino(&self) -> u32 {
        self.dirent().ino
    }

    pub fn data_size(&self) -> usize {
        // Whole dirent is free space
        if self.is_empty() {
            return 0;
        }

        (self.name_len() + size_of::<Dirent>() + 3) & !3
    }

    pub fn record_size(&self) -> usize {
        (self.dirent().ent_size as usize).max(size_of::<Dirent>())
    }

    pub fn free_space(&self) -> usize {
        self.data.len().saturating_sub(self.data_size())
    }

    fn name_len(&self) -> usize {
        let len = self.dirent().name_length_low as usize;
        if !self
            .fs
            .required_features
            .contains(FsRequiredFeatures::DIRENT_TYPE_FIELD)
        {
            todo!();
        }
        len
    }

    pub(super) fn name_bytes(&self) -> &[u8] {
        if !self.is_empty() {
            &self.data[size_of::<Dirent>()..size_of::<Dirent>() + self.name_len()]
        } else {
            b""
        }
    }

    fn name(&self) -> Option<&str> {
        core::str::from_utf8(self.name_bytes()).ok()
    }

    fn type_indicator(&self) -> Option<FileType> {
        if self
            .fs
            .required_features
            .contains(FsRequiredFeatures::DIRENT_TYPE_FIELD)
        {
            match self.dirent().type_indicator {
                1 => Some(FileType::File),
                2 => Some(FileType::Directory),
                7 => Some(FileType::Symlink),
                _ => None,
            }
        } else {
            None
        }
    }
}

impl<'a, D: DerefMut<Target = [u8]> + 'a> Record<'a, D> {
    pub fn dirent_mut(&mut self) -> &mut Dirent {
        bytemuck::from_bytes_mut(&mut self.data[..size_of::<Dirent>()])
    }
}

impl<'a> DirentIter<'a> {
    pub fn new(fs: &'a Ext2Fs, block: InodeBlock<'a>, offset: usize) -> Self {
        let data = if let InodeBlock::Data(data) = block {
            data
        } else {
            &[]
        };
        Self {
            fs,
            block: data,
            offset,
        }
    }

    pub fn offset(&self) -> usize {
        self.offset
    }

    pub fn next_record(&mut self) -> Option<Record<'_, &[u8]>> {
        if self.offset + size_of::<Dirent>() > self.block.len() {
            return None;
        }

        let dirent_struct_end = self.offset + size_of::<Dirent>();
        let dirent: &Dirent = bytemuck::from_bytes(&self.block[self.offset..dirent_struct_end]);
        let record_size = (dirent.ent_size as usize).max(size_of::<Dirent>());
        let end = (self.offset + record_size).min(self.block.len());
        let record = Record {
            data: &self.block[self.offset..end],
            fs: self.fs,
            offset: self.offset,
        };

        self.offset += record_size;
        Some(record)
    }

    pub fn list_entries(&mut self, entries: &mut [MaybeUninit<DirectoryEntry>]) -> usize {
        let mut entry_count = 0;
        while let Some(record) = self.next_record() {
            if entry_count >= entries.len() {
                break;
            }
            if record.is_empty() {
                continue;
            }

            if let Some(name) = record
                .name()
                .and_then(|name| FixedString::from_str(name).ok())
            {
                let ty = record.type_indicator();
                entries[entry_count].write(DirectoryEntry { ty, name });
                entry_count += 1;
            }
        }
        entry_count
    }

    pub fn find_ino(&mut self, name: DirentName<'_>) -> Option<u32> {
        while let Some(record) = self.next_record() {
            if record.name().map(|n| n == &*name).unwrap_or(false) {
                return Some(record.ino());
            }
        }
        None
    }
}

impl<'a> DirentIterMut<'a> {
    pub fn new(fs: &'a Ext2Fs, block: &'a mut [u8], offset: usize) -> Self {
        Self { fs, block, offset }
    }

    pub fn next_record(&mut self) -> Option<Record<'_, &mut [u8]>> {
        if self.offset + size_of::<Dirent>() > self.block.len() {
            return None;
        }

        let dirent_struct_end = self.offset + size_of::<Dirent>();
        let dirent: &Dirent = bytemuck::from_bytes(&self.block[self.offset..dirent_struct_end]);
        let record_size = (dirent.ent_size as usize).max(size_of::<Dirent>());
        let record = Record {
            data: &mut self.block[self.offset..self.offset + record_size],
            fs: self.fs,
            offset: self.offset,
        };

        self.offset += record_size;
        Some(record)
    }

    pub fn remove(&mut self, search: DirentName<'_>) -> Option<u32> {
        let mut ino = None;
        while let Some(mut record) = self.next_record() {
            if record.name_bytes() != search.as_bytes() {
                continue;
            }

            let dirent = record.dirent_mut();
            ino = Some(mem::replace(&mut dirent.ino, 0));
            break;
        }

        let ino = ino?;

        // TODO compact the block

        Some(ino)
    }

    pub fn try_fit(&mut self, name: DirentName<'_>, ino: u32, ty: u8) -> bool {
        let name = name.as_bytes();

        let entry_size = size_of::<Dirent>() + name.len();
        let aligned_entry_size = (entry_size + 3) & !3;

        let name_length_low = name.len() as u8;
        let type_indicator = if self
            .fs
            .required_features
            .contains(FsRequiredFeatures::DIRENT_TYPE_FIELD)
        {
            ty
        } else {
            todo!()
        };

        while let Some(mut record) = self.next_record() {
            if record.free_space() < aligned_entry_size {
                continue;
            }

            let free_space = record.free_space();

            let offset = if record.is_empty() {
                // ino = 0, offset = 0, basically just overwrite the old record
                log::info!(
                    "Fitting #{ino} into an empty record of size {free_space} ({})",
                    record.record_size()
                );
                0
            } else {
                // ino != 0, offset = end of the record, keep it
                let size = record.data_size();
                log::info!(
                    "Fitting #{ino} into a non-empty record of size {size}+{free_space} ({})",
                    record.record_size()
                );

                // Trim the record
                record.dirent_mut().ent_size = size as u16;

                size
            };
            let offset = offset + record.offset;

            log::info!("Will place #{ino} at {offset}, size {entry_size} ({free_space})");

            // Insert the new dirent, make it occupy all the free space
            let new_dirent = Dirent {
                ino,
                type_indicator,
                name_length_low,
                ent_size: free_space as u16,
            };
            let new_dirent_end = offset + size_of::<Dirent>();

            self.block[offset..new_dirent_end].copy_from_slice(bytemuck::bytes_of(&new_dirent));
            self.block[new_dirent_end..offset + entry_size].copy_from_slice(name);

            return true;
        }

        false
    }
}
