use core::{any::Any, fmt, mem::MaybeUninit};

use alloc::{sync::Arc, vec};
use libk::{
    block,
    error::Error,
    task::sync::AsyncMutex,
    vfs::{
        CommonImpl, CreateInfo, DirectoryImpl, DirectoryOpenPosition, Filename, Metadata, Node,
        NodeFlags, NodeRef,
    },
};
use libk_util::{
    get_le_u16, get_le_u32,
    string::{chars_equal_ignore_case, Utf16LeStr},
};
use yggdrasil_abi::{
    io::{DirectoryEntry, FileMode, GroupId, UserId},
    time::SystemTime,
    util::FixedString,
};

use crate::{
    data::{ClusterNumber, FatStr},
    file::FileNode,
    Fat32Fs,
};

pub const DIRENT_SIZE: usize = 32;

const ATTR_READ_ONLY: u8 = 0x01;
const ATTR_HIDDEN: u8 = 0x02;
const ATTR_SYSTEM: u8 = 0x04;
const ATTR_VOLUME_ID: u8 = 0x08;
const ATTR_DIRECTORY: u8 = 0x10;
const ATTR_ARCHIVE: u8 = 0x20;

const ATTR_LFN: u8 = ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID;
const ATTR_LFN_MASK: u8 =
    ATTR_READ_ONLY | ATTR_HIDDEN | ATTR_SYSTEM | ATTR_VOLUME_ID | ATTR_DIRECTORY | ATTR_ARCHIVE;

const LFN_LAST: u8 = 0x40;

#[derive(Debug)]
struct FatDirectoryEntry<'a> {
    long_filename: Option<&'a Utf16LeStr>,
    short_filename: ShortFilename<'a>,
    is_directory: bool,
    first_cluster: ClusterNumber,
    size_bytes: u32,
}

pub struct DirectoryNode {
    pub(crate) fs: Arc<Fat32Fs>,
    pub(crate) cluster: ClusterNumber,
    pub(crate) size_bytes: Option<u32>,
    pub(crate) metadata: Metadata,
    // Will be used when metadata needs to be updated
    #[allow(unused)]
    pub(crate) parent: Option<ClusterNumber>,
}

#[derive(PartialEq, Eq)]
struct ShortFilename<'a> {
    filename: &'a FatStr,
    extension: &'a FatStr,
}

impl DirectoryNode {
    async fn iterate<F: FnMut(usize, FatDirectoryEntry) -> Result<bool, Error>>(
        &self,
        mut predicate: F,
        start_position: u64,
        limit: usize,
    ) -> Result<(usize, u64), Error> {
        assert_eq!(start_position % DIRENT_SIZE as u64, 0);

        let mut lfn_buffer = [0; 512];

        let mut remaining = limit;
        let mut offset = 0;
        let mut position = start_position;
        let entries_per_sector = self.fs.layout.bytes_per_sector / DIRENT_SIZE;
        let mut end_of_dir = false;
        let mut entry_count = 0;

        while remaining != 0 && !end_of_dir {
            let sector_in_cluster = position / self.fs.layout.bytes_per_sector as u64;
            if sector_in_cluster >= self.fs.layout.sectors_per_cluster as u64 {
                todo!("TODO: handle multi-cluster directories");
            }
            let offset_in_sector = (position % self.fs.layout.bytes_per_sector as u64) as usize;
            let max_entries = core::cmp::min(
                limit - offset,
                entries_per_sector - offset_in_sector / DIRENT_SIZE,
            );

            self.fs
                .with_cluster_sector(self.cluster, sector_in_cluster, |buffer| {
                    for i in 0..max_entries {
                        let offset = offset_in_sector + i * DIRENT_SIZE;
                        let entry = &buffer[offset..offset + DIRENT_SIZE];

                        if entry[0] == 0 {
                            end_of_dir = true;
                            break;
                        }

                        // If name[0] == 0xE5, the entry is free
                        if entry[0] == 0xE5 {
                            continue;
                        }

                        let attr = entry[11];

                        if attr & ATTR_LFN_MASK == ATTR_LFN {
                            // LFN entry
                            let ord = ((entry[0] & !LFN_LAST) as usize - 1) * 2;

                            lfn_buffer[ord * 13..ord * 13 + 10].copy_from_slice(&entry[1..11]);
                            lfn_buffer[ord * 13 + 10..ord * 13 + 22]
                                .copy_from_slice(&entry[14..26]);
                            lfn_buffer[ord * 13 + 22..ord * 13 + 26]
                                .copy_from_slice(&entry[28..32]);
                        } else {
                            let mut lfn_length = 0;
                            for i in (0..lfn_buffer.len()).step_by(2) {
                                let word = get_le_u16(&lfn_buffer[i..]);
                                if word == 0 || word == 0xFFFF {
                                    lfn_length = i;
                                    break;
                                }
                            }

                            let long_filename = if lfn_length > 0 {
                                Utf16LeStr::from_utf16le(&lfn_buffer[..lfn_length]).ok()
                            } else {
                                None
                            };

                            let short_filename = ShortFilename {
                                filename: FatStr::new(&entry[..8]),
                                extension: FatStr::new(&entry[8..11]),
                            };

                            let is_directory = match attr & (ATTR_DIRECTORY | ATTR_VOLUME_ID) {
                                0x00 => false,
                                ATTR_DIRECTORY => true,
                                ATTR_VOLUME_ID => continue,
                                _ => continue,
                            };

                            let first_cluster_hi = get_le_u16(&entry[20..]);
                            let first_cluster_lo = get_le_u16(&entry[26..]);
                            let size_bytes = get_le_u32(&entry[28..]);
                            let first_cluster =
                                ((first_cluster_hi as u32) << 16) | (first_cluster_lo as u32);
                            let first_cluster = ClusterNumber(first_cluster);

                            let entry = FatDirectoryEntry {
                                long_filename,
                                short_filename,
                                is_directory,
                                first_cluster,
                                size_bytes,
                            };

                            if !predicate(entry_count, entry)? {
                                end_of_dir = true;
                                break;
                            }

                            entry_count += 1;
                            lfn_buffer.fill(0);
                        }
                    }
                    Ok(())
                })
                .await?;

            position += (max_entries * DIRENT_SIZE) as u64;
            offset += max_entries;
            remaining -= max_entries;
        }

        Ok((entry_count, position))
    }

    async fn read_entries_inner(
        &self,
        position: u64,
        buffer: &mut [MaybeUninit<DirectoryEntry>],
    ) -> Result<(usize, u64), Error> {
        let size = self.size_bytes.unwrap_or(u32::MAX);
        if position >= size as u64 {
            return Ok((0, position));
        }

        let limit = core::cmp::min(
            ((size - position as u32) / DIRENT_SIZE as u32) as usize,
            buffer.len(),
        );
        let (count, position) = self
            .iterate(
                |index, entry| {
                    let mut name = FixedString::empty();
                    if let Some(lfn) = entry.long_filename {
                        name.append_from_chars(lfn.chars()).unwrap();
                    } else {
                        name.append_from_bytes(entry.short_filename.filename.as_bytes());
                        if !entry.short_filename.extension.is_empty() {
                            name.append_from_bytes(b".");
                            name.append_from_bytes(entry.short_filename.extension.as_bytes());
                        }
                    }
                    let dirent = DirectoryEntry { ty: None, name };
                    buffer[index].write(dirent);
                    Ok(true)
                },
                position,
                limit,
            )
            .await?;

        assert_eq!(position % DIRENT_SIZE as u64, 0);

        Ok((count, position))
    }

    async fn lookup_inner(&self, name: &Filename) -> Result<NodeRef, Error> {
        let mut found = None;
        let size = self.size_bytes.unwrap_or(u32::MAX);

        self.iterate(
            |_, entry| {
                if entry.name_equals(name) {
                    found = Some((entry.is_directory, entry.first_cluster, entry.size_bytes));
                    Ok(false)
                } else {
                    Ok(true)
                }
            },
            0,
            (size as u64 / DIRENT_SIZE as u64) as usize,
        )
        .await?;

        let (is_directory, cluster, size) = found.ok_or(Error::DoesNotExist)?;

        let metadata = Metadata {
            uid: UserId::root(),
            gid: GroupId::root(),
            mode: if is_directory {
                FileMode::default_dir()
            } else {
                FileMode::default_file()
            },
            inode: Some(cluster.0),
            // TODO
            ctime: SystemTime::ZERO,
            mtime: SystemTime::ZERO,
            atime: SystemTime::ZERO,
            block_count: (size.div_ceil(self.fs.layout.bytes_per_sector as u32)) as u64,
            block_size: self.fs.layout.bytes_per_sector as u64,
        };

        if is_directory {
            let directory = DirectoryNode {
                fs: self.fs.clone(),
                cluster,
                size_bytes: Some(size),
                parent: Some(self.cluster),
                metadata,
            };

            Ok(Node::directory(
                directory,
                NodeFlags::empty(),
                Some(metadata),
                Some(self.fs.clone()),
            ))
        } else {
            let file = FileNode {
                fs: self.fs.clone(),
                size_bytes: size,
                parent: Some(self.cluster),
                metadata,
                cluster_chain_cache: AsyncMutex::new(vec![cluster]),
            };

            Ok(Node::regular(
                file,
                NodeFlags::empty(),
                Some(metadata),
                Some(self.fs.clone()),
            ))
        }
    }
}

impl CommonImpl for DirectoryNode {
    fn size(&self, _node: &NodeRef) -> Result<u64, Error> {
        Ok(self.size_bytes.unwrap_or(0) as u64)
    }

    fn metadata(&self, _node: &NodeRef) -> Result<Metadata, Error> {
        Ok(self.metadata)
    }

    fn set_metadata(&self, _node: &NodeRef, _metadata: &Metadata) -> Result<(), Error> {
        Err(Error::ReadOnly)
    }

    fn as_any(&self) -> &dyn Any {
        self
    }
}

impl DirectoryImpl for DirectoryNode {
    fn create_node(&self, _parent: &NodeRef, _info: &CreateInfo) -> Result<NodeRef, Error> {
        Err(Error::ReadOnly)
    }

    fn attach_node(
        &self,
        _parent: &NodeRef,
        _child: &NodeRef,
        _name: &Filename,
    ) -> Result<(), Error> {
        Err(Error::ReadOnly)
    }

    fn unlink_node(
        &self,
        _parent: &NodeRef,
        _child: &NodeRef,
        _name: &Filename,
    ) -> Result<(), Error> {
        Err(Error::ReadOnly)
    }

    fn open(&self, _node: &NodeRef) -> Result<DirectoryOpenPosition, Error> {
        Ok(DirectoryOpenPosition::FromPhysical(0))
    }

    fn lookup(&self, _node: &NodeRef, name: &Filename) -> Result<NodeRef, Error> {
        // TODO validate FAT32 filename
        block!(self.lookup_inner(name).await)?
    }

    fn read_entries(
        &self,
        _node: &NodeRef,
        pos: u64,
        entries: &mut [MaybeUninit<DirectoryEntry>],
    ) -> Result<(usize, u64), Error> {
        block!(self.read_entries_inner(pos, entries).await)?
    }

    fn len(&self, _node: &NodeRef) -> Result<usize, Error> {
        Ok((self.size_bytes.unwrap_or(0) / DIRENT_SIZE as u32) as usize)
    }
}

impl FatDirectoryEntry<'_> {
    pub fn name_equals(&self, predicate: &str) -> bool {
        if let Some(lfn) = self.long_filename {
            // Match by LFN
            chars_equal_ignore_case(lfn.chars(), predicate.chars())
        } else {
            // Match by SFN
            let Some(predicate) = ShortFilename::from_str(predicate) else {
                return false;
            };
            predicate.eq_ignore_case(&self.short_filename)
        }
    }
}

impl<'a> ShortFilename<'a> {
    pub fn from_str(s: &'a str) -> Option<Self> {
        let (filename, extension) = match s.split_once('.') {
            Some(xs) => xs,
            None => (s, ""),
        };
        let filename = FatStr::from_str(filename);
        let extension = FatStr::from_str(extension);
        if filename.is_empty() || filename.len() > 8 || extension.len() > 3 {
            return None;
        }
        Some(Self {
            filename,
            extension,
        })
    }

    pub fn eq_ignore_case(&self, other: &Self) -> bool {
        self.filename.eq_ignore_case(other.filename)
            && self.extension.eq_ignore_case(other.extension)
    }
}

impl fmt::Debug for ShortFilename<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "\"{}", self.filename)?;
        if !self.extension.is_empty() {
            write!(f, ".{}", self.extension)?;
        }
        write!(f, "\"")
    }
}

#[cfg(test)]
mod tests {
    use crate::{data::FatStr, directory::ShortFilename};

    #[test]
    fn test_short_filename_from_str() {
        assert_eq!(
            ShortFilename::from_str("a1b2c3d4.txt"),
            Some(ShortFilename {
                filename: FatStr::new(b"a1b2c3d4"),
                extension: FatStr::new(b"txt")
            })
        );
        assert_eq!(
            ShortFilename::from_str("a1b2c3d4"),
            Some(ShortFilename {
                filename: FatStr::new(b"a1b2c3d4"),
                extension: FatStr::new(b"")
            })
        );
        assert_eq!(ShortFilename::from_str("a1b2c3d4e5.txt"), None);
        assert_eq!(ShortFilename::from_str("a1b2c3d4.long"), None);
        assert_eq!(ShortFilename::from_str("."), None);
        assert_eq!(ShortFilename::from_str(""), None);
        assert_eq!(ShortFilename::from_str(".ext"), None)
    }

    #[test]
    fn test_short_filename_ignore_case_eq() {
        assert!(ShortFilename::eq_ignore_case(
            &ShortFilename::from_str("abcdefgh.txt").unwrap(),
            &ShortFilename::from_str("AbCdEfGh.txT").unwrap()
        ));
    }
}
