#include "fs/tmpfs/FileSystem.h"
#include "fs/Mount.h"
#include "fs/devices/DeviceRegistry.h"
#include <luna/Alloc.h>
#include <luna/CString.h>
#include <luna/Ignore.h>

namespace TmpFS
{
    Result<SharedPtr<VFS::FileSystem>> FileSystem::create()
    {
        SharedPtr<FileSystem> fs = TRY(adopt_shared_if_nonnull(new (std::nothrow) FileSystem()));
        SharedPtr<VFS::Inode> root = TRY(fs->create_dir_inode({}));
        root->chmod(0755);
        fs->set_root(root);
        return (SharedPtr<VFS::FileSystem>)fs;
    }

    Result<SharedPtr<VFS::Inode>> FileSystem::create_file_inode()
    {
        SharedPtr<FileInode> inode = TRY(make_shared<FileInode>());
        inode->set_fs(*this, {});
        inode->set_inode_number(m_next_inode_number++, {});
        return (SharedPtr<VFS::Inode>)inode;
    }

    Result<SharedPtr<VFS::Inode>> FileSystem::create_dir_inode(SharedPtr<VFS::Inode> parent)
    {
        SharedPtr<DirInode> inode = TRY(make_shared<DirInode>());

        TRY(inode->add_entry(inode, "."));
        TRY(inode->add_entry(parent ? parent : (SharedPtr<VFS::Inode>)inode, ".."));

        inode->set_self(inode, {});
        inode->set_fs(*this, {});
        inode->set_inode_number(m_next_inode_number++, {});

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

    Result<SharedPtr<VFS::Inode>> FileSystem::create_device_inode(u32 major, u32 minor)
    {
        SharedPtr<Device> device = TRY(DeviceRegistry::fetch_special_device(major, minor));

        SharedPtr<DeviceInode> inode = TRY(make_shared<DeviceInode>());
        inode->set_fs(*this, {});
        inode->set_inode_number(m_next_inode_number++, {});
        inode->set_device(device, {});

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

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

    void FileSystem::set_root(SharedPtr<VFS::Inode> root)
    {
        m_root_inode = root;
    }

    Result<SharedPtr<VFS::Inode>> DirInode::find(const char* name) const
    {
        for (const auto& entry : m_entries)
        {
            if (!strcmp(name, entry.name.chars())) return entry.inode;
        }

        return err(ENOENT);
    }

    Result<void> DirInode::replace_entry(SharedPtr<VFS::Inode> inode, const char* name)
    {
        for (auto& entry : m_entries)
        {
            if (!strcmp(name, entry.name.chars()))
            {
                entry.inode = inode;
                return {};
            }
        }

        return err(ENOENT);
    }

    Option<VFS::DirectoryEntry> DirInode::get(usize index) const
    {
        if (index >= m_entries.size()) return {};

        return m_entries[index];
    }

    Result<void> DirInode::add_entry(SharedPtr<VFS::Inode> inode, const char* name)
    {
        if (find(name).has_value()) return err(EEXIST);

        VFS::DirectoryEntry entry { inode, name };

        TRY(m_entries.try_append(move(entry)));

        inode->did_link();

        return {};
    }

    Result<void> DirInode::remove_entry(const char* name)
    {
        SharedPtr<VFS::Inode> inode = TRY(find(name));

        if (inode->type() == VFS::InodeType::Directory && inode->entries() != 2) return err(ENOTEMPTY);

        if (inode->is_mountpoint()) return err(EBUSY);

        m_entries.remove_first_matching(
            [&](const VFS::DirectoryEntry& entry) { return !strcmp(entry.name.chars(), name); });

        inode->did_unlink();

        return {};
    }

    Result<SharedPtr<VFS::Inode>> DirInode::create_file(const char* name)
    {
        auto inode = TRY(m_fs->create_file_inode());

        TRY(add_entry(inode, name));

        return inode;
    }

    Result<SharedPtr<VFS::Inode>> DirInode::create_subdirectory(const char* name)
    {
        auto inode = TRY(m_fs->create_dir_inode(m_self));

        TRY(add_entry(inode, name));

        return inode;
    }

    Result<usize> FileInode::read(u8* buf, usize offset, usize length) const
    {
        if (length == 0) return 0;

        if (offset > m_data_buffer.size()) return 0;
        if (offset + length > m_data_buffer.size()) length = m_data_buffer.size() - offset;

        memcpy(buf, m_data_buffer.data() + offset, length);

        return length;
    }

    Result<usize> FileInode::write(const u8* buf, usize offset, usize length)
    {
        if (length == 0) return 0;

        if (offset > m_data_buffer.size())
        {
            // Fill the in-between space with zeroes.
            usize old_size = m_data_buffer.size();
            usize zeroes = offset - old_size;

            TRY(m_data_buffer.try_resize(offset));

            memset(m_data_buffer.data() + old_size, 0, zeroes);
        }

        u8* slice = TRY(m_data_buffer.slice(offset, length));
        memcpy(slice, buf, length);

        return length;
    }

    Result<void> FileInode::truncate(usize size)
    {
        usize old_size = m_data_buffer.size();

        TRY(m_data_buffer.try_resize(size));

        if (size > old_size) memset(m_data_buffer.data() + old_size, 0, size - old_size);

        return {};
    }

    usize FileInode::size() const
    {
        return m_data_buffer.size();
    }
}