/*
 * interrupt.c
 *
 * Copyright (C) 2018 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 <interrupt.h>
#include <segments.h>
#include <lock.h>
#include <thread.h>
#include <cpu.h>

static byte_t isr_stubs[IDT_NUM_INTERRUPTS * ISR_STUB_SIZE];
static idt_entry_t idt[IDT_NUM_INTERRUPTS];
static interrupt_handler_t handlers[IDT_NUM_INTERRUPTS];

static void idt_main_handler(byte_t interrupt_num, registers_t regs)
{
    regs.esp += 16;
    if (handlers[interrupt_num].procedure == NULL) return;

    thread_t *thread = get_current_thread();

    if (thread)
    {
        if (thread->in_kernel == 0) thread->last_context = &regs;
        thread->in_kernel++;
    }

    if (handlers[interrupt_num].interrupts) cpu_enable_interrupts();
    handlers[interrupt_num].procedure(&regs, interrupt_num);

    if (thread)
    {
        ASSERT(thread->in_kernel > 0);

        if (--thread->in_kernel == 0)
        {
            thread->last_context = NULL;
            if (thread->terminating) thread->terminated = TRUE;
            if (thread->terminated || thread->frozen > 0) syscall_yield_quantum();
            ASSERT(!thread->terminated && (thread->frozen <= 0));
        }
    }

    cpu_disable_interrupts();
}

dword_t set_int_handler(byte_t interrupt_num, isr_proc_t proc, bool_t interrupts, bool_t usermode)
{
    dword_t ret = ERR_SUCCESS;
    critical_t critical;

    enter_critical(&critical);

    if (handlers[interrupt_num].procedure != NULL)
    {
        ret = ERR_EXISTS;
        goto done;
    }

    handlers[interrupt_num].procedure = proc;
    handlers[interrupt_num].interrupts = interrupts;
    idt[interrupt_num].type = usermode ? IDT_GATE_USER : IDT_GATE_KERNEL;

done:
    leave_critical(&critical);
    return ret;
}

dword_t remove_int_handler(byte_t interrupt_num)
{
    dword_t ret = ERR_SUCCESS;
    critical_t critical;

    enter_critical(&critical);

    if (handlers[interrupt_num].procedure == NULL)
    {
        ret = ERR_NOTFOUND;
        goto done;
    }

    handlers[interrupt_num].procedure = NULL;
    idt[interrupt_num].type = IDT_GATE_KERNEL;

done:
    leave_critical(&critical);
    return ret;
}

void interrupt_init(void)
{
    dword_t i, offset = 0;

    for (i = 0; i < IDT_NUM_INTERRUPTS; i++)
    {
        idt[i].offset = (dword_t)&isr_stubs[offset] & 0xFFFF;
        idt[i].offset_high = (dword_t)&isr_stubs[offset] >> 16;
        idt[i].selector = get_kernel_code_selector();
        idt[i].type = IDT_GATE_KERNEL;
        idt[i].zero = 0;

        if (!HAS_ERROR_CODE(i))
        {
            isr_stubs[offset++] = 0x6A; // push 0
            isr_stubs[offset++] = 0x00;
        }

        isr_stubs[offset++] = 0x60; // pushad

        isr_stubs[offset++] = 0x1E; // push ds

        isr_stubs[offset++] = 0x6A; // push get_kernel_data_selector()
        isr_stubs[offset++] = get_kernel_data_selector();

        isr_stubs[offset++] = 0x58; // pop eax

        isr_stubs[offset++] = 0x8E; // mov ds, ax
        isr_stubs[offset++] = 0xD8;

        isr_stubs[offset++] = 0x8E; // mov es, ax
        isr_stubs[offset++] = 0xC0;

        isr_stubs[offset++] = 0x8E; // mov fs, ax
        isr_stubs[offset++] = 0xE0;

        isr_stubs[offset++] = 0x8E; // mov gs, ax
        isr_stubs[offset++] = 0xE8;

        isr_stubs[offset++] = 0x6A; // push i
        isr_stubs[offset++] = (byte_t) i;

        isr_stubs[offset++] = 0xE8; // call idt_main_handler
        dword_t rel_addr = (dword_t)idt_main_handler - (dword_t)&isr_stubs[offset + 4];
        isr_stubs[offset++] = rel_addr & 0xFF;
        isr_stubs[offset++] = (rel_addr >> 8) & 0xFF;
        isr_stubs[offset++] = (rel_addr >> 16) & 0xFF;
        isr_stubs[offset++] = (rel_addr >> 24) & 0xFF;

        isr_stubs[offset++] = 0x58; // pop eax

        if (!HAS_ERROR_CODE(i))
        {
            isr_stubs[offset++] = 0x81; // cmp dword [esp + 0x24], CONTEXT_SWITCH_MAGIC
            isr_stubs[offset++] = 0x7C;
            isr_stubs[offset++] = 0x24;
            isr_stubs[offset++] = 0x24;
            isr_stubs[offset++] = CONTEXT_SWITCH_MAGIC & 0xFF;
            isr_stubs[offset++] = (CONTEXT_SWITCH_MAGIC >> 8) & 0xFF;
            isr_stubs[offset++] = (CONTEXT_SWITCH_MAGIC >> 16) & 0xFF;
            isr_stubs[offset++] = CONTEXT_SWITCH_MAGIC >> 24;

            isr_stubs[offset++] = 0x75; // jnz +0x04
            isr_stubs[offset++] = 0x04;

            isr_stubs[offset++] = 0x8B; // mov esp, [esp + 0x10]
            isr_stubs[offset++] = 0x64;
            isr_stubs[offset++] = 0x24;
            isr_stubs[offset++] = 0x10;
        }

        isr_stubs[offset++] = 0x58; // pop eax

        isr_stubs[offset++] = 0x8E; // mov ds, ax
        isr_stubs[offset++] = 0xD8;

        isr_stubs[offset++] = 0x8E; // mov es, ax
        isr_stubs[offset++] = 0xC0;

        isr_stubs[offset++] = 0x8E; // mov fs, ax
        isr_stubs[offset++] = 0xE0;

        isr_stubs[offset++] = 0x8E; // mov gs, ax
        isr_stubs[offset++] = 0xE8;

        isr_stubs[offset++] = 0x61; // popad

        isr_stubs[offset++] = 0x83; // add esp, 4
        isr_stubs[offset++] = 0xC4;
        isr_stubs[offset++] = 0x04;

        isr_stubs[offset++] = 0xCF; // iret
    }

    cpu_set_interrupt_table(idt, sizeof(idt));
}
