#define MODULE "stdio"

#include "interrupts/Context.h"
#include "io/Serial.h"
#include "log/Log.h"
#include "memory/VMM.h"
#include "render/TextRenderer.h"
#include "std/errno.h"
#include "std/stdlib.h"
#include "sys/Syscall.h"
#include "sys/UserMemory.h"
#include "thread/Scheduler.h"
#include "thread/Task.h"
#include <sys/types.h>

#define OPEN_READ 1
#define OPEN_WRITE 2
#define OPEN_NONBLOCK 4
#define OPEN_CLOEXEC 8
#define OPEN_DIRECTORY 16
#define OPEN_TRUNCATED 32
#define OPEN_CREATE 64
#define OPEN_APPEND 128
#define OPEN_EXCL 256

#define SEEK_SET 0
#define SEEK_CUR 1
#define SEEK_END 2

#define FCNTL_DUPFD 0
#define FCNTL_ISTTY 1
#define FCNTL_GETFD 2
#define FCNTL_SETFD 3

#define FD_CLOEXEC 1

void sys_fcntl(Context* context, int fd, int command, uintptr_t arg)
{
    Task* current_task = Scheduler::current_task();
    int err;
    Descriptor* file = current_task->open_descriptor_from_fd(fd, err);
    if (!file)
    {
        context->rax = -err;
        return;
    }
    if (command == FCNTL_DUPFD)
    {
        int minfd = (int)arg;
        if (minfd < 0 || minfd >= TASK_MAX_FDS)
        {
            context->rax = -EINVAL;
            return;
        }
        int dupfd = current_task->alloc_fd_greater_than_or_equal(minfd);
        if (dupfd < 0)
        {
            context->rax = -EMFILE;
            return;
        }
        current_task->files[dupfd] = *file;
        context->rax = dupfd;
        kdbgln("fcntl(F_DUPFD): duplicated fd %d, result is %d", fd, dupfd);
        return;
    }
    else if (command == FCNTL_ISTTY)
    {
        VFS::Node* node = file->node();
        if (node->tty) { context->rax = 1; }
        else
            context->rax = -ENOTTY;
        return;
    }
    else if (command == FCNTL_GETFD)
    {
        int flags = 0;
        if (file->close_on_exec()) context->rax |= FD_CLOEXEC;
        context->rax = flags;
        return;
    }
    else if (command == FCNTL_SETFD)
    {
        int flags = (int)arg;
        if (flags & FD_CLOEXEC) file->set_close_on_exec(true);
        else
            file->set_close_on_exec(false);
        context->rax = 0;
        return;
    }
    else
    {
        context->rax = -EINVAL;
        return;
    }
}

void sys_seek(Context* context, int fd, long offset, int whence)
{
    if (whence < SEEK_SET || whence > SEEK_END)
    {
        context->rax = -EINVAL;
        return;
    }
    int err;
    Descriptor* file = Scheduler::current_task()->open_descriptor_from_fd(fd, err);
    if (!file)
    {
        context->rax = -err;
        return;
    }
    long new_offset;
    if (whence == SEEK_SET) new_offset = offset;
    else if (whence == SEEK_CUR)
        new_offset = offset + file->offset();
    else if (whence == SEEK_END)
        new_offset = file->length() + offset;
    else
        __builtin_unreachable();
    if (new_offset < 0)
    {
        context->rax = -EINVAL; // FIXME: Is this the right error?
        return;
    }
    if (new_offset == file->offset())
    {
        context->rax = new_offset;
        return;
    }
    int result = file->seek(new_offset);
    if (result < 0)
    {
        context->rax = result;
        return;
    }
    context->rax = new_offset;
    return;
}

void sys_write(Context* context, int fd, size_t size, const char* addr)
{
    if (!validate_user_read((uintptr_t)addr, size))
    {
        context->rax = -EFAULT;
        return;
    }
    int err;
    Descriptor* file = Scheduler::current_task()->open_descriptor_from_fd(fd, err);
    if (!file)
    {
        context->rax = -err;
        return;
    }
    if (!file->can_write())
    {
        context->rax = -EBADF;
        return;
    }
    ssize_t result = file->user_write(size, addr);
    context->rax = (size_t)result;
    return;
}

void sys_open(Context* context, const char* filename, int flags, mode_t) // FIXME: mode is not used.
{
    Task* current_task = Scheduler::current_task();
    int fd = current_task->alloc_fd();
    if (fd < 0)
    {
        context->rax = -EMFILE;
        return;
    }

    auto result = strdup_from_user(filename);
    if (result.has_error())
    {
        context->rax = -result.error();
        return;
    }
    char* kfilename = result.release_value();

    VFS::Node* node = VFS::resolve_path(kfilename);
    if (!node)
    {
        bool create = (flags & OPEN_CREATE) > 0;
        if (create) kwarnln("FIXME: open(O_CREAT) is not implemented");
        kfree(kfilename);
        context->rax = -ENOENT;
        return;
    }
    else
    {
        bool excl = (flags & OPEN_EXCL) > 0;

        if (excl)
        {
            kfree(kfilename);
            context->rax = -EEXIST;
            return;
        }
    }

    bool can_read = (flags & OPEN_READ) > 0;
    bool can_write = (flags & OPEN_WRITE) > 0;
    if (!can_read && !can_write)
    {
        kfree(kfilename);
        context->rax = -EINVAL;
        return;
    }

    if (can_read && !VFS::can_read(node, current_task->euid, current_task->egid))
    {
        kwarnln("open failed because process with uid %d and gid %d couldn't open file %s with mode %d for reading",
                current_task->euid, current_task->egid, kfilename, node->mode);
        kfree(kfilename);
        context->rax = -EACCES;
        return;
    }

    if (can_write && !VFS::can_write(node, current_task->euid, current_task->egid))
    {
        kwarnln("open failed because process with uid %d and gid %d couldn't open file %s with mode %d for writing",
                current_task->euid, current_task->egid, kfilename, node->mode);
        kfree(kfilename);
        context->rax = -EACCES;
        return;
    }

    bool able_to_block = (flags & OPEN_NONBLOCK) == 0;
    bool close_on_exec = (flags & OPEN_CLOEXEC) > 0;

    bool only_directory = (flags & OPEN_DIRECTORY) > 0;

    bool truncate = (flags & OPEN_TRUNCATED) > 0;
    if (truncate)
    {
        kfree(kfilename);
        kerrorln("FIXME: open(O_TRUNC) is not implemented");
        context->rax = -ENOTSUP;
        return;
    }

    bool append = (flags & OPEN_APPEND) > 0;

    if (only_directory && node->type != VFS_DIRECTORY)
    {
        kfree(kfilename);
        context->rax = -ENOTDIR;
        return;
    }

    kdbgln("open(): opening %s %s, allocated file descriptor %d", kfilename,
           (can_read && can_write) ? "rw"
           : can_read              ? "r-"
                                   : "-w",
           fd);

    kfree(kfilename);
    current_task->files[fd].open(node, can_read, can_write, able_to_block, close_on_exec);

    if (append && current_task->files[fd].node()->type != VFS_DEVICE)
    {
        current_task->files[fd].seek((long)current_task->files[fd].length());
    }

    context->rax = fd;
    return;
}

void sys_read(Context* context, int fd, size_t size, char* buffer)
{
    if (!validate_user_write((uintptr_t)buffer, size))
    {
        context->rax = -EFAULT;
        return;
    }
    int err;
    Descriptor* file = Scheduler::current_task()->open_descriptor_from_fd(fd, err);
    if (!file)
    {
        context->rax = -err;
        return;
    }
    if (!file->can_read())
    {
        context->rax = -EBADF;
        return;
    }
    if (VFS::would_block(file->node()))
    {
        if (!file->able_to_block())
        {
            context->rax = -EAGAIN;
            return;
        }
        Task* current_task = Scheduler::current_task();
        current_task->state = current_task->Blocking;
        current_task->block_reason = BlockReason::Reading;
        current_task->blocking_read_info.fd = fd;
        current_task->blocking_read_info.buf = buffer;
        current_task->blocking_read_info.size = size;
        return Scheduler::task_yield(context);
    }
    ssize_t result = file->user_read(size, buffer);
    context->rax = (size_t)result;
    return;
}

void sys_close(Context* context, int fd)
{
    int err;
    Descriptor* file = Scheduler::current_task()->open_descriptor_from_fd(fd, err);
    if (!file)
    {
        context->rax = -err;
        return;
    }
    kdbgln("close(): releasing file descriptor %d", fd);
    file->close();
    context->rax = 0;
    return;
}

void sys_mkdir(Context* context, const char* filename, mode_t mode)
{
    auto result = strdup_from_user(filename);
    if (result.has_error())
    {
        context->rax = -result.error();
        return;
    }
    char* kfilename = result.release_value();

    Task* current_task = Scheduler::current_task();

    int rc = VFS::do_mkdir(kfilename, current_task->euid, current_task->egid, mode & (~current_task->umask));

    kfree(kfilename);

    context->rax = rc;
}

void sys_access(Context* context, const char* path, int) // FIXME: Use the amode argument.
{
    auto result = strdup_from_user(path);
    if (result.has_error())
    {
        context->rax = -result.error();
        return;
    }
    char* kpath = result.release_value();
    if (!VFS::exists(kpath)) { context->rax = -ENOENT; }
    else
        context->rax = 0;
    kfree(kpath);
}

void sys_dup2(Context* context, int fd, int fd2)
{
    int err;
    Descriptor* file1 = Scheduler::current_task()->open_descriptor_from_fd(fd, err);
    if (!file1)
    {
        context->rax = -err;
        return;
    }
    Descriptor* file2 = Scheduler::current_task()->descriptor_from_fd(fd2, err);
    if (!file2)
    {
        context->rax = -err;
        return;
    }
    if (file2->is_open()) file2->close();
    *file2 = *file1;
    kinfoln("dup2(): overwrote fd %d with fd %d", fd2, fd);
    context->rax = fd2;
}

void sys_umask(Context* context, mode_t cmask)
{
    Task* current_task = Scheduler::current_task();
    context->rax = current_task->umask;
    current_task->umask = cmask;
}

void sys_ioctl(Context* context, int fd, int cmd, uintptr_t arg)
{
    int err;
    Descriptor* file = Scheduler::current_task()->open_descriptor_from_fd(fd, err);
    if (!file)
    {
        context->rax = -err;
        return;
    }
    kinfoln("ioctl(): fd %d, cmd %d, arg %lu", fd, cmd, arg);
    context->rax = file->ioctl(cmd, arg);
}