#include "fs/ext2/FileSystem.h"
#include "fs/ext2/Inode.h"
#include <luna/Common.h>

static VFS::InodeType vfs_type_from_ext2_type(mode_t mode)
{
    auto type = mode & 0xf000;

    switch (type)
    {
    case EXT2_FIFO: return VFS::InodeType::FIFO;
    case EXT2_CHR: return VFS::InodeType::CharacterDevice;
    case EXT2_DIR: return VFS::InodeType::Directory;
    case EXT2_BLK: return VFS::InodeType::BlockDevice;
    case EXT2_REG: return VFS::InodeType::RegularFile;
    case EXT2_LNK: return VFS::InodeType::Symlink;
    case EXT2_SOCK: [[fallthrough]]; // TODO: Sockets not supported on Luna at the moment.
    default: fail("ext2: Unknown or unsupported inode type");
    }
}

namespace Ext2
{
    FileSystem::FileSystem()
    {
    }

    Result<SharedPtr<VFS::Inode>> FileSystem::find_inode_by_number(ino_t inum, bool initialize_dir_now)
    {
        // Inode numbers start at 1.
        check(inum <= m_superblock.nr_inodes);

        auto maybe_inode = m_inode_cache.try_get(inum);
        if (maybe_inode.has_value()) return maybe_inode.value();

        const u32 block_group = (u32)((inum - 1) / m_superblock.inodes_per_block_group);

        const auto* block_group_descriptor = TRY(find_block_group_descriptor(block_group));
        check(block_group_descriptor);

        // FIXME: Even if the inode size is bigger (Ext2::FileSystem::m_inode_size), we only read this amount. Enlarge
        // the Inode structure to fit this case.
        static constexpr usize INODE_SIZE = 128;

        const u64 index = (inum - 1) % m_superblock.inodes_per_block_group;

        const u64 inode_address = (block_group_descriptor->inode_table_start * m_block_size) + (index * m_inode_size);

        auto inode = TRY(adopt_shared_if_nonnull(new (std::nothrow) Ext2::Inode({}, this)));
        TRY(m_host_device->read((u8*)&inode->m_raw_inode, inode_address, INODE_SIZE));
        inode->m_type = vfs_type_from_ext2_type(inode->m_raw_inode.mode);
        inode->m_metadata.inum = inum;
        inode->m_metadata.size = (m_uses_extended_size && (inode->m_type == VFS::InodeType::RegularFile))
                                     ? ((u64)inode->m_raw_inode.size_high << 32) | (u64)inode->m_raw_inode.size_low
                                     : inode->m_raw_inode.size_low;
        inode->m_metadata.mode = inode->m_raw_inode.mode & 07777;
        inode->m_metadata.nlinks = inode->m_raw_inode.nlinks;
        inode->m_metadata.uid = inode->m_raw_inode.uid;
        inode->m_metadata.gid = inode->m_raw_inode.gid;
        inode->m_metadata.atime = { .tv_sec = inode->m_raw_inode.atime, .tv_nsec = 0 };
        inode->m_metadata.mtime = { .tv_sec = inode->m_raw_inode.mtime, .tv_nsec = 0 };
        inode->m_metadata.ctime = { .tv_sec = inode->m_raw_inode.create_time, .tv_nsec = 0 };

#ifdef EXT2_DEBUG
        kdbgln("ext2: Read inode %lu with mode %#x (%#x + %#o), size %lu", inum, inode->m_raw_inode.mode,
               inode->m_raw_inode.mode & 0xf000, inode->metadata().mode, inode->metadata().size);
#endif

        m_inode_cache.try_set(inum, inode);

        if (initialize_dir_now && inode->m_type == VFS::InodeType::Directory) TRY(inode->lazy_initialize_dir());

        return (SharedPtr<VFS::Inode>)inode;
    }

    Result<const BlockGroupDescriptor*> FileSystem::find_block_group_descriptor(u32 index)
    {
        check(index < m_block_groups);

        auto maybe_desc = m_block_group_descriptor_cache.try_get_ref(index);
        if (maybe_desc) return maybe_desc;

        const u64 address = (m_superblock.first_data_block + 1) * m_block_size + (index * sizeof(BlockGroupDescriptor));

        BlockGroupDescriptor descriptor;
        TRY(m_host_device->read((u8*)&descriptor, address, sizeof(descriptor)));

        check(TRY(m_block_group_descriptor_cache.try_set(index, descriptor)));

        return m_block_group_descriptor_cache.try_get_ref(index);
    }

    Result<SharedPtr<VFS::FileSystem>> FileSystem::create(SharedPtr<Device> host_device)
    {
        SharedPtr<FileSystem> fs = TRY(adopt_shared_if_nonnull(new (std::nothrow) FileSystem()));
        const usize nread = TRY(host_device->read((u8*)&fs->m_superblock, 1024, 1024));
        if (nread != 1024) return err(EINVAL);                            // Source had an invalid superblock.
        if (fs->m_superblock.signature != EXT2_MAGIC) return err(EINVAL); // Source had an invalid superblock.

        if (fs->m_superblock.major_version >= 1)
        {
            auto required = fs->m_superblock.ext_superblock.required_features;
            if (required & EXT2_REQUIRED_COMPAT_D_TYPE) fs->m_dirs_have_type_field = true;

            required &= ~EXT2_REQUIRED_COMPAT_D_TYPE;

            if (required > 0)
            {
                kwarnln("ext2: File system has required features not supported by the implementation, cannot mount");
                return err(EINVAL);
            }

            fs->m_uses_extended_size = true;
            fs->m_inode_size = fs->m_superblock.ext_superblock.inode_size;
        }

        fs->m_host_device = host_device;

        fs->m_block_size = 1024 << fs->m_superblock.log_block_size;
        fs->m_block_groups = ceil_div(fs->m_superblock.nr_blocks, fs->m_superblock.blocks_per_block_group);

#ifdef EXT2_DEBUG
        kdbgln("ext2: Mounting new Ext2 file system, block size=%lu, blocks=%u, inodes=%u, block group=(%u blocks, %u "
               "inodes), %lu block groups",
               fs->m_block_size, fs->m_superblock.nr_blocks, fs->m_superblock.nr_inodes,
               fs->m_superblock.blocks_per_block_group, fs->m_superblock.inodes_per_block_group, fs->m_block_groups);
#endif

        // Lookup the root inode.
        fs->m_root_inode = TRY(fs->find_inode_by_number(2, true));

        return (SharedPtr<VFS::FileSystem>)fs;
    }

    Result<void> FileSystem::set_mount_dir(SharedPtr<VFS::Inode> inode)
    {
        return m_root_inode->replace_entry(inode, "..");
    }

    Result<void> FileSystem::reset_mount_dir()
    {
        return m_root_inode->replace_entry(m_root_inode, "..");
    }
}