#include "Log.h"
#include "fs/VFS.h"
#include "memory/MemoryManager.h"
#include "sys/Syscall.h"
#include "thread/Scheduler.h"
#include <bits/modes.h>
#include <bits/open-flags.h>

// These flags are needed after open(), the rest only affect open().
constexpr int FLAGS_TO_KEEP = O_RDWR | O_APPEND | O_NONBLOCK;

Result<u64> sys_openat(Registers*, SyscallArgs args)
{
    int dirfd = (int)args[0];
    auto path = TRY(MemoryManager::strdup_from_user(args[1]));
    int flags = (int)args[2];
    mode_t mode = (mode_t)args[3];

    Thread* current = Scheduler::current();

    SharedPtr<VFS::Inode> inode;

    // Caller did not pass either O_RDONLY, O_WRONLY or O_RDWR
    if ((flags & O_RDWR) == 0) { return err(EINVAL); }

    if (flags & O_TMPFILE)
    {
        if (!(flags & O_WRONLY)) return err(EINVAL);
        if (flags & O_CREAT) return err(EINVAL);
    }

    int error;
    SharedPtr<VFS::Inode> parent_inode;
    bool ok = current->resolve_atfile(dirfd, path, false, !(flags & O_NOFOLLOW), &parent_inode)
                  .try_set_value_or_error(inode, error);
    if (!ok)
    {
        if (error == ENOENT && (flags & O_CREAT) && !path.is_empty())
        {
            inode = TRY(VFS::create_file(path.chars(), mode & ~current->umask, current->auth, parent_inode));
            // FIXME: Pass these in create_file().
            auto metadata = inode->metadata();
            metadata.uid = current->auth.euid;
            metadata.gid = current->auth.egid;
            TRY(inode->set_metadata(metadata));
        }
        else
            return err(error);
    }
    else if (flags & O_EXCL)
        return err(EEXIST);
    else
    {
        if ((flags & O_RDONLY) && !VFS::can_read(inode, current->auth)) return err(EACCES);
        if ((flags & O_WRONLY) && !VFS::can_write(inode, current->auth)) return err(EACCES);
    }

    // This should only be possible if O_NOFOLLOW was in flags.
    if (inode->type() == VFS::InodeType::Symlink) return err(ELOOP);

    if (inode->type() == VFS::InodeType::Socket) return err(ENXIO);

    if (flags & O_TMPFILE)
    {
        if (inode->type() != VFS::InodeType::Directory) return err(EINVAL);
        inode = TRY(inode->fs()->create_file_inode(mode & current->umask));
        auto metadata = inode->metadata();
        metadata.uid = current->auth.euid;
        metadata.gid = current->auth.egid;
        TRY(inode->set_metadata(metadata));
    }

    if ((flags & O_WRONLY) && inode->fs() && inode->fs()->is_readonly()) return err(EROFS);

    if (inode->type() != VFS::InodeType::Directory && (flags & O_DIRECTORY)) return err(ENOTDIR);

    if (inode->type() == VFS::InodeType::Directory)
    {
        if ((flags & O_WRONLY) || (flags & O_CREAT)) return err(EISDIR);
    }

    if ((flags & O_WRONLY) && (flags & O_TRUNC)) inode->truncate(0);

    int fd = TRY(current->allocate_fd(0));

#ifdef OPEN_DEBUG
    kdbgln("openat: opening file %s from dirfd %d, flags %d, mode %#o = fd %d", path.chars(), dirfd, flags, mode, fd);
#endif

    current->fd_table[fd] =
        FileDescriptor { TRY(make_shared<OpenFileDescription>(inode, flags & FLAGS_TO_KEEP)), 0, flags & O_CLOEXEC };

    return (u64)fd;
}

Result<u64> sys_close(Registers*, SyscallArgs args)
{
    int fd = (int)args[0];
    if (fd < 0 || fd >= FD_MAX) return err(EBADF);

    Thread* current = Scheduler::current();

    Option<FileDescriptor>& descriptor = current->fd_table[fd];

    if (!descriptor.has_value()) return err(EBADF);

    descriptor = {};

    return 0;
}