/*
 * Memory-resident Virtual Filesystem Driver
 * main.c
 *
 * Copyright (C) 2017 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 "ram.h"
#include <heap.h>
#include <syscalls.h>

const char driver_name[] = "ramfs";

static dword_t ramfs_mount(const char *device, const char *mountpoint, dword_t flags);
static dword_t ramfs_unmount(mounted_volume_t *volume);
static dword_t ramfs_load_file(file_t **file);
static dword_t ramfs_unload_file(file_t *file);
static dword_t ramfs_open_file(file_instance_t **instance);
static dword_t ramfs_close_file(file_instance_t *instance);
static dword_t ramfs_delete_file(mounted_volume_t *volume, const char *path, bool_t purge);
static dword_t ramfs_read_file(file_instance_t *file, void *buffer, qword_t offset, size_t length, size_t *bytes_read);
static dword_t ramfs_write_file(file_instance_t *file, const void *buffer, qword_t offset, size_t length, size_t *bytes_written);
static dword_t ramfs_list_dir(file_instance_t *file, char *filename, bool_t continue_scan);
static dword_t ramfs_set_file(file_t *file, dword_t info_type, const void *buffer, size_t size);

static fs_driver_t ramfs_driver =
{
    .name = "RAM",
    .mount = ramfs_mount,
    .unmount = ramfs_unmount,
    .load_file = ramfs_load_file,
    .unload_file = ramfs_unload_file,
    .open_file = ramfs_open_file,
    .close_file = ramfs_close_file,
    .delete_file = ramfs_delete_file,
    .read_file = ramfs_read_file,
    .write_file = ramfs_write_file,
    .list_dir = ramfs_list_dir,
    .set_file = ramfs_set_file,
};

static ramfs_node_t *ramfs_get_entry(ramfs_volume_t *volume, const char *path)
{
    char path_copy[MAX_PATH];
    char *token, *endptr;
    strcpy(path_copy, path);

    ramfs_node_t *current = &volume->root;

    for (token = strtok_r(path_copy, PATH_DELIMITER_STRING, &endptr);
         token != NULL;
         token = strtok_r(NULL, PATH_DELIMITER_STRING, &endptr))
    {
        if (*token == '\0' || !(current->attributes & FILE_ATTR_DIRECTORY)) return NULL;

        list_entry_t *ptr;
        list_entry_t *files = &current->files;

        for (ptr = files->next; ptr != files; ptr = ptr->next)
        {
            current = CONTAINER_OF(ptr, ramfs_node_t, list);
            if (strcmp(current->name, token) == 0) break;
        }

        if (ptr == files) return NULL;
    }

    return current;
}

static void ramfs_purge_file(ramfs_volume_t *volume, ramfs_node_t *node)
{
    volume->max_size += strlen(node->name) + 1;
    free(node->name);
    node->name = NULL;

    if (!(node->attributes & FILE_ATTR_DIRECTORY))
    {
        volume->max_size += node->size;
        free(node->contents);
    }

    volume->max_size += sizeof(ramfs_node_t);
    free(node);
}

static dword_t ramfs_mount(const char *device, const char *mountpoint, dword_t flags)
{
    char *endptr;
    qword_t size = strtoull(device, &endptr, 10);
    if (!size) return ERR_INVALID;

    switch (*endptr)
    {
    case 'K':
        size *= 1024ULL;
        break;

    case 'M':
        size *= 1048576ULL;
        break;

    case 'G':
        size *= 1073741824ULL;
        break;
    }

    ramfs_volume_t *volume = (ramfs_volume_t*)malloc(sizeof(ramfs_volume_t));
    if (volume == NULL) return ERR_NOMEMORY;

    volume->header.mountpoint = strdup(mountpoint);
    volume->header.driver = &ramfs_driver;
    volume->header.device = NULL;
    volume->max_size = size;
    list_init(&volume->root.list);
    volume->root.name = "";
    volume->root.attributes = FILE_ATTR_DIRECTORY;
    list_init(&volume->root.files);

    dword_t ret = register_mounted_volume(&volume->header);
    if (ret != ERR_SUCCESS) free(volume);
    return ret;
}

static void ramfs_release_node_recursive(ramfs_node_t *node)
{
    if (node->attributes & FILE_ATTR_DIRECTORY)
    {
        while (node->files.next != &node->files)
        {
            ramfs_node_t *child_node = CONTAINER_OF(node->files.next, ramfs_node_t, list);
            ramfs_release_node_recursive(child_node);
            list_remove(&child_node->list);
            heap_free(&evictable_heap, child_node);
        }
    }
    else
    {
        heap_free(&evictable_heap, node->contents);
        node->contents = NULL;
        node->size = 0ULL;
    }

    heap_free(&evictable_heap, node->name);
    node->name = NULL;
}

static dword_t ramfs_unmount(mounted_volume_t *_volume)
{
    ramfs_volume_t *volume = CONTAINER_OF(_volume, ramfs_volume_t, header);
    ramfs_release_node_recursive(&volume->root);

    unregister_mounted_volume(&volume->header);
    free(volume->header.mountpoint);
    free(volume);
    return ERR_SUCCESS;
}

static dword_t ramfs_load_file(file_t **_file)
{
    ramfs_file_t *file = realloc(*_file, sizeof(ramfs_file_t));
    if (file == NULL) return ERR_NOMEMORY;
    *_file = &file->header;

    ramfs_volume_t *volume = CONTAINER_OF(file->header.volume, ramfs_volume_t, header);
    ramfs_node_t *node = ramfs_get_entry(volume, file->header.path);

    if (node == NULL)
    {
        if (!(file->header.global_mode & FILE_MODE_CREATE)) return ERR_NOTFOUND;
        char parent_dir[MAX_PATH];

        strcpy(parent_dir, file->header.path);
        char *last_delimiter = strrchr(parent_dir, PATH_DELIMITER_CHAR);
        if (last_delimiter) *last_delimiter = '\0';
        else *parent_dir = '\0';

        ramfs_node_t *dir = ramfs_get_entry(volume, parent_dir);
        if (dir == NULL || !(dir->attributes & FILE_ATTR_DIRECTORY)) return ERR_ISNOTDIR;

        node = heap_alloc(&evictable_heap, sizeof(ramfs_node_t));
        if (node == NULL) return ERR_DISKFULL;

        node->name = heap_alloc(&evictable_heap, strlen(last_delimiter + 1) + 1);
        strcpy(node->name, last_delimiter + 1);

        node->attributes = file->header.attributes;

        if (node->attributes & FILE_ATTR_DIRECTORY)
        {
            list_init(&node->files);
        }
        else
        {
            node->contents = NULL;
            node->size = 0;
        }

        list_append(&dir->files, &node->list);
    }

    file->header.attributes = node->attributes;
    file->node = node;

    return ERR_SUCCESS;
}

static dword_t ramfs_unload_file(file_t *_file)
{
    ramfs_file_t *file = CONTAINER_OF(_file, ramfs_file_t, header);
    ramfs_volume_t *volume = CONTAINER_OF(file->header.volume, ramfs_volume_t, header);

    if (file->header.attributes & FILE_ATTR_DELETED)
    {
        ramfs_purge_file(volume, file->node);
    }

    return ERR_SUCCESS;
}

static dword_t ramfs_open_file(file_instance_t **_instance)
{
    ramfs_file_instance_t *instance = realloc(*_instance, sizeof(ramfs_file_instance_t));
    if (instance == NULL) return ERR_NOMEMORY;
    *_instance = &instance->header;
    return ERR_SUCCESS;
}

static dword_t ramfs_close_file(file_instance_t *instance)
{
    return ERR_SUCCESS;
}

static dword_t ramfs_delete_file(mounted_volume_t *_volume, const char *path, bool_t purge)
{
    ramfs_volume_t *volume = CONTAINER_OF(volume, ramfs_volume_t, header);
    ramfs_node_t *node = ramfs_get_entry(volume, path);
    if (node == NULL) return ERR_NOTFOUND;

    list_remove(&node->list);
    if (purge) ramfs_purge_file(volume, node);

    return ERR_SUCCESS;
}

static dword_t ramfs_read_file(file_instance_t *_instance, void *buffer, qword_t offset, size_t length, size_t *bytes_read)
{
    ramfs_file_t *file = CONTAINER_OF(_instance->global, ramfs_file_t, header);
    if (length == 0) return ERR_SUCCESS;
    if ((offset + length) > file->node->size) return ERR_BEYOND;

    uintptr_t actual_length = MIN(length, file->node->size - offset);
    memcpy(buffer, &file->node->contents[(uintptr_t)offset], actual_length);
    *bytes_read = actual_length;

    return ERR_SUCCESS;
}

static dword_t ramfs_write_file(file_instance_t *_instance, const void *buffer, qword_t offset, size_t length, size_t *bytes_written)
{
    ramfs_file_t *file = CONTAINER_OF(_instance->global, ramfs_file_t, header);
    ramfs_volume_t *volume = CONTAINER_OF(file->header.volume, ramfs_volume_t, header);
    if (length == 0) return ERR_SUCCESS;

    if ((offset + length) > file->node->size)
    {
        if ((offset + length - file->node->size) > volume->max_size) return ERR_DISKFULL;

        byte_t *new_contents = heap_realloc(&evictable_heap, file->node->contents, offset + length);
        if (!new_contents) return ERR_DISKFULL;

        file->node->contents = new_contents;
        memset(&file->node->contents[file->node->size], 0, offset + length - file->node->size);
        file->node->size = offset + length;
        volume->max_size -= offset + length - file->node->size;
    }

    memcpy(&file->node->contents[(uintptr_t)offset], buffer, length);
    *bytes_written = length;
    return ERR_SUCCESS;
}

static dword_t ramfs_list_dir(file_instance_t *_instance, char *filename, bool_t continue_scan)
{
    ramfs_file_t *directory = CONTAINER_OF(_instance->global, ramfs_file_t, header);
    ramfs_file_instance_t *instance = CONTAINER_OF(_instance, ramfs_file_instance_t, header);
    if (!continue_scan) instance->dir_scan_node = directory->node->files.next;
    if (instance->dir_scan_node == &directory->node->files) return ERR_NOMORE;

    ramfs_node_t *node = CONTAINER_OF(instance->dir_scan_node, ramfs_node_t, list);
    strcpy(filename, node->name);

    instance->dir_scan_node = instance->dir_scan_node->next;
    return ERR_SUCCESS;
}

static dword_t ramfs_set_file(file_t *_file, dword_t info_type, const void *buffer, size_t size)
{
    ramfs_volume_t *volume = CONTAINER_OF(_file->volume, ramfs_volume_t, header);
    ramfs_file_t *file = CONTAINER_OF(_file, ramfs_file_t, header);

    switch (info_type)
    {
    case FILE_INFO_ATTRIBUTES:
    case FILE_INFO_TIME:
        // TODO
        return ERR_NOSYSCALL;

    case FILE_INFO_SIZE:
        if (size >= sizeof(qword_t))
        {
            qword_t new_size = *((qword_t*)buffer);
            if ((new_size - file->node->size) > volume->max_size) return ERR_DISKFULL;

            byte_t *new_contents = heap_realloc(&evictable_heap, file->node->contents, new_size);
            if (!new_contents) return ERR_DISKFULL;

            if (new_size > file->node->size)
            {
                memset(&file->node->contents[file->node->size], 0, new_size - file->node->size);
            }

            file->node->contents = new_contents;
            volume->max_size -= new_size - file->node->size;
            file->node->size = new_size;
        }
        else
        {
            return ERR_SMALLBUF;
        }

    default:
        return ERR_INVALID;
    }
}

dword_t driver_load(const char *parameters)
{
    register_filesystem_driver(&ramfs_driver);
    return ERR_SUCCESS;
}
