/*
 * start.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 <boot/multiboot.h>
#include <common.h>
#include <log.h>
#include <cpu.h>
#include <memory.h>
#include <heap.h>
#include <segments.h>
#include <interrupt.h>
#include <timer.h>
#include <clock.h>
#include <process.h>
#include <thread.h>
#include <exception.h>
#include <video.h>
#include <device.h>
#include <power.h>
#include <module.h>
#include <exec/elf.h>

#define DO_TASK(x, ...) x(__VA_ARGS__); advance_progress()

#define MAX_CMDLINE_LENGTH 256

static char cmdline[MAX_CMDLINE_LENGTH] = "";
static uintptr_t lowest_physical = 0;
static multiboot_tag_mmap_t *mmap = NULL;
static multiboot_tag_sections_t *kernel_sections = NULL;
static size_t tasks_completed = 0, num_tasks = 12;
static const char *manager_path = "";

bool_t video_initialized = FALSE;
const elf32_symbol_t *kernel_symtab = NULL;
dword_t kernel_symcount = 0;
const char *kernel_strtab = NULL;

void *get_kernel_symbol(const char *name)
{
    int i;

    for (i = 0; i < kernel_symcount; i++)
    {
        if (strcmp(name, &kernel_strtab[kernel_symtab[i].st_name]) == 0)
        {
            return (void*)kernel_symtab[i].st_value;
        }
    }

    return NULL;
}

const char *lookup_kernel_symbol_name(uintptr_t address)
{
    int i;

    for (i = 0; i < kernel_symcount; i++)
    {
        if (address >= kernel_symtab[i].st_value && address < (kernel_symtab[i].st_value + kernel_symtab[i].st_size))
        {
            return &kernel_strtab[kernel_symtab[i].st_name];
        }
    }

    return NULL;
}


static dword_t __attribute__((__noreturn__)) system_idle_thread(void *param)
{
    UNUSED_PARAMETER(param);

    while (TRUE)
    {
        syscall_yield_quantum();
        cpu_halt();
    }
}

static void pre_initialization(void)
{
    cpu_init();
    segments_init();
    interrupt_init();
    syscalls_init();
    irq_init();
    exceptions_init();
    object_init();
    cpu_enable_interrupts();
}

static void scan_multiboot_info(multiboot_tag_t *mboot)
{
    multiboot_tag_t *tag;
    for (tag = mboot; tag->type != MULTIBOOT_INFO_END; tag = (multiboot_tag_t*)(((uintptr_t)tag + tag->size + 7) & ~7))
    {
        switch (tag->type)
        {
        case MULTIBOOT_INFO_CMDLINE:
            strncpy(cmdline, ((multiboot_tag_cmdline_t*)tag)->string, sizeof(cmdline));
            break;

        case MULTIBOOT_INFO_MODULES:
            {
                multiboot_tag_module_t *module = (multiboot_tag_module_t*)tag;
                if (module->mod_end > lowest_physical) lowest_physical = module->mod_end;
                num_tasks++;
            }
            break;

        case MULTIBOOT_INFO_MEMORY_MAP:
            mmap = (multiboot_tag_mmap_t*)tag;
            break;

        case MULTIBOOT_INFO_SECTIONS:
            {
                kernel_sections = (multiboot_tag_sections_t*)tag;
                elf32_section_t *sections = (elf32_section_t*)(kernel_sections + 1);

                int i;
                for (i = 0; i < kernel_sections->num; i++)
                {
                    uintptr_t section_end = sections[i].sh_addr + sections[i].sh_size;
                    if (section_end < 0x80000000 && section_end > lowest_physical) lowest_physical = section_end;
                }
            }
            break;
        }
    }
}

static bool_t load_kernel_symbols()
{
    int i;
    const elf32_section_t *sections = (elf32_section_t*)(kernel_sections + 1);

    for (i = 0; i < kernel_sections->num; i++)
    {
        if (sections[i].sh_type == ELF_SECTION_TYPE_SYMTAB && !kernel_symtab)
        {
            if (map_memory((void*)sections[i].sh_addr, (void**)&kernel_symtab, sections[i].sh_size, MEMORY_BLOCK_ACCESSIBLE) != ERR_SUCCESS)
            {
                continue;
            }

            kernel_symcount = sections[i].sh_size / sizeof(elf32_symbol_t);
        }
        else if (sections[i].sh_type == ELF_SECTION_TYPE_STRTAB && i != kernel_sections->shndx && !kernel_strtab)
        {
            if (map_memory((void*)sections[i].sh_addr, (void**)&kernel_strtab, sections[i].sh_size, MEMORY_BLOCK_ACCESSIBLE) != ERR_SUCCESS)
            {
                continue;
            }
        }
    }

    if (!kernel_symtab || !kernel_strtab)
    {
        if (kernel_symtab) unmap_memory((void*)kernel_symtab);
        if (kernel_strtab) unmap_memory((void*)kernel_strtab);
        kernel_symcount = 0;
        return FALSE;
    }

    return TRUE;
}

static void setup_memory_management(void)
{
    if (!mmap) KERNEL_CRASH("The bootloader failed to supply a memory map");
    memory_init(mmap, PAGE_ALIGN_UP(lowest_physical));
    heap_init();

    map_memory_internal((void*)TEXT_VIDEO_MEMORY,
                        (void*)TEXT_VIDEO_MEMORY,
                        TEXT_HEIGHT * TEXT_WIDTH * sizeof(word_t),
                        PAGE_PRESENT | PAGE_WRITABLE);
}

static void parse_command_line(void)
{
    static char cmdline_copy[MAX_CMDLINE_LENGTH];
    strcpy(cmdline_copy, cmdline);

    char *token;
    for (token = strtok(cmdline_copy, " "); token != NULL; token = strtok(NULL, " "))
    {
        char *separator = strchr(token, ':');
        if (separator == NULL) continue;

        char *value = separator + 1;
        *separator = '\0';

        if (strcmp(token, "manager") == 0)
        {
            manager_path = value;
        }
        else if (strcmp(token, "debug") == 0)
        {
            char *ptr = strchr(value, ',');

            if (ptr)
            {
                *ptr = '\0';
                log_level_t min_level = *(ptr + 1) - '0';
                if (min_level >= LOG_DEBUG || min_level <= LOG_CRITICAL) debug_min_level = min_level;
            }

            debug_channel = value;
        }
    }
}

static handle_t start_system_manager(void)
{
    char *root_disk = strdup(manager_path);
    char *slash = strchr(root_disk, '/');
    if (slash == NULL) KERNEL_CRASH("Invalid or missing system manager path");
    *slash = 0;

    log_write(LOG_NORMAL, "Mounting root disk: %s\n", root_disk);
    dword_t ret = syscall_mount(root_disk, root_disk, NULL, MOUNT_FLAG_READONLY);
    if (ret != ERR_SUCCESS) KERNEL_CRASH("Cannot mount root disk");
    free(root_disk);

    handle_t manager_process;
    handle_t main_thread;

    process_params_t parameters;
    parameters.command_line = cmdline;
    parameters.standard_input = INVALID_HANDLE;
    parameters.standard_output = INVALID_HANDLE;
    parameters.standard_error = INVALID_HANDLE;

    log_write(LOG_NORMAL, "Starting the system manager: %s\n", manager_path);
    ret = syscall_create_process(manager_path, 0, &parameters, &manager_process, &main_thread);
    if (ret != ERR_SUCCESS) KERNEL_CRASH("Cannot start system manager");

    syscall_close_object(main_thread);
    return manager_process;
}

static void draw_progress_bar(void)
{
    static const char text[] = "The Monolithium kernel is loading, please wait...";

    word_t *video_mem = (word_t*)(video_initialized ? VIDEO_MEMORY : TEXT_VIDEO_MEMORY);
    memset(video_mem, 0x00, TEXT_HEIGHT * TEXT_WIDTH * sizeof(word_t));

    int i;
    size_t length = sizeof(text) - 1;
    size_t text_start = 10 * TEXT_WIDTH + (TEXT_WIDTH - length) / 2;
    for (i = 0; i < length; i++) video_mem[text_start + i] = text[i] | 0x0700;

    video_mem[11 * TEXT_WIDTH + 14] = 0x07DA;
    for (i = 15; i < TEXT_WIDTH - 15; i++) video_mem[11 * TEXT_WIDTH + i] = 0x07C4;
    video_mem[12 * TEXT_WIDTH - 15] = 0x07BF;

    video_mem[12 * TEXT_WIDTH + 14] = video_mem[13 * TEXT_WIDTH - 15] = 0x07B3;

    video_mem[13 * TEXT_WIDTH + 14] = 0x07C0;
    for (i = 15; i < TEXT_WIDTH - 15; i++) video_mem[13 * TEXT_WIDTH + i] = 0x07C4;
    video_mem[14 * TEXT_WIDTH - 15] = 0x07D9;
}

static void advance_progress(void)
{
    word_t *video_mem = (word_t*)(video_initialized ? VIDEO_MEMORY : TEXT_VIDEO_MEMORY);
    size_t old_status = tasks_completed * 50 / num_tasks;
    size_t new_status = (tasks_completed + 1) * 50 / num_tasks;

    int i;
    for (i = old_status; i < new_status; i++) video_mem[12 * TEXT_WIDTH + i + 15] = 0x7020;
    tasks_completed++;
}

void kernel_main(uintptr_t mboot_tags, size_t mboot_size)
{
    log_write(LOG_NORMAL, "Monolithium 0.1\n");

    pre_initialization();
    log_write(LOG_NORMAL, "Pre-initialization complete\n");

    draw_progress_bar();

    extern int _end;
    lowest_physical = PAGE_ALIGN_UP((uintptr_t)&_end) - 0x7FF00000;
    if ((mboot_tags + mboot_size) > lowest_physical) lowest_physical = mboot_tags + mboot_size;

    multiboot_tag_t *mboot = (multiboot_tag_t*)(PAGE_ALIGN_UP((uintptr_t)&_end) + PAGE_OFFSET(mboot_tags));
    map_memory_internal((void*)mboot_tags, mboot, mboot_size, PAGE_PRESENT | PAGE_WRITABLE);
    DO_TASK(scan_multiboot_info, mboot);
    DO_TASK(parse_command_line);
    DO_TASK(setup_memory_management);
    if (kernel_sections) load_kernel_symbols();
    DO_TASK(device_init);
    DO_TASK(timer_init);
    DO_TASK(clock_init);
    DO_TASK(process_init);
    DO_TASK(thread_init);

    thread_t *idle;
    ASSERT(create_system_thread(system_idle_thread, 0, THREAD_PRIORITY_IDLE, 0, NULL, &idle) == ERR_SUCCESS);

    DO_TASK(user_init);
    DO_TASK(pci_init);
    DO_TASK(video_init);
    unmap_memory_internal((void*)TEXT_VIDEO_MEMORY, TEXT_HEIGHT * TEXT_WIDTH * sizeof(word_t));
    video_initialized = TRUE;

    multiboot_tag_t *tag;
    for (tag = mboot; tag->type != MULTIBOOT_INFO_END; tag = (multiboot_tag_t*)(((uintptr_t)tag + tag->size + 7) & ~7))
    {
        if (tag->type == MULTIBOOT_INFO_MODULES)
        {
            multiboot_tag_module_t *module = (multiboot_tag_module_t*)tag;
            module_t *mod = (module_t*)malloc(sizeof(module_t));
            mod->physical_address = module->mod_start;
            mod->size = module->mod_end - module->mod_start;

            dword_t ret = module_load_from_physical(mod, module->string);
            if (ret == ERR_SUCCESS)
            {
                log_write(LOG_NORMAL, "Loaded module %s at 0x%08X\n", mod->name, mod->base_address);
            }
            else
            {
                if (mod->name) log_write(LOG_ERROR, "Cannot load module %s: %s\n", mod->name, get_error_string(ret));
                else log_write(LOG_ERROR, "Cannot load module from physical address 0x%08X: %s\n", module->mod_start, get_error_string(ret));
                free(mod);
            }

            advance_progress();
            // TODO: Reclaim pages
        }
    }

    handle_t manager_process = DO_TASK(start_system_manager);
    syscall_wait_for_one(manager_process, NULL, NO_TIMEOUT);

    KERNEL_CRASH("The system manager has stopped working");
    ASSERT(FALSE);
}
