use vapfs::{Index, Timestamp};
use crate::crc32;

/// # Magic
/// Constant value that identifies the Superblock as a valid VapUFS filesystem
pub const MAGIC: u64 = 0x766170554653;

/// # Superblock
/// The primary struct of a VapUFS filesystem, contains metadata about the filesystem.
/// Located at byte offset 1024 of the block device.
/// All values are big-endian unless otherwise specified.
/// Directly after the Superblock are the free data blocks bitmap and the free inodes bitmap.
/// Free data blocks bitmap is data_block_count / 8 bytes long (rounded up).
/// Free inodes bitmap is inode_count / 8 bytes long (rounded up).
#[repr(C)]
#[derive(Copy, Clone)]
pub struct Superblock {
    /// magic number that identifies the Superblock as a valid VapUFS filesystem
    pub magic: u64,
    /// size of each block in bytes
    pub block_size: u32,
    /// location of first data block in blocks
    pub first_data_block: Index,
    /// location of first inode block in blocks
    pub first_inode_block: Index,
    /// location of first journal block in blocks
    pub first_journal_block: Index,
    /// total count of inodes, including unused inodes
    pub inode_count: Index,
    /// total count of data blocks, including unused blocks
    pub data_block_count: Index,
    /// total count of blocks dedicated to journal
    pub journal_block_count: Index,
    /// total count of inodes in use
    pub allocated_inode_count: Index,
    /// total count of data blocks in use
    pub allocated_data_block_count: Index,
    /// timestamp of creation
    pub creation_time: Timestamp,
    /// timestamp of last modification
    pub last_modification_time: Timestamp,
    /// crc32 checksum of this Superblock
    pub checksum: u32,
    /// index of the current journal entry being processed
    /// incremented after a journal entry is committed, set to 0 if past max
    pub journal_position: u32,
    /// reserved values for expansion
    pub reserved: [u64; 7],
    // 156 bytes used so far
    // rest of the block is used by the free data blocks bitmap and the free inodes bitmap
}

impl Superblock {
    /// in-place conversion from the storage representation (big endian) to the native representation
    pub fn convert_big_endian_to_native(&mut self) {
        #[cfg(target_endian = "little")]
        {
            self.magic = u64::from_be(self.magic);
            self.block_size = u32::from_be(self.block_size);
            self.first_data_block = u64::from_be(self.first_data_block);
            self.first_inode_block = u64::from_be(self.first_inode_block);
            self.first_journal_block = u64::from_be(self.first_journal_block);
            self.inode_count = u64::from_be(self.inode_count);
            self.data_block_count = u64::from_be(self.data_block_count);
            self.journal_block_count = u64::from_be(self.journal_block_count);
            self.allocated_inode_count = u64::from_be(self.allocated_inode_count);
            self.allocated_data_block_count = u64::from_be(self.allocated_data_block_count);
            self.creation_time = u64::from_be(self.creation_time);
            self.last_modification_time = u64::from_be(self.last_modification_time);
            for i in 0..8 {
                self.reserved[i] = u64::from_be(self.reserved[i]);
            }
        }
    }

    /// in-place conversion from the native representation to the storage representation (big endian)
    pub fn convert_native_to_big_endian(&mut self) {
        #[cfg(target_endian = "little")]
        {
            self.magic = u64::to_be(self.magic);
            self.block_size = u32::to_be(self.block_size);
            self.first_data_block = u64::to_be(self.first_data_block);
            self.first_inode_block = u64::to_be(self.first_inode_block);
            self.first_journal_block = u64::to_be(self.first_journal_block);
            self.inode_count = u64::to_be(self.inode_count);
            self.data_block_count = u64::to_be(self.data_block_count);
            self.journal_block_count = u64::to_be(self.journal_block_count);
            self.allocated_inode_count = u64::to_be(self.allocated_inode_count);
            self.allocated_data_block_count = u64::to_be(self.allocated_data_block_count);
            self.creation_time = u64::to_be(self.creation_time);
            self.last_modification_time = u64::to_be(self.last_modification_time);
            for i in 0..8 {
                self.reserved[i] = u64::to_be(self.reserved[i]);
            }
        }
    }

    /// returns true if the crc32 checksum is currently valid
    /// returns false otherwise
    pub fn is_checksum_valid(&self) -> bool {
        let mut buf = [0; core::mem::size_of::<Self>() - (8 * 8)]; // don't hash last 7 u64s, the reserved u32, or the checksum
        unsafe {
            core::ptr::copy(self as *const Self as *const u8, buf.as_mut_ptr(), buf.len());
        }
        let checksum = crc32::crc32(&buf);

        checksum == self.checksum
    }

    /// updates the crc32 checksum
    pub fn recalculate_checksum(&mut self) {
        let mut buf = [0; core::mem::size_of::<Self>() - (8 * 8)]; // don't hash last 7 u64s, the reserved u32, or the checksum
        unsafe {
            core::ptr::copy(self as *const Self as *const u8, buf.as_mut_ptr(), buf.len());
        }
        let checksum = crc32::crc32(&buf);

        self.checksum = checksum;
    }
}

/// # UNIXPermissions
/// UNIX permissions
#[repr(u16)]
pub enum UNIXMode {
    /// Read, Other
    Read = 1 << 0,
    /// Write, Other
    Write = 1 << 1,
    /// Execute, Other
    Execute = 1 << 2,
    /// Read, Group
    ReadGroup = 1 << 3,
    /// Write, Group
    WriteGroup = 1 << 4,
    /// Execute, Group
    ExecuteGroup = 1 << 5,
    /// Read, Owner
    ReadOwner = 1 << 6,
    /// Write, Owner
    WriteOwner = 1 << 7,
    /// Execute, Owner
    ExecuteOwner = 1 << 8,
    /// Sticky bit
    Sticky = 1 << 9,
    /// Set GID
    SetGID = 1 << 10,
    /// Set UID
    SetUID = 1 << 11,
}

/// # Filetype
/// Represents a file's type in the last 4 bits of the mode field of an inode.
#[repr(u16)]
pub enum Filetype {
    /// Named FIFO pipe
    FIFO = 0b0001 << 12,
    /// Character device
    CharDevice = 0b0010 << 12,
    /// Directory
    Directory = 0b0011 << 12,
    /// Block device
    BlockDevice = 0b0100 << 12,
    /// Regular file
    RegularFile = 0b0101 << 12,
    /// Symbolic link
    SymbolicLink = 0b0110 << 12,
    /// Socket
    Socket = 0b0111 << 12,
}

/// # Inode
/// Usually represents a file or directory, used to store metadata and locations of data blocks.
#[repr(C)]
pub struct Inode {
    /// UNIX permissions / mode and filetype
    pub mode: u16,
    /// Number of links to this inode
    pub link_count: u16,
    /// User ID of owner
    pub uid: u32,
    /// Group ID of owner
    pub gid: u32,
    /// Size in bytes
    pub size: Index,
    /// Size in blocks
    pub block_count: Index,
    /// Timestamp of creation
    pub creation_time: Timestamp,
    /// Timestamp of last access
    pub last_access_time: Timestamp,
    /// Timestamp of last modification
    pub last_modification_time: Timestamp,
    /// Timestamp of last Inode modification
    pub last_inode_modification_time: Timestamp,
    /// Timestamp of deletion
    pub deletion_time: Timestamp,
    /// Flags field, see `InodeFlags`
    pub flags: u32,
    /// Direct-Block-Addresses, last three are indirect if `InodeFlags::INDIRECT` is set
    /// Indirect blocks are Indexes to other blocks, their contents are a u64 count "N" followed by N u64 indexes
    /// both are pointers to data blocks, starting from data block 0
    pub direct_block_addresses: [Index; 12],
    /// CRC32 checksum of this inode
    pub checksum: u32,
    // 172 bytes used so far
}

impl Inode {
    /// in-place conversion from the storage representation (big endian) to the native representation
    pub fn convert_big_endian_to_native(&mut self) {
        #[cfg(target_endian = "little")]
        {
            self.mode = u16::from_be(self.mode);
            self.link_count = u16::from_be(self.link_count);
            self.uid = u32::from_be(self.uid);
            self.gid = u32::from_be(self.gid);
            self.size = u64::from_be(self.size);
            self.block_count = u64::from_be(self.block_count);
            self.creation_time = u64::from_be(self.creation_time);
            self.last_access_time = u64::from_be(self.last_access_time);
            self.last_modification_time = u64::from_be(self.last_modification_time);
            self.last_inode_modification_time = u64::from_be(self.last_inode_modification_time);
            self.deletion_time = u64::from_be(self.deletion_time);
            self.flags = u32::from_be(self.flags);
            for i in 0..12 {
                self.direct_block_addresses[i] = u64::from_be(self.direct_block_addresses[i]);
            }
            self.checksum = u32::from_be(self.checksum);
        }
    }

    /// in-place conversion from the native representation to the storage representation (big endian)
    pub fn convert_native_to_big_endian(&mut self) {
        #[cfg(target_endian = "little")]
        {
            self.mode = u16::to_be(self.mode);
            self.link_count = u16::to_be(self.link_count);
            self.uid = u32::to_be(self.uid);
            self.gid = u32::to_be(self.gid);
            self.size = u64::to_be(self.size);
            self.block_count = u64::to_be(self.block_count);
            self.creation_time = u64::to_be(self.creation_time);
            self.last_access_time = u64::to_be(self.last_access_time);
            self.last_modification_time = u64::to_be(self.last_modification_time);
            self.last_inode_modification_time = u64::to_be(self.last_inode_modification_time);
            self.deletion_time = u64::to_be(self.deletion_time);
            self.flags = u32::to_be(self.flags);
            for i in 0..12 {
                self.direct_block_addresses[i] = u64::to_be(self.direct_block_addresses[i]);
            }
            self.checksum = u32::to_be(self.checksum);
        }
    }

    /// returns true if the crc32 checksum is currently valid
    /// returns false otherwise
    pub fn is_checksum_valid(&self) -> bool {
        let mut buf = [0; core::mem::size_of::<Self>() - 4]; // don't hash the checksum
        unsafe {
            core::ptr::copy(self as *const Self as *const u8, buf.as_mut_ptr(), buf.len());
        }
        let checksum = crc32::crc32(&buf);

        checksum == self.checksum
    }

    /// updates the crc32 checksum
    pub fn recalculate_checksum(&mut self) {
        let mut buf = [0; core::mem::size_of::<Self>() - 4]; // don't hash the checksum
        unsafe {
            core::ptr::copy(self as *const Self as *const u8, buf.as_mut_ptr(), buf.len());
        }
        let checksum = crc32::crc32(&buf);

        self.checksum = checksum;
    }
}

/// # InodeFlags
/// Flags field of an inode
#[repr(u32)]
pub enum InodeFlags {
    /// File is corrupted
    CORRUPT = 1 << 0,
    /// File uses indirect blocks
    INDIRECT = 1 << 1,
}

/// # JournalEntry
/// A Journal Entry
#[repr(C)]
#[derive(Copy, Clone)]
pub struct JournalEntry {
    /// JournalOperation
    pub operation: u32,
    /// crc32 hash of the content with flags set to zero, used to verify journal was fully written without verifying the stage
    pub zeroed_content_crc32: u32,
    pub content: JournalEntryContents,
}

impl JournalEntry {
    /// in-place conversion from the storage representation (big endian) to the native representation
    pub fn convert_big_endian_to_native(&mut self) {
        #[cfg(target_endian = "little")]
        {
            self.operation = u32::from_be(self.operation);
            self.zeroed_content_crc32 = u32::from_be(self.zeroed_content_crc32);
            match self.operation {
                0 => { // SingleBlockWrite
                    unsafe { self.content.block_write.convert_big_endian_to_native() };
                }
                1 => { // MultiblockWrite
                    unsafe { self.content.multiblock_write.convert_big_endian_to_native() };
                }

                _ => {}
            }
        }
    }

    /// in-place conversion from the native representation to the storage representation (big endian)
    pub fn convert_native_to_big_endian(&mut self) {
        #[cfg(target_endian = "little")]
        {
            self.operation = u32::to_be(self.operation);
            self.zeroed_content_crc32 = u32::to_be(self.zeroed_content_crc32);
            match self.operation {
                0 => { // SingleBlockWrite
                    unsafe { self.content.block_write.convert_native_to_big_endian() };
                }
                1 => { // MultiblockWrite
                    unsafe { self.content.multiblock_write.convert_native_to_big_endian() };
                }

                _ => {}
            }
        }
    }
}

/// # JournalOperation
/// Type of operation performed by a Journal Entry
#[repr(u32)]
pub enum JournalOperation {
    /// A single block write, described by a `JournalBlockWrite`
    SingleBlockWrite = 0,
    /// A multi-block write, described by a `JournalMultiblockWrite`
    MultiblockWrite = 1,
}

/// # JournalBlockWrite
/// writes are performed as follows:
/// 1. create and write the journal entry
/// 2. allocate a data block for the data to be written
/// 3. set the entry's allocated flag
/// 4. write the data to the allocated data block
/// 5. set the entry's stored flag
/// == the following steps will be performed upon a journal flush, i.e. when the driver has decided that it's time to finally "commit" the journal ==
/// 6. depending on the destination of the write, either:
///     - update inode metadata to point to the new data block
///     - copy the data from the old data block to the new data block
/// 7. set the entry's written flag
/// 8. deallocate the old data block
/// 9. set the entry's deallocated flag
/// == done! ==
/// ideally, with this schema the filesystem shall never become corrupt
/// all fields in the entry should be modified individually on the block device instead of rewriting the entire entry
#[repr(C)]
#[derive(Copy, Clone)]
pub struct JournalBlockWrite {
    /// JBRFlags stating how far the write has progressed
    pub flags: u32,
    /// are we writing to an inode instead of a data block?
    pub target_is_inode: bool,
    /// target inode number
    pub target_inode: Index,
    /// target block number (if target is a data block, this will be the index in the inode's direct block array;
    /// if greater than 12, the indirect block this points to (i / 12) will be used, and the index in that block will be i % 12)
    /// (if target is an inode, this field will be ignored)
    pub target_block: Index,
    /// actual data block number, unused if target is an inode
    pub real_target_block: Index,
    /// block number of source data block
    pub source_block: Index,
    /// crc32 hash of the source data block
    pub source_block_crc32: u32,
}

impl JournalBlockWrite {
    /// in-place conversion from the storage representation (big endian) to the native representation
    pub fn convert_big_endian_to_native(&mut self) {
        #[cfg(target_endian = "little")]
        {
            self.flags = u32::from_be(self.flags);
            self.target_block = u64::from_be(self.target_block);
            self.source_block = u64::from_be(self.source_block);
            self.source_block_crc32 = u32::from_be(self.source_block_crc32);
        }
    }

    /// in-place conversion from the native representation to the storage representation (big endian)
    pub fn convert_native_to_big_endian(&mut self) {
        #[cfg(target_endian = "little")]
        {
            self.flags = u32::to_be(self.flags);
            self.target_block = u64::to_be(self.target_block);
            self.source_block = u64::to_be(self.source_block);
            self.source_block_crc32 = u32::to_be(self.source_block_crc32);
        }
    }
}

/// # JBRFlags
/// Flags field of a JournalBlockWrite
#[repr(u32)]
#[derive(Copy, Clone)]
pub enum JBRFlags {
    /// source data block has been chosen but not yet allocated
    Chosen = 1,
    /// source data block has been allocated
    Allocated = 2,
    /// data has been written to the source data block
    Stored = 3,
    /// source data block has either replaced an old data block or has been written to an inode
    Written = 4,
    /// source data block (in the case of a write to an inode) or old data block (in the case of a write to a data block) has been deallocated
    /// (i.e. this journal entry has been fully committed)
    CompleteAndDeallocated = 0,
}

/// # JournalMultiblockWrite
/// a special entry for writing to multiple blocks at once,
/// used for circumstances where it is very important that all blocks are either
/// written successfully, or not written at all (i.e. directory blocks)
/// writes are performed as follows:
/// 1. create and write the journal entry
/// 2. allocate a data block to store a MultiblockWriteList
/// 3. set the entry's allocated_list flag
/// 4. allocate data blocks to store the data to be written
/// 5. set the entry's allocated_data flag
/// 6. write the data to the allocated data blocks and the list to the allocated list block
/// 7. set the entry's stored flag
/// == the following steps will be performed upon a journal flush ==
/// 8. update inode metadata to point to the new data blocks
/// 9. set the entry's written flag
/// 10. deallocate the old data blocks & list block
/// 11. set the entry's deallocated flag
/// == done! ==
#[repr(C)]
#[derive(Copy, Clone)]
pub struct JournalMultiblockWrite {
    /// JMWFlags stating how far the write has progressed
    pub flags: u32,
    /// inode number of target inode
    pub target_inode: Index,
    /// block number of first target block
    pub target_block: Index,
    /// number of target blocks
    pub target_block_count: Index,
    /// block number of list block structure
    pub list_block: Index,
    /// block number of old list block structure
    pub old_list_block: Index,
    /// crc32 hash of the list block
    pub list_block_crc32: u32,
}

impl JournalMultiblockWrite {
    /// in-place conversion from the storage representation (big endian) to the native representation
    pub fn convert_big_endian_to_native(&mut self) {
        #[cfg(target_endian = "little")]
        {
            self.flags = u32::from_be(self.flags);
            self.target_block = u64::from_be(self.target_block);
            self.target_block_count = u64::from_be(self.target_block_count);
            self.list_block = u64::from_be(self.list_block);
            self.list_block_crc32 = u32::from_be(self.list_block_crc32);
        }
    }

    /// in-place conversion from the native representation to the storage representation (big endian)
    pub fn convert_native_to_big_endian(&mut self) {
        #[cfg(target_endian = "little")]
        {
            self.flags = u32::to_be(self.flags);
            self.target_block = u64::to_be(self.target_block);
            self.target_block_count = u64::to_be(self.target_block_count);
            self.list_block = u64::to_be(self.list_block);
            self.list_block_crc32 = u32::to_be(self.list_block_crc32);
        }
    }
}

/// # JMWFlags
/// Flags field of a JournalMultiblockWrite
#[repr(u32)]
#[derive(Copy, Clone)]
pub enum JMWFlags {
    /// list block has been chosen but not yet allocated
    ChosenList = 1,
    /// list block has been allocated
    AllocatedList = 2,
    /// data blocks have been chosen and stored in the list block but not yet allocated
    ChosenData = 3,
    /// data blocks have been allocated
    AllocatedData = 4,
    /// data has been written to the data blocks and the list block
    Stored = 5,
    /// data blocks have replaced old data blocks
    Written = 6,
    /// data blocks and list block have been deallocated
    /// (i.e. this journal entry has been fully committed)
    CompleteAndDeallocated = 0,
}

/// # ListBlock
/// a list of data blocks for a journaled multiblock write, similar in structure to an inode
#[repr(C)]
#[derive(Copy, Clone)]
pub struct ListBlock {
    pub using_indirect_blocks: bool,
    /// Direct-Block-Addresses, last three are indirect if `using_indirect_blocks` is set
    /// Indirect blocks are Indexes to other blocks, their contents are a u64 count "N" followed by N u64 indexes
    /// both are pointers to data blocks, starting from data block 0
    pub direct_block_addresses: [Index; 12],
}

impl ListBlock {
    /// in-place conversion from the storage representation (big endian) to the native representation
    pub fn convert_big_endian_to_native(&mut self) {
        #[cfg(target_endian = "little")]
        {
            for i in 0..12 {
                self.direct_block_addresses[i] = u64::from_be(self.direct_block_addresses[i]);
            }
        }
    }

    /// in-place conversion from the native representation to the storage representation (big endian)
    pub fn convert_native_to_big_endian(&mut self) {
        #[cfg(target_endian = "little")]
        {
            for i in 0..12 {
                self.direct_block_addresses[i] = u64::to_be(self.direct_block_addresses[i]);
            }
        }
    }
}

/// # JournalEntryContents
/// union of all possible journal entries
#[repr(C)]
#[derive(Copy, Clone)]
pub union JournalEntryContents {
    pub block_write: JournalBlockWrite,
    pub multiblock_write: JournalMultiblockWrite,
}