#include "Log.h"
#include "fs/VFS.h"
#include "memory/MemoryManager.h"
#include "sys/Syscall.h"
#include "thread/ELF.h"
#include "thread/Scheduler.h"
#include "thread/ThreadImage.h"
#include <bits/modes.h>
#include <bits/open-flags.h>
#include <luna/CString.h>
#include <luna/ScopeGuard.h>
#include <luna/Vector.h>

static Result<Vector<String>> copy_string_vector_from_userspace(u64 address)
{
    Vector<String> result;

    const u64* user_vector = (const u64*)address;

    u64 string_addr;
    while (true)
    {
        if (!MemoryManager::copy_from_user_typed(user_vector, &string_addr)) return err(EFAULT);
        if (!string_addr) break;

        auto string = TRY(MemoryManager::strdup_from_user(string_addr));
        TRY(result.try_append(move(string)));
        user_vector++;
    }

    return result;
}

static u64 calculate_userspace_stack_size(const Vector<String>& v)
{
    u64 total { 0 };

    for (const auto& str : v)
    {
        // The string's byte count + a terminating NUL byte.
        total += str.length() + 1;
        // The pointer to said string in the userspace array.
        total += sizeof(char*);
    }

    // The NULL pointer at the end of the userspace array.
    total += sizeof(char*);

    return total;
}

static constexpr usize MAX_ARGV_STACK_SIZE = 2 * ARCH_PAGE_SIZE;

Result<u64> sys_execve(Registers* regs, SyscallArgs args)
{
    auto path = TRY(MemoryManager::strdup_from_user(args[0]));
    auto argv = TRY(copy_string_vector_from_userspace(args[1]));
    auto envp = TRY(copy_string_vector_from_userspace(args[2]));

    if ((calculate_userspace_stack_size(argv) + calculate_userspace_stack_size(envp)) > MAX_ARGV_STACK_SIZE)
        return err(E2BIG);

    auto current = Scheduler::current();

    auto inode = TRY(VFS::resolve_path(path.chars(), current->auth, current->current_directory));

    if (!VFS::can_execute(inode, current->auth)) return err(EACCES);

    kinfoln("exec: attempting to replace current image with %s", path.chars());

    auto guard = make_scope_guard([current] { MMU::switch_page_directory(current->directory); });

    auto image = TRY(ThreadImage::try_load_from_elf(inode));

    u64 user_argv = TRY(image->push_string_vector_on_stack(argv));
    usize user_argc = argv.size();

    u64 user_envp = TRY(image->push_string_vector_on_stack(envp));
    usize user_envc = envp.size();

    // From now on, nothing should fail.

    kinfoln("exec: image load ok, will now replace existing process image");

    guard.deactivate();

    for (int i = 0; i < FD_MAX; i++)
    {
        auto& descriptor = current->fd_table[i];
        if (!descriptor.has_value()) continue;
        if (descriptor->flags & O_CLOEXEC)
        {
            descriptor->inode->remove_handle();
            descriptor = {};
        }
    }

    MMU::delete_userspace_page_directory(current->directory);

    if (VFS::is_setuid(inode)) current->auth.euid = current->auth.suid = inode->uid();
    if (VFS::is_setgid(inode)) current->auth.egid = current->auth.sgid = inode->gid();

    current->name = path.chars();

    image->apply(current);

    MMU::switch_page_directory(current->directory);

    current->set_arguments(user_argc, user_argv, user_envc, user_envp);

    memcpy(regs, &current->regs, sizeof(*regs));

    return 0;
}

Result<u64> sys_fork(Registers* regs, SyscallArgs)
{
    auto current = Scheduler::current();

    auto guard = make_scope_guard([current] { MMU::switch_page_directory(current->directory); });

    memcpy(&current->regs, regs, sizeof(*regs));

    auto current_directory_path = TRY(current->current_directory_path.clone());

    auto image = TRY(ThreadImage::clone_from_thread(current));

    auto thread = TRY(new_thread());

    thread->state = ThreadState::Runnable;
    thread->is_kernel = false;
    thread->fp_data.save();
    thread->name = current->name;
    thread->auth = current->auth;
    thread->current_directory = current->current_directory;
    thread->current_directory_path = move(current_directory_path);
    thread->umask = current->umask;
    thread->parent = current;

    for (int i = 0; i < FD_MAX; i++)
    {
        thread->fd_table[i] = current->fd_table[i];
        if (current->fd_table[i].has_value()) current->fd_table[i]->inode->add_handle();
    }

    image->apply(thread);

    memcpy(&thread->regs, regs, sizeof(*regs));

    thread->set_return(0);

    Scheduler::add_thread(thread);

    kinfoln("fork: thread %lu forked into child %lu", current->id, thread->id);

    return thread->id;
}