#include "fs/VFS.h"
#include "Log.h"
#include "fs/Mount.h"
#include "thread/Thread.h"
#include <bits/modes.h>
#include <luna/PathParser.h>
#include <luna/Utf8.h>

namespace VFS
{
    SharedPtr<FileSystem> root_fs;

    Inode& root_inode()
    {
        return *root_fs->root_inode();
    }

    static constexpr int MAX_SYMLINKS = 8;

    Result<SharedPtr<Inode>> resolve_path_impl(const char* path, Credentials auth, SharedPtr<Inode> current_inode,
                                               bool follow_last_symlink, int& symlinks_followed)
    {
        if (symlinks_followed >= MAX_SYMLINKS) return err(ELOOP);

        if (*path == '\0') return err(ENOENT);

        auto parser = TRY(PathParser::create(path));

        SharedPtr<Inode> parent_inode = current_inode;

        const char* section;
        while (parser.next().try_set_value(section))
        {
            if (!can_execute(current_inode, auth)) return err(EACCES);
            current_inode = TRY(current_inode->find(section));

            if (current_inode->type() == VFS::InodeType::Symlink && (follow_last_symlink || parser.has_next()))
            {
                auto link = TRY(current_inode->readlink());

                SharedPtr<VFS::Inode> symlink_root;

                if (PathParser::is_absolute(link.chars())) symlink_root = root_fs->root_inode();
                else
                    symlink_root = parent_inode;

                symlinks_followed++;
                current_inode = TRY(resolve_path_impl(link.chars(), auth, symlink_root, true, symlinks_followed));
                symlinks_followed--;
            }

            parent_inode = current_inode;
        }

        return current_inode;
    }

    Result<SharedPtr<Inode>> resolve_path(const char* path, Credentials auth, SharedPtr<VFS::Inode> working_directory,
                                          bool follow_last_symlink)
    {
        SharedPtr<Inode> current_inode;

        if (PathParser::is_absolute(path) || !working_directory) current_inode = root_fs->root_inode();
        else
            current_inode = working_directory;

        int symlinks_followed = 0;

        return resolve_path_impl(path, auth, current_inode, follow_last_symlink, symlinks_followed);
    }

    Result<SharedPtr<Inode>> create_directory(const char* path, Credentials auth, SharedPtr<Inode> working_directory)
    {
        auto parser = TRY(PathParser::create(path));
        auto parent_path = TRY(parser.dirname());

        auto parent_inode = TRY(resolve_path(parent_path.chars(), auth, working_directory));

        if (!can_write(parent_inode, auth)) return err(EACCES);

        auto child_name = TRY(parser.basename());

        TRY(validate_filename(child_name.view()));

        return parent_inode->create_subdirectory(child_name.chars());
    }

    Result<SharedPtr<Inode>> create_file(const char* path, Credentials auth, SharedPtr<Inode> working_directory)
    {
        auto parser = TRY(PathParser::create(path));
        auto parent_path = TRY(parser.dirname());

        auto parent_inode = TRY(resolve_path(parent_path.chars(), auth, working_directory));

        if (!can_write(parent_inode, auth)) return err(EACCES);

        auto child_name = TRY(parser.basename());

        TRY(validate_filename(child_name.view()));

        return parent_inode->create_file(child_name.chars());
    }

    Result<void> validate_filename(StringView name)
    {
        // Forbid problematic characters that could cause trouble in shell scripts and the like.
        if (strpbrk(name.chars(), "*?:[]\"<>\\")) return err(EINVAL);

        // Forbid filenames with leading spaces.
        if (!name.is_empty() && name[0] == ' ') return err(EINVAL);

        // Forbid filenames with trailing spaces.
        if (!name.is_empty() && name[name.length() - 1] == ' ') return err(EINVAL);

        for (const auto& c : name)
        {
            // Forbid filenames with control characters.
            if (c < 32 || c == 127) return err(EINVAL);
        }

        // Forbid filenames that are not valid UTF-8.

        Utf8StringDecoder decoder(name.chars());

        // This will fail if the filename is invalid UTF-8.
        TRY(decoder.code_points());

        return {};
    }

    bool can_execute(SharedPtr<Inode> inode, Credentials auth)
    {
        if (auth.euid == 0) return true;

        if (inode->uid() == auth.euid) { return inode->mode() & S_IXUSR; }
        if (inode->gid() == auth.egid) { return inode->mode() & S_IXGRP; }

        return inode->mode() & S_IXOTH;
    }

    bool can_write(SharedPtr<Inode> inode, Credentials auth)
    {
        if (auth.euid == 0) return true;

        if (inode->uid() == auth.euid) { return inode->mode() & S_IWUSR; }
        if (inode->gid() == auth.egid) { return inode->mode() & S_IWGRP; }

        return inode->mode() & S_IWOTH;
    }

    bool can_read(SharedPtr<Inode> inode, Credentials auth)
    {
        if (auth.euid == 0) return true;

        if (inode->uid() == auth.euid) { return inode->mode() & S_IRUSR; }
        if (inode->gid() == auth.egid) { return inode->mode() & S_IRGRP; }

        return inode->mode() & S_IROTH;
    }

    bool is_setuid(SharedPtr<Inode> inode)
    {
        return inode->mode() & S_ISUID;
    }

    bool is_setgid(SharedPtr<Inode> inode)
    {
        return inode->mode() & S_ISGID;
    }

    bool is_sticky(SharedPtr<Inode> inode)
    {
        return inode->mode() & S_ISVTX;
    }

    bool is_seekable(SharedPtr<Inode> inode)
    {
        return inode->type() != InodeType::FIFO && inode->type() != InodeType::CharacterDevice;
    }

    Result<void> mount_root(SharedPtr<VFS::FileSystem> fs)
    {
        root_fs = fs;

        return {};
    }

    Result<void> mount(const char* path, SharedPtr<VFS::FileSystem> fs, Credentials auth,
                       SharedPtr<VFS::Inode> working_directory)
    {
        auto parser = TRY(PathParser::create(path));
        auto parent_path = TRY(parser.dirname());
        auto child = TRY(parser.basename());

        kdbgln("vfs: Mounting filesystem on target %s", path);

        auto parent_inode = TRY(resolve_path(parent_path.chars(), auth, working_directory));

        auto inode = TRY(parent_inode->find(child.chars()));
        if (inode->type() != VFS::InodeType::Directory) return err(ENOTDIR);
        if (inode->is_mountpoint()) return err(EBUSY);

        auto mount = TRY(MountInode::create(inode, fs));

        TRY(parent_inode->replace_entry(mount, child.chars()));

        return {};
    }

    Result<void> umount(const char* path, Credentials auth, SharedPtr<VFS::Inode> working_directory)
    {
        auto parser = TRY(PathParser::create(path));
        auto parent_path = TRY(parser.dirname());
        auto child = TRY(parser.basename());

        kdbgln("vfs: Unmounting filesystem on target %s", path);

        auto parent_inode = TRY(resolve_path(parent_path.chars(), auth, working_directory));

        auto inode = TRY(parent_inode->find(child.chars()));
        if (!inode->is_mountpoint()) return err(EINVAL);

        // There are still open file descriptors referencing files within this file system.
        if (inode->fs()->handles() != 0) return err(EBUSY);

        auto mount = (MountInode*)inode.ptr();
        TRY(parent_inode->replace_entry(mount->source(), child.chars()));
        g_mounts.remove(mount);

        return {};
    }
}