#include <fbos/init.h>
#include <fbos/sbi.h>
#include <fbos/printk.h>

// Mask for 'scause' to check whether it came from an interrupt or an exception.
#define INTERRUPT_MASK 0x8000000000000000
#define IS_EXCEPTION(x) ((x & INTERRUPT_MASK) == 0)

// Mask for 'scause' to figure out if the interrupt was caused by the timer.
#define TIMER_SCAUSE_MASK 0x05

// Mask for 'scause' to figure out if the exception was cause by U privilege
// mode making an 'ecall'.
#define USER_ECALL_MASK 0x08

// Identifier for the 'write' system call.
#define NR_WRITE 0x01

// Declared in include/fbos/init.h.
uint64_t seconds_elapsed;

// Set up a timer through the SBI interface that sends an interrupt in one
// second from the time this function is called.
__kernel void time_out_in_one_second(void)
{
	struct sbi_ret ret;
	register uint64_t one_second asm("a0");

	asm volatile("rdtime t0\n\t"
				 "mv t1, %1\n\t"
				 "add %0, t0, t1"
				 : "=r"(one_second)
				 : "r"(info.cpu_freq)
				 : "t0", "t1");

	ret = sbi_ecall1(TIME_EXT, TIME_SET_TIMER, one_second);
	if (ret.error != SBI_SUCCESS) {
		die("Could not set timer\n");
	}
}

/*
 * Exception handler. For this kernel, it only ensures that the 'write' system
 * call is the one responsible for this exception and handles it; otherwise it
 * will die.
 */
__kernel __always_inline void exception_handler(uint64_t cause)
{
	register char *message asm("a0");
	register size_t n asm("a1");
	register uint64_t syscall_id asm("a7");

	if ((cause & USER_ECALL_MASK) != USER_ECALL_MASK) {
		die("Don't know how to handle this exception :D\n");
	}
	if (syscall_id != NR_WRITE) {
		die("Bad syscall\n");
	}

	sys_write(message, n);
}

/*
 * Direct interrupt handler. Handles interrupts such as the timer event and user
 * mode entries.
 *
 * NOTE: as per RISC-V specification, the handler's address as set on the
 * 'stvec' register *must* be aligned on a 4-byte boundary. Hence, ensuring a
 * proper alignment is mandatory.
 *
 * NOTE: the '__s_interrupt' attribute already handles the saving/restoring of
 * all registers. It's probably a bit over the top since it also does that for
 * registers we never care on this kernel (e.g. floating point registers), but
 * it's convenient.
 */
__aligned(4) __s_interrupt __kernel void interrupt_handler(void)
{
	uint64_t cause;
	asm volatile("csrr %0, scause" : "=r"(cause)::);

	if (IS_EXCEPTION(cause)) {
		exception_handler(cause);
		prepare_switch_to(TASK_INIT);
		goto end;
	}

	if ((cause & TIMER_SCAUSE_MASK) == TIMER_SCAUSE_MASK) {
		// Clear timer interrupt pending bit from the 'sip' register.
		asm volatile("li t0, 32\n\t"
					 "csrc sip, t0\n\t"
					 "csrc sie, t0"
					 :
					 :
					 : "t0");

		// BEHOLD! The fizz buzz logic! :D
		seconds_elapsed += 1;
		if ((seconds_elapsed % 15) == 0) {
			prepare_switch_to(TASK_FIZZBUZZ);
		} else if ((seconds_elapsed % 5) == 0) {
			prepare_switch_to(TASK_BUZZ);
		} else if ((seconds_elapsed % 3) == 0) {
			prepare_switch_to(TASK_FIZZ);
		}

		// Re-enable timer interrupts.
		asm volatile("li t0, 32\n\t"
					 "csrs sie, t0"
					 :
					 :
					 : "t0");

		// Reset the timer one second from now.
		time_out_in_one_second();
	} else {
		printk("WARN: unknown interrupt just came in...\n");
	}

end:
	/*
	 * Here the restoring of the stack will happen, so it's actually not empty.
	 *
	 * NOTE: the restore of registers when we switch to another process here is
	 * actually pretty pointless (we are not 'restoring' anything in the point
	 * of view of the process we are about to schedule), but we keep the 'sp'
	 * register sane, at least.
	 *
	 * Anyways, when we schedule a process in this kernel we don't actually
	 * schedule it in the proper sense: we don't return to the last 'pc' for
	 * that process, but we actually run it from the entry address again. This
	 * is not relevant for this kernel because in the end our processes only do
	 * one thing, and 're-starting' is effectively the same in this (silly)
	 * kernel of ours :)
	 */
	;
}

__kernel void setup_interrupts(void)
{
	/*
	 * - stvec:   point to our interrupt handler. The two least-significant bits are
	 *            going to be '00', meaning we are using direct mode.
	 * - sstatus: set the SIE (S Interrupt Enable) bit. Interrupts are now on!
	 */
	asm volatile("csrw stvec, %0\n\t"
				 "csrsi sstatus, 2"
				 :
				 : "r"(&interrupt_handler)
				 :);

	/*
	 * - sie: set bit 5 (STIE: S Timer Interrupt Enable).
	 *
	 * NOTE: head.S zeroes out both 'sip' and 'sie' registers. Hence, there are
	 * no pending interrupts.
	 */
	asm volatile("li t0, 32\n\t"
				 "csrs sie, t0"
				 :
				 :
				 : "t0");

	// And initialize the timer to send an interrupt in one second from now.
	time_out_in_one_second();
}
