/**
** @file	syscalls.c
**
** @author	CSCI-452 class of 20245
**
** @brief	System call implementations
*/

#define KERNEL_SRC

#include <common.h>

#include <cio.h>
#include <clock.h>
#include <procs.h>
#include <sio.h>
#include <syscalls.h>
#include <user.h>
#include <vm.h>
#include <x86/pic.h>

/*
** PRIVATE DEFINITIONS
*/

/*
** Macros to simplify tracing a bit
**
** TRACING_SYSCALLS and TRACING_SYSRETS are defined in debug.h,
** controlled by the TRACE ** macro. If not tracing these, SYSCALL_ENTER
** is a no-op, and SYSCALL_EXIT just does a return.
*/

#if TRACING_SYSCALLS

#define SYSCALL_ENTER(x)                                         \
	do {                                                         \
		cio_printf("--> %s, pid %08x", __func__, (uint32_t)(x)); \
	} while (0)

#else

#define SYSCALL_ENTER(x) /* */

#endif /* TRACING_SYSCALLS */

#if TRACING_SYSRETS

#define SYSCALL_EXIT(x)                                       \
	do {                                                      \
		cio_printf("<-- %s %08x\n", __func__, (uint32_t)(x)); \
		return;                                               \
	} while (0)

#else

#define SYSCALL_EXIT(x) return

#endif /* TRACING_SYSRETS */

/*
** PRIVATE DATA TYPES
*/

/*
** PUBLIC GLOBAL VARIABLES
*/

/*
** IMPLEMENTATION FUNCTIONS
*/

// a macro to simplify syscall entry point specification
// we don't declare these static because we may want to call
// some of them from other parts of the kernel
#define SYSIMPL(x) void sys_##x(pcb_t *pcb)

/*
** Second-level syscall handlers
**
** All have this prototype:
**
**	static void sys_NAME( pcb_t *pcb );
**
** where the parameter 'pcb' is a pointer to the PCB of the process
** making the system call.
**
** Values being returned to the user are placed into the EAX
** field in the context save area for that process.
*/

/**
** sys_exit - terminate the calling process
**
** Implements:
**		void exit( int32_t status );
**
** Does not return
*/
SYSIMPL(exit)
{
	// sanity check
	assert(pcb != NULL);

	SYSCALL_ENTER(pcb->pid);

	// retrieve the exit status of this process
	pcb->exit_status = (int32_t)ARG(pcb, 1);

	// now, we need to do the following:
	// 	reparent any children of this process and wake up init if need be
	// 	find this process' parent and wake it up if it's waiting

	pcb_zombify(pcb);

	// pick a new winner
	dispatch();

	SYSCALL_EXIT(0);
}

/**
** sys_waitpid - wait for a child process to terminate
**
** Implements:
**		int waitpid( uint_t pid, int32_t *status );
**
** Blocks the calling process until the specified child (or any child)
** of the caller terminates. Intrinsic return is the PID of the child that
** terminated, or an error code; on success, returns the child's termination
** status via 'status' if that pointer is non-NULL.
*/
SYSIMPL(waitpid)
{
	// sanity check
	assert(pcb != NULL);

	SYSCALL_ENTER(pcb->pid);

	/*
	** We need to do two things here:  (1) find out whether or
	** not this process has any children in the system, and (2)
	** find out whether the desired child (or any child, if the
	** target PID is 0) has terminated.
	**
	** To do this, we loop until we find a the requested PID or
	** a Zombie child process, or have gone through all of the
	** slots in the process table.
	**
	** If the target PID is 0, we don't care which child process
	** we reap here; there could be several, but we only need to
	** find one.
	*/

	// verify that we aren't looking for ourselves!
	uint_t target = ARG(pcb, 1);

	if (target == pcb->pid) {
		RET(pcb) = E_BAD_PARAM;
		SYSCALL_EXIT(E_BAD_PARAM);
	}

	// Good.  Now, figure out what we're looking for.

	pcb_t *child = NULL;

	if (target != 0) {
		// we're looking for a specific child
		child = pcb_find_pid(target);

		if (child != NULL) {
			// found the process; is it one of our children:
			if (child->parent != pcb) {
				// NO, so we can't wait for it
				RET(pcb) = E_BAD_PARAM;
				SYSCALL_EXIT(E_BAD_PARAM);
			}

			// yes!  is this one ready to be collected?
			if (child->state != STATE_ZOMBIE) {
				// no, so we'll have to block for now
				child = NULL;
			}

		} else {
			// no such child
			RET(pcb) = E_BAD_PARAM;
			SYSCALL_EXIT(E_BAD_PARAM);
		}

	} else {
		// looking for any child

		// we need to find a process that is our child
		// and has already exited

		child = NULL;
		bool_t found = false;

		// unfortunately, we can't stop at the first child,
		// so we need to do the iteration ourselves
		register pcb_t *curr = ptable;

		for (int i = 0; i < N_PROCS; ++i, ++curr) {
			if (curr->parent == pcb) {
				// found one!
				found = true;

				// has it already exited?
				if (curr->state == STATE_ZOMBIE) {
					// yes, so we're done here
					child = curr;
					break;
				}
			}
		}

		if (!found) {
			// got through the loop without finding a child!
			RET(pcb) = E_NO_CHILDREN;
			SYSCALL_EXIT(E_NO_CHILDREN);
		}
	}

	/*
	** At this point, one of these situations is true:
	**
	**  * we are looking for a specific child and found it
	**  * we are looking for any child and found one
	**
	** Either way, 'child' will be non-NULL if the selected
	** process has already become a Zombie.  If that's the
	** case, we collect its status and clean it up; otherwise,
	** we block this process.
	*/

	// did we find one to collect?
	if (child == NULL) {
		// no - mark the parent as "Waiting"
		pcb->state = STATE_WAITING;
		assert(pcb_queue_insert(waiting, pcb) == SUCCESS);

		// select a new current process
		dispatch();
		SYSCALL_EXIT((uint32_t)current);
	}

	// found a Zombie; collect its information and clean it up
	RET(pcb) = child->pid;

	// get "status" pointer from parent
	int32_t *stat = (int32_t *)ARG(pcb, 2);

	// if stat is NULL, the parent doesn't want the status
	if (stat != NULL) {
		// ********************************************************
		// ** Potential VM issue here!  This code assigns the exit
		// ** status into a variable in the parent's address space.
		// ** This works in the baseline because we aren't using
		// ** any type of memory protection.  If address space
		// ** separation is implemented, this code will very likely
		// ** STOP WORKING, and will need to be fixed.
		// ********************************************************
		*stat = child->exit_status;
	}

	// clean up the child
	pcb_cleanup(child);

	SYSCALL_EXIT(RET(pcb));
}

/**
** sys_fork - create a new process
**
** Implements:
**		int fork( void );
**
** Creates a new process that is a duplicate of the calling process.
** Returns the child's PID to the parent, and 0 to the child, on success;
** else, returns an error code to the parent.
*/
SYSIMPL(fork)
{
	// sanity check
	assert(pcb != NULL);

	SYSCALL_ENTER(pcb->pid);

	// Make sure there's room for another process!
	pcb_t *new;
	if (pcb_alloc(&new) != SUCCESS || new == NULL) {
		RET(pcb) = E_NO_PROCS;
		SYSCALL_EXIT(RET(pcb));
	}

	// duplicate the memory image of the parent
	int status = user_duplicate(new, pcb);
	if (status != SUCCESS) {
		pcb_free(new);
		RET(pcb) = status;
		SYSCALL_EXIT(status);
	}

	// Set the child's identity.
	new->pid = next_pid++;
	new->parent = pcb;
	new->state = STATE_NEW;

	// replicate other things inherited from the parent
	new->priority = pcb->priority;

	// Set the return values for the two processes.
	RET(pcb) = new->pid;
	RET(new) = 0;

	// Schedule the child, and let the parent continue.
	schedule(new);

	SYSCALL_EXIT(new->pid);
}

/**
** sys_exec - replace the memory image of a process
**
** Implements:
**		void exec( uint_t what, char **args );
**
** Replaces the memory image of the calling process with that of the
** indicated program.
**
** Returns only on failure.
*/
SYSIMPL(exec)
{
	// sanity check
	assert(pcb != NULL);

	uint_t what = ARG(pcb, 1);
	const char **args = (const char **)ARG(pcb, 2);

	SYSCALL_ENTER(pcb->pid);

	// locate the requested program
	prog_t *prog = user_locate(what);
	if (prog == NULL) {
		RET(pcb) = E_NOT_FOUND;
		SYSCALL_EXIT(E_NOT_FOUND);
	}

	// we have located the program, but before we can load it,
	// we need to clean up the existing VM hierarchy
	vm_free(pcb->pdir);
	pcb->pdir = NULL;

	// "load" it and set up the VM tables for this process
	int status = user_load(prog, pcb, args, false);
	if (status != SUCCESS) {
		RET(pcb) = status;
		SYSCALL_EXIT(status);
	}

	/*
	** Decision:
	**	(A) schedule this process and dispatch another,
	**	(B) let this one continue in its current time slice
	**	(C) reset this one's time slice and let it continue
	**
	** We choose option A.
	**
	** If scheduling the process fails, the exec() has failed. However,
	** all trace of the old process is gone by now, so we can't return
	** an error status to it.
	*/

	schedule(pcb);

	dispatch();
}

/**
** sys_read - read into a buffer from an input channel
**
** Implements:
**		int read( uint_t chan, void *buffer, uint_t length );
**
** Reads up to 'length' bytes from 'chan' into 'buffer'. Returns the
** count of bytes actually transferred.
*/
SYSIMPL(read)
{
	// sanity check
	assert(pcb != NULL);

	SYSCALL_ENTER(pcb->pid);

	// grab the arguments
	uint_t chan = ARG(pcb, 1);
	char *buf = (char *)ARG(pcb, 2);
	uint_t len = ARG(pcb, 3);

	// if the buffer is of length 0, we're done!
	if (len == 0) {
		RET(pcb) = 0;
		SYSCALL_EXIT(0);
	}

	// try to get the next character(s)
	int n = 0;

	if (chan == CHAN_CIO) {
		// console input is non-blocking
		if (cio_input_queue() < 1) {
			RET(pcb) = 0;
			SYSCALL_EXIT(0);
		}
		// at least one character
		n = cio_gets(buf, len);
		RET(pcb) = n;
		SYSCALL_EXIT(n);

	} else if (chan == CHAN_SIO) {
		// SIO input is blocking, so if there are no characters
		// available, we'll block this process
		n = sio_read(buf, len);
		RET(pcb) = n;
		SYSCALL_EXIT(n);
	}

	// bad channel code
	RET(pcb) = E_BAD_PARAM;
	SYSCALL_EXIT(E_BAD_PARAM);
}

/**
** sys_write - write from a buffer to an output channel
**
** Implements:
**		int write( uint_t chan, const void *buffer, uint_t length );
**
** Writes 'length' bytes from 'buffer' to 'chan'. Returns the
** count of bytes actually transferred.
*/
SYSIMPL(write)
{
	// sanity check
	assert(pcb != NULL);

	SYSCALL_ENTER(pcb->pid);

	// grab the parameters
	uint_t chan = ARG(pcb, 1);
	char *buf = (char *)ARG(pcb, 2);
	uint_t length = ARG(pcb, 3);

	// this is almost insanely simple, but it does separate the
	// low-level device access fromm the higher-level syscall implementation

	// assume we write the indicated amount
	int rval = length;

	// simplest case
	if (length >= 0) {
		if (chan == CHAN_CIO) {
			cio_write(buf, length);

		} else if (chan == CHAN_SIO) {
			sio_write(buf, length);

		} else {
			rval = E_BAD_CHAN;
		}
	}

	RET(pcb) = rval;

	SYSCALL_EXIT(rval);
}

/**
** sys_getpid - returns the PID of the calling process
**
** Implements:
**		uint_t getpid( void );
*/
SYSIMPL(getpid)
{
	// sanity check!
	assert(pcb != NULL);

	SYSCALL_ENTER(pcb->pid);

	// return the time
	RET(pcb) = pcb->pid;
}

/**
** sys_getppid - returns the PID of the parent of the calling process
**
** Implements:
**		uint_t getppid( void );
*/
SYSIMPL(getppid)
{
	// sanity check!
	assert(pcb != NULL);
	assert(pcb->parent != NULL);

	SYSCALL_ENTER(pcb->pid);

	// return the time
	RET(pcb) = pcb->parent->pid;
}

/**
** sys_gettime - returns the current system time
**
** Implements:
**		uint32_t gettime( void );
*/
SYSIMPL(gettime)
{
	// sanity check!
	assert(pcb != NULL);

	SYSCALL_ENTER(pcb->pid);

	// return the time
	RET(pcb) = system_time;
}

/**
** sys_getprio - the scheduling priority of the calling process
**
** Implements:
**		int getprio( void );
*/
SYSIMPL(getprio)
{
	// sanity check!
	assert(pcb != NULL);

	SYSCALL_ENTER(pcb->pid);

	// return the time
	RET(pcb) = pcb->priority;
}

/**
** sys_setprio - sets the scheduling priority of the calling process
**
** Implements:
**		int setprio( int new );
*/
SYSIMPL(setprio)
{
	// sanity check!
	assert(pcb != NULL);

	SYSCALL_ENTER(pcb->pid);

	// remember the old priority
	int old = pcb->priority;

	// set the priority
	pcb->priority = ARG(pcb, 1);

	// return the old value
	RET(pcb) = old;
}

/**
** sys_kill - terminate a process with extreme prejudice
**
** Implements:
**		int32_t kill( uint_t pid );
**
** Marks the specified process (or the calling process, if PID is 0)
** as "killed". Returns 0 on success, else an error code.
*/
SYSIMPL(kill)
{
	// sanity check
	assert(pcb != NULL);

	SYSCALL_ENTER(pcb->pid);

	// who is the victim?
	uint_t pid = ARG(pcb, 1);

	// if it's this process, convert this into a call to exit()
	if (pid == pcb->pid) {
		pcb->exit_status = EXIT_KILLED;
		pcb_zombify(pcb);
		dispatch();
		SYSCALL_EXIT(EXIT_KILLED);
	}

	// must be a valid "ordinary user" PID
	// QUESTION: what if it's the idle process?
	if (pid < FIRST_USER_PID) {
		RET(pcb) = E_FAILURE;
		SYSCALL_EXIT(E_FAILURE);
	}

	// OK, this is an acceptable victim; see if it exists
	pcb_t *victim = pcb_find_pid(pid);
	if (victim == NULL) {
		// nope!
		RET(pcb) = E_NOT_FOUND;
		SYSCALL_EXIT(E_NOT_FOUND);
	}

	// must have a state that is possible
	assert(victim->state >= FIRST_VIABLE && victim->state < N_STATES);

	// how we perform the kill depends on the victim's state
	int32_t status = SUCCESS;

	switch (victim->state) {
	case STATE_KILLED: // FALL THROUGH
	case STATE_ZOMBIE:
		// you can't kill it if it's already dead
		RET(pcb) = SUCCESS;
		break;

	case STATE_READY: // FALL THROUGH
	case STATE_SLEEPING: // FALL THROUGH
	case STATE_BLOCKED: // FALL THROUGH
		// here, the process is on a queue somewhere; mark
		// it as "killed", and let the scheduler deal with it
		victim->state = STATE_KILLED;
		RET(pcb) = SUCCESS;
		break;

	case STATE_RUNNING:
		// we have met the enemy, and it is us!
		pcb->exit_status = EXIT_KILLED;
		pcb_zombify(pcb);
		status = EXIT_KILLED;
		// we need a new current process
		dispatch();
		break;

	case STATE_WAITING:
		// similar to the 'running' state, but we don't need
		// to dispatch a new process
		victim->exit_status = EXIT_KILLED;
		status = pcb_queue_remove_this(waiting, victim);
		pcb_zombify(victim);
		RET(pcb) = status;
		break;

	default:
		// this is a really bad potential problem - we have an
		// unexpected or bogus process state, but we didn't
		// catch that earlier.
		sprint(b256, "*** kill(): victim %d, odd state %d\n", victim->pid,
			   victim->state);
		PANIC(0, b256);
	}

	SYSCALL_EXIT(status);
}

/**
** sys_sleep - put the calling process to sleep for some length of time
**
** Implements:
**		uint_t sleep( uint_t ms );
**
** Puts the calling process to sleep for 'ms' milliseconds (or just yields
** the CPU if 'ms' is 0).  ** Returns the time the process spent sleeping.
*/
SYSIMPL(sleep)
{
	// sanity check
	assert(pcb != NULL);

	SYSCALL_ENTER(pcb->pid);

	// get the desired duration
	uint_t length = ARG(pcb, 1);

	if (length == 0) {
		// just yield the CPU
		// sleep duration is 0
		RET(pcb) = 0;

		// back on the ready queue
		schedule(pcb);

	} else {
		// sleep for a while
		pcb->wakeup = system_time + length;

		if (pcb_queue_insert(sleeping, pcb) != SUCCESS) {
			// something strange is happening
			WARNING("sleep pcb insert failed");
			// if this is the current process, report an error
			if (current == pcb) {
				RET(pcb) = -1;
			}
			// return without dispatching a new process
			return;
		}
	}

	// only dispatch if the current process called us
	if (pcb == current) {
		current = NULL;
		dispatch();
	}
}

/*
** PRIVATE FUNCTIONS GLOBAL VARIABLES
*/

/*
** The system call jump table
**
** Initialized using designated initializers to ensure the entries
** are correct even if the syscall code values should happen to change.
** This also makes it easy to add new system call entries, as their
** position in the initialization list is irrelevant.
*/

static void (*const syscalls[N_SYSCALLS])(pcb_t *) = {
	[SYS_exit] = sys_exit,		 [SYS_waitpid] = sys_waitpid,
	[SYS_fork] = sys_fork,		 [SYS_exec] = sys_exec,
	[SYS_read] = sys_read,		 [SYS_write] = sys_write,
	[SYS_getpid] = sys_getpid,	 [SYS_getppid] = sys_getppid,
	[SYS_gettime] = sys_gettime, [SYS_getprio] = sys_getprio,
	[SYS_setprio] = sys_setprio, [SYS_kill] = sys_kill,
	[SYS_sleep] = sys_sleep
};

/**
** Name:	sys_isr
**
** System call ISR
**
** @param vector   Vector number for this interrupt
** @param code     Error code (0 for this interrupt)
*/
static void sys_isr(int vector, int code)
{
	// keep the compiler happy
	(void)vector;
	(void)code;

	// sanity check!
	assert(current != NULL);
	assert(current->context != NULL);

	// retrieve the syscall code
	int num = REG(current, eax);

#if TRACING_SYSCALLS
	cio_printf("** --> SYS pid %u code %u\n", current->pid, num);
#endif

	// validate it
	if (num < 0 || num >= N_SYSCALLS) {
		// bad syscall number
		// could kill it, but we'll just force it to exit
		num = SYS_exit;
		ARG(current, 1) = EXIT_BAD_SYSCALL;
	}

	// call the handler
	syscalls[num](current);

#if TRACING_SYSCALLS
	cio_printf("** <-- SYS pid %u ret %u\n", current->pid, RET(current));
#endif

	// tell the PIC we're done
	outb(PIC1_CMD, PIC_EOI);
}

/*
** PUBLIC FUNCTIONS
*/

/**
** Name:  sys_init
**
** Syscall module initialization routine
**
** Dependencies:
**    Must be called after cio_init()
*/
void sys_init(void)
{
#if TRACING_INIT
	cio_puts(" Sys");
#endif

	// install the second-stage ISR
	install_isr(VEC_SYSCALL, sys_isr);
}