/* See the end of this file for copyright, license, and warranty information. */

/**
 * @file sched.c
 * @brief Simple round-robin scheduler.
 *
 * Tasks are stored in a lookup table, `tasks`, which is indexed by pid.
 * The global `current` variable points to the task that is currently running,
 * which must only be accessed from scheduling context (i.e. from within a
 * syscall or scheduling interrupt handler).
 *
 * When `schedule()` is called, it first processes the kevent queue in which irq
 * handlers store broadcasts for changes in hardware state, such as a DMA buffer
 * having been fully transmitted.  Tasks register an event listener for the
 * event they are waiting for before entering I/O wait, and remove their waiting
 * flag in the listener callback.
 *
 * After all events are processed, `schedule()` iterates over the task table
 * starting from one task after the one that has been currently running, and
 * chooses the first one it encounters that is suitable for being woken back up
 * (i.e. is in state `TASK_QUEUE`).  Thus, the previously running task is only
 * executed again if no other tasks are ready to be executed.  If no task is
 * runnable, the idle task is selected.
 *
 * The last step is performing the in-kernel context switch to the next task
 * to be run, which is done by `do_switch()`.  This routine stores the current
 * register state in the old task's TCB and loads the registers from the new
 * one.  Execution then continues where the task that is switched to previously
 * called `do_switch()`, and eventually returns back to userspace by returning
 * from the exception handler.
 */

#include <arch-generic/do_switch.h>
#include <arch-generic/sched.h>
#include <arch-generic/watchdog.h>

#include <ardix/atomic.h>
#include <ardix/kevent.h>
#include <ardix/malloc.h>
#include <ardix/sched.h>
#include <ardix/types.h>

#include <errno.h>
#include <stddef.h>
#include <string.h>

extern uint32_t _sstack;
extern uint32_t _estack;

static struct task *tasks[CONFIG_SCHED_MAXTASK];
struct task *volatile current;

static struct task kernel_task;
static struct task idle_task;

static void task_destroy(struct kent *kent)
{
	struct task *task = container_of(kent, struct task, kent);
	tasks[task->pid] = NULL;
	free(task);
}

int sched_init(void)
{
	int err;

	kernel_task.kent.parent = kent_root;
	kernel_task.kent.destroy = task_destroy;
	err = kent_init(&kernel_task.kent);
	if (err != 0)
		goto out;

	memset(&kernel_task.tcb, 0, sizeof(kernel_task.tcb));
	kernel_task.bottom = &_estack;
	kernel_task.pid = 0;
	kernel_task.state = TASK_READY;

	tasks[0] = &kernel_task;
	current = &kernel_task;

	for (unsigned int i = 1; i < ARRAY_SIZE(tasks); i++)
		tasks[i] = NULL;

	err = arch_watchdog_init();
	if (err != 0)
		goto out;

	err = arch_sched_init(CONFIG_SCHED_FREQ);
	if (err != 0)
		goto out;

	err = arch_idle_task_init(&idle_task);
	if (err != 0)
		goto out;

	/*
	 * we don't really need to deallocate resources on error because we
	 * are going to panic anyways if the scheduler fails to initialize.
	 */
out:
	return err;
}

/**
 * @brief Determine whether the specified task is a candidate for execution.
 *
 * @param task The task
 * @returns whether `task` could be run next
 */
static inline bool can_run(const struct task *task)
{
	switch (task->state) {
	case TASK_SLEEP:
		return tick - task->last_tick >= task->sleep;
	case TASK_QUEUE:
	case TASK_READY:
		return true;
	case TASK_DEAD:
	case TASK_IOWAIT:
	case TASK_LOCKWAIT:
		return false;
	}

	return false; /* this shouldn't be reached */
}

void schedule(void)
{
	atomic_enter();

	struct task *old = current;
	pid_t nextpid = old->pid;

	struct task *new = NULL;

	kevents_process();

	if (old->state == TASK_READY)
		old->state = TASK_QUEUE;

	for (unsigned int i = 0; i < ARRAY_SIZE(tasks); i++) {
		/*
		 * increment nextpid before accessing the task table
		 * because it is -1 if the idle task was running
		 */
		nextpid++;
		nextpid %= ARRAY_SIZE(tasks);

		struct task *tmp = tasks[nextpid];
		if (tmp != NULL && can_run(tmp)) {
			new = tmp;
			break;
		}
	}

	if (new == NULL)
		new = &idle_task;

	new->state = TASK_READY;
	new->last_tick = tick;
	current = new;

	atomic_leave();

	if (old != new)
		do_switch(old, new);
}

void yield(enum task_state state)
{
	current->state = state;
	schedule();
}

long sys_sleep(unsigned long int millis)
{
	current->sleep = ms_to_ticks(millis);
	yield(TASK_SLEEP);
	/* TODO: return actual milliseconds */
	return 0;
}

/*
 * This file is part of Ardix.
 * Copyright (c) 2020, 2021 Felix Kopp <owo@fef.moe>.
 *
 * Ardix is non-violent software: you may only use, redistribute,
 * and/or modify it under the terms of the CNPLv6+ as found in
 * the LICENSE file in the source code root directory or at
 * <https://git.pixie.town/thufie/CNPL>.
 *
 * Ardix comes with ABSOLUTELY NO WARRANTY, to the extent
 * permitted by applicable law.  See the CNPLv6+ for details.
 */