/*
 * device.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 <device.h>
#include <filesystem.h>
#include <memory.h>
#include <heap.h>
#include <syscalls.h>
#include <exception.h>

static DECLARE_LIST(block_dev_drivers);
static DECLARE_LIST(char_dev_drivers);
static DECLARE_LIST(block_devices);
static DECLARE_LIST(char_devices);
static DECLARE_LOCK(block_dev_driver_list_lock);
static DECLARE_LOCK(char_dev_driver_list_lock);
static DECLARE_LOCK(block_device_list_lock);
static DECLARE_LOCK(char_device_list_lock);

dword_t device_mount(const char *device, const char *mountpoint, dword_t flags);
dword_t device_unmount(mounted_volume_t *volume);
dword_t device_load_file(file_t **file);
dword_t device_unload_file(file_t *file);
dword_t device_open_file(file_instance_t **instance);
dword_t device_close_file(file_instance_t *instance);
dword_t device_delete_file(mounted_volume_t *volume, const char *path, bool_t purge);
dword_t device_read_file(file_instance_t *file, void *buffer, qword_t offset, size_t length, size_t *bytes_read);
dword_t device_write_file(file_instance_t *file, const void *buffer, qword_t offset, size_t length, size_t *bytes_written);
dword_t device_list_dir(file_instance_t *directory, char *filename, bool_t continue_scan);
dword_t device_set_file(file_t *file, dword_t info_type, const void *buffer, size_t size);

static fs_driver_t device_fs_driver =
{
    .name        = "DeviceFS",
    .mount       = device_mount,
    .unmount     = device_unmount,
    .load_file   = device_load_file,
    .unload_file = device_unload_file,
    .open_file   = device_open_file,
    .close_file  = device_close_file,
    .delete_file = device_delete_file,
    .read_file   = device_read_file,
    .write_file  = device_write_file,
    .list_dir    = device_list_dir,
    .set_file    = device_set_file
};


static mounted_volume_t block_device_volume =
{
    .mountpoint = "BlockDevices",
    .flags      = 0,
    .lock       = {0},
    .device     = NULL,
    .open_files = 0ULL,
    .driver     = &device_fs_driver
};

static mounted_volume_t char_device_volume =
{
    .mountpoint = "CharDevices",
    .flags      = 0,
    .lock       = {0},
    .device     = NULL,
    .open_files = 0ULL,
    .driver     = &device_fs_driver
};

device_t *get_block_device(const char *name)
{
    list_entry_t *i;
    device_t *ptr = NULL;
    lock_acquire_shared(&block_device_list_lock);

    for (i = block_devices.next; i != &block_devices; i = i->next)
    {
        ptr = CONTAINER_OF(i, device_t, list);
        if (strcmp(ptr->name, name) == 0) break;
    }

    if (i == &block_devices) ptr = NULL;

    lock_release(&block_device_list_lock);
    return ptr;
}

dword_t register_block_dev_driver(block_dev_driver_t *driver)
{
    dword_t ret = driver->init_proc();

    if (ret == ERR_SUCCESS)
    {
        driver->mounted_devices = 0;
        lock_acquire(&block_dev_driver_list_lock);
        list_append(&block_dev_drivers, &driver->list);
        lock_release(&block_dev_driver_list_lock);
    }

    return ret;
}

dword_t unregister_block_dev_driver(block_dev_driver_t *driver)
{
    if (driver->mounted_devices) return ERR_BUSY;

    driver->cleanup_proc();
    lock_acquire(&block_dev_driver_list_lock);
    list_remove(&driver->list);
    lock_release(&block_dev_driver_list_lock);

    return ERR_SUCCESS;
}

dword_t register_block_device(device_t *device)
{
    dword_t ret = ERR_SUCCESS;
    list_entry_t *i;

    device->type = BLOCK_DEVICE;

    lock_acquire(&block_device_list_lock);

    for (i = block_devices.next; i != &block_devices; i = i->next)
    {
        device_t *ptr = CONTAINER_OF(i, device_t, list);

        if (strcmp(ptr->name, device->name) == 0)
        {
            ret = ERR_EXISTS;
            goto cleanup;
        }
    }

    list_append(&block_devices, &device->list);

cleanup:
    lock_release(&block_device_list_lock);
    return ret;
}

dword_t unregister_block_device(device_t *device)
{
    lock_acquire(&block_device_list_lock);
    list_remove(&device->list);
    lock_release(&block_device_list_lock);

    return ERR_SUCCESS;
}

device_t *get_char_device(const char *name)
{
    list_entry_t *i;
    device_t *device;

    lock_acquire_shared(&block_device_list_lock);
    for (i = char_devices.next; i != &char_devices; i = i->next)
    {
        device = CONTAINER_OF(i, device_t, list);
        if (strcmp(device->name, name) == 0) break;
    }
    lock_release(&block_device_list_lock);

    return (i != &char_devices) ? device : NULL;
}

dword_t register_char_dev_driver(char_dev_driver_t *driver)
{
    dword_t ret = driver->init_proc();

    if (ret == ERR_SUCCESS)
    {
        lock_acquire(&char_dev_driver_list_lock);
        list_append(&char_dev_drivers, &driver->list);
        lock_release(&char_dev_driver_list_lock);
    }

    return ret;
}

dword_t unregister_char_dev_driver(char_dev_driver_t *driver)
{
    driver->cleanup_proc();
    lock_acquire(&char_dev_driver_list_lock);
    list_remove(&driver->list);
    lock_release(&char_dev_driver_list_lock);

    return ERR_SUCCESS;
}

dword_t register_char_device(device_t *device)
{
    dword_t ret = ERR_SUCCESS;
    list_entry_t *i;

    device->type = CHAR_DEVICE;

    lock_acquire(&char_device_list_lock);

    for (i = char_devices.next; i != &char_devices; i = i->next)
    {
        device_t *ptr = CONTAINER_OF(i, device_t, list);

        if (strcmp(ptr->name, device->name) == 0)
        {
            ret = ERR_EXISTS;
            goto cleanup;
        }
    }

    list_append(&char_devices, &device->list);

cleanup:
    lock_release(&char_device_list_lock);
    return ret;
}

dword_t unregister_char_device(device_t *device)
{
    lock_acquire(&char_device_list_lock);
    list_remove(&device->list);
    lock_release(&char_device_list_lock);

    return ERR_SUCCESS;
}

dword_t device_mount(const char *device, const char *mountpoint, dword_t flags)
{
    return ERR_INVALID;
}

dword_t device_unmount(mounted_volume_t *volume)
{
    return ERR_INVALID;
}

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

    if (strlen(file->header.path) > 0)
    {
        if (file->header.volume == &block_device_volume)
        {
            file->device = get_block_device(file->header.path);
            return file->device ? ERR_SUCCESS : ERR_NOTFOUND;
        }
        else if (file->header.volume == &char_device_volume)
        {
            file->device = get_char_device(file->header.path);
            return file->device ? ERR_SUCCESS : ERR_NOTFOUND;
        }
        else
        {
            return ERR_INVALID;
        }
    }
    else
    {
        file->device = NULL;
        return ERR_SUCCESS;
    }
}

dword_t device_unload_file(file_t *file)
{
    return ERR_SUCCESS;
}

dword_t device_open_file(file_instance_t **_instance)
{
    device_file_t *file = (device_file_t*)(*_instance)->global;

    if (!file->device)
    {
        device_dir_inst_t *instance = (device_dir_inst_t*)realloc(*_instance, sizeof(device_dir_inst_t));
        if (instance == NULL) return ERR_NOMEMORY;
        *_instance = &instance->header;
    }

    return ERR_SUCCESS;
}

dword_t device_close_file(file_instance_t *instance)
{
    return ERR_SUCCESS;
}

dword_t device_delete_file(mounted_volume_t *volume, const char *path, bool_t purge)
{
    return ERR_INVALID;
}

dword_t device_read_file(file_instance_t *_file, void *buffer, qword_t offset, size_t length, size_t *bytes_read)
{
    device_file_t *file = CONTAINER_OF(_file->global, device_file_t, header);
    return device_read(file->device, buffer, offset, length, bytes_read);
}

dword_t device_write_file(file_instance_t *_file, const void *buffer, qword_t offset, size_t length, size_t *bytes_written)
{
    device_file_t *file = CONTAINER_OF(_file->global, device_file_t, header);
    return device_write(file->device, buffer, offset, length, bytes_written);
}

dword_t device_list_dir(file_instance_t *directory, char *filename, bool_t continue_scan)
{
    device_file_t *file = CONTAINER_OF(directory->global, device_file_t, header);
    if (file->device) return ERR_ISNOTDIR;

    device_dir_inst_t *instance = CONTAINER_OF(directory, device_dir_inst_t, header);

    if (!continue_scan)
    {
        instance->next_device = CONTAINER_OF(file->header.volume == &block_device_volume
                                             ? block_devices.next : char_devices.next,
                                             device_t,
                                             list);
    }

    if (&instance->next_device->list == &block_devices
        || &instance->next_device->list == &char_devices)
    {
        return ERR_NOMORE;
    }

    strcpy(filename, instance->next_device->name);
    instance->next_device = CONTAINER_OF(instance->next_device->list.next, device_t, list);

    return ERR_SUCCESS;
}

dword_t device_set_file(file_t *file, dword_t info_type, const void *buffer, size_t size)
{
    return ERR_INVALID;
}

dword_t device_read(device_t *device, void *buffer, qword_t offset, size_t length, size_t *bytes_read)
{
    dword_t ret;
    lock_acquire_shared(&device->lock);

    if (device->type == BLOCK_DEVICE)
    {
        ret = device->block_driver->read_proc(device, buffer, offset, length, bytes_read);
    }
    else if (device->type == CHAR_DEVICE)
    {
        ret = device->char_driver->read_proc(device, buffer, length, bytes_read);
    }
    else
    {
        ret = ERR_INVALID;
    }

    lock_release(&device->lock);
    return ret;
}

dword_t device_write(device_t *device, const void *buffer, qword_t offset, size_t length, size_t *bytes_written)
{
    dword_t ret;
    lock_acquire(&device->lock);

    if (device->type == BLOCK_DEVICE)
    {
        ret = device->block_driver->write_proc(device, buffer, offset, length, bytes_written);
    }
    else if (device->type == CHAR_DEVICE)
    {
        ret = device->char_driver->write_proc(device, buffer, length, bytes_written);
    }
    else
    {
        ret = ERR_INVALID;
    }

    lock_release(&device->lock);
    return ret;
}

dword_t device_ioctl_internal(device_t *device, dword_t control_code, const void *in_buffer, size_t in_length, void *out_buffer, size_t out_length)
{
    dword_t ret;
    lock_acquire(&device->lock);

    if (device->type == BLOCK_DEVICE)
    {
        ret = device->block_driver->ioctl_proc(device, control_code, in_buffer, in_length, out_buffer, out_length);
    }
    else if (device->type == CHAR_DEVICE)
    {
        ret = device->char_driver->ioctl_proc(device, control_code, in_buffer, in_length, out_buffer, out_length);
    }
    else
    {
        ret = ERR_INVALID;
    }

    lock_release(&device->lock);
    return ret;
}

sysret_t syscall_device_ioctl(handle_t device, dword_t control_code, const void *in_buffer, size_t in_length, void *out_buffer, size_t out_length)
{
    dword_t ret = ERR_SUCCESS;
    byte_t *safe_in_buffer = NULL;
    byte_t *safe_out_buffer = NULL;

    if (get_previous_mode() == USER_MODE)
    {
        if (in_buffer && !check_usermode(in_buffer, in_length)) return ERR_BADPTR;
        if (out_buffer && !check_usermode(out_buffer, out_length)) return ERR_BADPTR;

        if (in_buffer)
        {
            if ((safe_in_buffer = (byte_t*)malloc(in_length)) == NULL)
            {
                ret = ERR_NOMEMORY;
                goto cleanup;
            }

            EH_TRY
            {
                memcpy(safe_in_buffer, in_buffer, in_length);
            }
            EH_CATCH
            {
                ret = ERR_BADPTR;
                EH_ESCAPE(goto cleanup);
            }
            EH_DONE;
        }

        if (out_buffer)
        {
            if ((safe_out_buffer = (byte_t*)malloc(out_length)) == NULL)
            {
                ret = ERR_NOMEMORY;
                goto cleanup;
            }
        }
    }
    else
    {
        safe_in_buffer = (byte_t*)in_buffer;
        safe_out_buffer = out_buffer;
    }

    file_instance_t *instance;

    if (!reference_by_handle(device, OBJECT_FILE_INSTANCE, (object_t**)&instance))
    {
        ret = ERR_INVALID;
        goto cleanup;
    }

    device_file_t *file = CONTAINER_OF(instance->global, device_file_t, header);
    ret = device_ioctl_internal(file->device, control_code, safe_in_buffer, in_length, safe_out_buffer, out_length);

cleanup:
    if (get_previous_mode() == USER_MODE)
    {
        EH_TRY
        {
            if (safe_out_buffer) memcpy(out_buffer, safe_out_buffer, out_length);
        }
        EH_CATCH ret = ERR_BADPTR;
        EH_DONE;

        if (safe_in_buffer) free(safe_in_buffer);
        if (safe_out_buffer) free(safe_out_buffer);
    }

    return ret;
}

void device_init(void)
{
    register_filesystem_driver(&device_fs_driver);
    register_mounted_volume(&block_device_volume);
    register_mounted_volume(&char_device_volume);
}
