/*
 * syscalls.c
 *
 * Copyright (C) 2016 Aleksandar Andrejevic <theflash@sdf.lonestar.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <syscalls.h>
#include <thread.h>
#include <heap.h>
#include <memory.h>
#include <device.h>
#include <process.h>
#include <thread.h>
#include <timer.h>
#include <power.h>
#include <exception.h>
#include <pipe.h>
#include <semaphore.h>

extern sysret_t syscall_function(const void*, dword_t*, dword_t);

#include "syscall_table.inl"

static void system_service_handler(registers_t *regs, byte_t int_num)
{
    dword_t parameters[MAX_PARAMETERS];

    if (regs->eax >= SERVICE_COUNT)
    {
        regs->eax = ERR_NOSYSCALL;
        return;
    }

    if (get_previous_mode() == USER_MODE)
    {
        if (!check_usermode((dword_t*)regs->edx, sizeof(parameters)))
        {
            regs->eax = ERR_BADPTR;
            return;
        }

        EH_TRY
        {
            memcpy(parameters, (dword_t*)regs->edx, sizeof(parameters));
        }
        EH_CATCH
        {
            regs->eax = ERR_BADPTR;
            EH_ESCAPE(return);
        }
        EH_DONE;
    }
    else
    {
        memcpy(parameters, (dword_t*)regs->edx, sizeof(parameters));
    }

    sysret_t result = syscall_function(service_table[regs->eax], parameters, sizeof(parameters));
    regs->eax = (dword_t)result;
    regs->edx = (dword_t)(result >> 32);
}

processor_mode_t get_previous_mode()
{
    thread_t *thread = get_current_thread();
    return thread ? thread->previous_mode : KERNEL_MODE;
}

char *copy_user_string(const char *string)
{
    int length = 0;
    ASSERT(get_previous_mode() == USER_MODE);

    EH_TRY length = strlen(string);
    EH_CATCH length = -1;
    EH_DONE;

    if (length == -1) return NULL;
    if (!check_usermode(string, length + 1)) return NULL;

    char *result = (char*)malloc(length + 1);
    if (result == NULL) return NULL;

    EH_TRY
    {
        strcpy(result, string);
    }
    EH_CATCH
    {
        free(result);
        result = NULL;
    }
    EH_DONE;

    return result;
}

sysret_t syscall(syscall_number_t num, ...)
{
    int i;
    qword_t ret = 0ULL;
    dword_t parameters[MAX_PARAMETERS];
    thread_t *thread = get_current_thread();

    if (num >= SERVICE_COUNT) return ERR_NOSYSCALL;

    va_list params;
    va_start(params, num);
    for (i = 0; i < MAX_PARAMETERS; i++) parameters[i] = va_arg(params, dword_t);
    va_end(params);

    processor_mode_t old_mode = get_previous_mode();
    if (thread) thread->previous_mode = KERNEL_MODE;
    ret = syscall_function(service_table[num], parameters, sizeof(parameters));
    if (thread) thread->previous_mode = old_mode;

    return ret;
}

bool_t check_usermode(const void *pointer, dword_t size)
{
    dword_t first_addr = PAGE_ALIGN((dword_t)pointer);
    dword_t last_addr  = PAGE_ALIGN((dword_t)pointer + size - 1);

    if (first_addr >= USER_AREA_START && last_addr <= USER_AREA_END) return TRUE;
    else return FALSE;
}

void syscalls_init()
{
    set_int_handler(SYSCALL_INTERRUPT, system_service_handler, TRUE, TRUE);
}
