/* vim: tabstop=4 shiftwidth=4 noexpandtab
 * This file is part of ToaruOS and is released under the terms
 * of the NCSA / University of Illinois License - see LICENSE.md
 * Copyright (C) 2011-2018 K. Lange
 * Copyright (C) 2012 Markus Schober
 *
 * Task Switching and Management Functions
 *
 */
#include <kernel/system.h>
#include <kernel/process.h>
#include <kernel/logging.h>
#include <kernel/shm.h>
#include <kernel/mem.h>

#define TASK_MAGIC 0xDEADBEEF

uint32_t next_pid = 0;

#define PUSH(stack, type, item) stack -= sizeof(type); \
							*((type *) stack) = item

page_directory_t *kernel_directory;
page_directory_t *current_directory;

/*
 * Clone a page directory and its contents.
 * (If you do not intend to clone the contents, do it yourself!)
 *
 * @param  src Pointer to source directory to clone from.
 * @return A pointer to a new directory.
 */
page_directory_t *
clone_directory(
		page_directory_t * src
		) {
	/* Allocate a new page directory */
	uintptr_t phys;
	page_directory_t * dir = (page_directory_t *)kvmalloc_p(sizeof(page_directory_t), &phys);
	/* Clear it out */
	memset(dir, 0, sizeof(page_directory_t));
	dir->ref_count = 1;

	/* And store it... */
	dir->physical_address = phys;
	uint32_t i;
	for (i = 0; i < 1024; ++i) {
		/* Copy each table */
		if (!src->tables[i] || (uintptr_t)src->tables[i] == (uintptr_t)0xFFFFFFFF) {
			continue;
		}
		if (kernel_directory->tables[i] == src->tables[i]) {
			/* Kernel tables are simply linked together */
			dir->tables[i] = src->tables[i];
			dir->physical_tables[i] = src->physical_tables[i];
		} else {
			if (i * 0x1000 * 1024 < SHM_START) {
				/* User tables must be cloned */
				uintptr_t phys;
				dir->tables[i] = clone_table(src->tables[i], &phys);
				dir->physical_tables[i] = phys | 0x07;
			}
		}
	}
	return dir;
}

/*
 * Free a directory and its tables
 */
void release_directory(page_directory_t * dir) {
	dir->ref_count--;

	if (dir->ref_count < 1) {
		uint32_t i;
		for (i = 0; i < 1024; ++i) {
			if (!dir->tables[i] || (uintptr_t)dir->tables[i] == (uintptr_t)0xFFFFFFFF) {
				continue;
			}
			if (kernel_directory->tables[i] != dir->tables[i]) {
				if (i * 0x1000 * 1024 < SHM_START) {
					for (uint32_t j = 0; j < 1024; ++j) {
						if (dir->tables[i]->pages[j].frame) {
							free_frame(&(dir->tables[i]->pages[j]));
						}
					}
				}
				free(dir->tables[i]);
			}
		}
		free(dir);
	}
}

void release_directory_for_exec(page_directory_t * dir) {
	uint32_t i;
	/* This better be the only owner of this directory... */
	for (i = 0; i < 1024; ++i) {
		if (!dir->tables[i] || (uintptr_t)dir->tables[i] == (uintptr_t)0xFFFFFFFF) {
			continue;
		}
		if (kernel_directory->tables[i] != dir->tables[i]) {
			if (i * 0x1000 * 1024 < USER_STACK_BOTTOM) {
				for (uint32_t j = 0; j < 1024; ++j) {
					if (dir->tables[i]->pages[j].frame) {
						free_frame(&(dir->tables[i]->pages[j]));
					}
				}
				dir->physical_tables[i] = 0;
				free(dir->tables[i]);
				dir->tables[i] = 0;
			}
		}
	}
}

extern char * default_name;

/*
 * Clone a page table
 *
 * @param src      Pointer to a page table to clone.
 * @param physAddr [out] Pointer to the physical address of the new page table
 * @return         A pointer to a new page table.
 */
page_table_t *
clone_table(
		page_table_t * src,
		uintptr_t * physAddr
		) {
	/* Allocate a new page table */
	page_table_t * table = (page_table_t *)kvmalloc_p(sizeof(page_table_t), physAddr);
	memset(table, 0, sizeof(page_table_t));
	uint32_t i;
	for (i = 0; i < 1024; ++i) {
		/* For each frame in the table... */
		if (!src->pages[i].frame) {
			continue;
		}
		/* Allocate a new frame */
		alloc_frame(&table->pages[i], 0, 0);
		/* Set the correct access bit */
		if (src->pages[i].present)	table->pages[i].present = 1;
		if (src->pages[i].rw)		table->pages[i].rw = 1;
		if (src->pages[i].user)		table->pages[i].user = 1;
		if (src->pages[i].writethrough)	table->pages[i].writethrough = 1;
		if (src->pages[i].cachedisable)	table->pages[i].cachedisable = 1;
		/* Copy the contents of the page from the old table to the new one */
		copy_page_physical(src->pages[i].frame * 0x1000, table->pages[i].frame * 0x1000);
	}
	return table;
}

uintptr_t frozen_stack = 0;

/*
 * Install multitasking functionality.
 */
void tasking_install(void) {
	IRQ_OFF; /* Disable interrupts */

	debug_print(NOTICE, "Initializing multitasking");

	/* Initialize the process tree */
	initialize_process_tree();
	/* Spawn the initial process */
	current_process = spawn_init();
	kernel_idle_task = spawn_kidle();
	/* Initialize the paging environment */
#if 0
	set_process_environment((process_t *)current_process, current_directory);
#endif
	/* Switch to the kernel directory */
	switch_page_directory(current_process->thread.page_directory);

	frozen_stack = (uintptr_t)valloc(KERNEL_STACK_SIZE);

	/* Reenable interrupts */
	IRQ_RES;
}

/*
 * Fork.
 *
 * @return To the parent: PID of the child; to the child: 0
 */
uint32_t fork(void) {
	IRQ_OFF;

	uintptr_t esp, ebp;

	current_process->syscall_registers->eax = 0;

	/* Make a pointer to the parent process (us) on the stack */
	process_t * parent = (process_t *)current_process;
	assert(parent && "Forked from nothing??");
	/* Clone the current process' page directory */
	page_directory_t * directory = clone_directory(current_directory);
	assert(directory && "Could not allocate a new page directory!");
	/* Spawn a new process from this one */
	debug_print(INFO,"\033[1;32mALLOC {\033[0m");
	process_t * new_proc = spawn_process(current_process, 0);
	debug_print(INFO,"\033[1;32m}\033[0m");
	assert(new_proc && "Could not allocate a new process!");
	/* Set the new process' page directory to clone */
	set_process_environment(new_proc, directory);

	struct regs r;
	memcpy(&r, current_process->syscall_registers, sizeof(struct regs));
	new_proc->syscall_registers = &r;

	esp = new_proc->image.stack;
	ebp = esp;

	new_proc->syscall_registers->eax = 0;

	PUSH(esp, struct regs, r);

	new_proc->thread.esp = esp;
	new_proc->thread.ebp = ebp;

	new_proc->is_tasklet = parent->is_tasklet;

	new_proc->thread.eip = (uintptr_t)&return_to_userspace;

	/* Add the new process to the ready queue */
	make_process_ready(new_proc);

	IRQ_RES;

	/* Return the child PID */
	return new_proc->id;
}

int create_kernel_tasklet(tasklet_t tasklet, char * name, void * argp) {
	IRQ_OFF;

	uintptr_t esp, ebp;

	if (current_process->syscall_registers) {
		current_process->syscall_registers->eax = 0;
	}

	page_directory_t * directory = kernel_directory;
	/* Spawn a new process from this one */
	process_t * new_proc = spawn_process(current_process, 0);
	assert(new_proc && "Could not allocate a new process!");
	/* Set the new process' page directory to the original process' */
	set_process_environment(new_proc, directory);
	directory->ref_count++;
	/* Read the instruction pointer */


	if (current_process->syscall_registers) {
		struct regs r;
		memcpy(&r, current_process->syscall_registers, sizeof(struct regs));
		new_proc->syscall_registers = &r;
	}

	esp = new_proc->image.stack;
	ebp = esp;

	if (current_process->syscall_registers) {
		new_proc->syscall_registers->eax = 0;
	}
	new_proc->is_tasklet = 1;
	new_proc->name = name;

	PUSH(esp, uintptr_t, (uintptr_t)name);
	PUSH(esp, uintptr_t, (uintptr_t)argp);
	PUSH(esp, uintptr_t, (uintptr_t)&task_exit);

	new_proc->thread.esp = esp;
	new_proc->thread.ebp = ebp;

	new_proc->thread.eip = (uintptr_t)tasklet;

	/* Add the new process to the ready queue */
	make_process_ready(new_proc);

	IRQ_RES;

	/* Return the child PID */
	return new_proc->id;
}


/*
 * clone the current thread and create a new one in the same
 * memory space with the given pointer as its new stack.
 */
uint32_t
clone(uintptr_t new_stack, uintptr_t thread_func, uintptr_t arg) {
	uintptr_t esp, ebp;

	IRQ_OFF;

	current_process->syscall_registers->eax = 0;

	/* Make a pointer to the parent process (us) on the stack */
	process_t * parent = (process_t *)current_process;
	assert(parent && "Cloned from nothing??");
	page_directory_t * directory = current_directory;
	/* Spawn a new process from this one */
	process_t * new_proc = spawn_process(current_process, 1);
	assert(new_proc && "Could not allocate a new process!");
	/* Set the new process' page directory to the original process' */
	set_process_environment(new_proc, directory);
	directory->ref_count++;
	/* Read the instruction pointer */

	struct regs r;
	memcpy(&r, current_process->syscall_registers, sizeof(struct regs));
	new_proc->syscall_registers = &r;

	esp = new_proc->image.stack;
	ebp = esp;

	/* Set the gid */
	if (current_process->group) {
		new_proc->group = current_process->group;
	} else {
		/* We are the session leader */
		new_proc->group = current_process->id;
	}

	new_proc->syscall_registers->ebp = new_stack;
	new_proc->syscall_registers->eip = thread_func;

	/* Push arg, bogus return address onto the new thread's stack */
	PUSH(new_stack, uintptr_t, arg);
	PUSH(new_stack, uintptr_t, THREAD_RETURN);

	/* Set esp, ebp, and eip for the new thread */
	new_proc->syscall_registers->esp = new_stack;
	new_proc->syscall_registers->useresp = new_stack;

	PUSH(esp, struct regs, r);

	new_proc->thread.esp = esp;
	new_proc->thread.ebp = ebp;

	new_proc->is_tasklet = parent->is_tasklet;

	new_proc->thread.eip = (uintptr_t)&return_to_userspace;

	/* Add the new process to the ready queue */
	make_process_ready(new_proc);

	IRQ_RES;

	/* Return the child PID */
	return new_proc->id;
}

/*
 * Get the process ID of the current process.
 *
 * @return The PID of the current process.
 */
uint32_t getpid(void) {
	/* Fairly self-explanatory. */
	return current_process->id;
}

/*
 * Switch to the next ready task.
 *
 * This is called from the interrupt handler for the interval timer to
 * perform standard task switching.
 */
void switch_task(uint8_t reschedule) {
	if (!current_process) {
		/* Tasking is not yet installed. */
		return;
	}
	if (!current_process->running) {
		switch_next();
	}

	/* Collect the current kernel stack and instruction pointers */
	uintptr_t esp, ebp, eip;
	asm volatile ("mov %%esp, %0" : "=r" (esp));
	asm volatile ("mov %%ebp, %0" : "=r" (ebp));
	eip = read_eip();
	if (eip == 0x10000) {
		/* Returned from EIP after task switch, we have
		 * finished switching. */
		fix_signal_stacks();

		/* XXX: Signals */
		if (!current_process->finished) {
			if (current_process->signal_queue->length > 0) {
				node_t * node = list_dequeue(current_process->signal_queue);
				signal_t * sig = node->value;
				free(node);
				handle_signal((process_t *)current_process, sig);
			}
		}

		return;
	}

	/* Remember this process' ESP/EBP/EIP */
	current_process->thread.eip = eip;
	current_process->thread.esp = esp;
	current_process->thread.ebp = ebp;
	current_process->running = 0;

	/* Save floating point state */
	switch_fpu();

	if (reschedule && current_process != kernel_idle_task) {
		/* And reinsert it into the ready queue */
		make_process_ready((process_t *)current_process);
	}

	/* Switch to the next task */
	switch_next();
}

/*
 * Immediately switch to the next task.
 *
 * Does not store the ESP/EBP/EIP of the current thread.
 */
void switch_next(void) {
	uintptr_t esp, ebp, eip;
	/* Get the next available process */
	current_process = next_ready_process();
	/* Retreive the ESP/EBP/EIP */
	eip = current_process->thread.eip;
	esp = current_process->thread.esp;
	ebp = current_process->thread.ebp;
	unswitch_fpu();

	/* Validate */
	if ((eip < (uintptr_t)&code) || (eip > (uintptr_t)heap_end)) {
		debug_print(WARNING, "Skipping broken process %d! [eip=0x%x <0x%x or >0x%x]", current_process->id, eip, &code, &end);
		switch_next();
	}

	if (current_process->finished) {
		debug_print(WARNING, "Tried to switch to process %d, but it claims it is finished.", current_process->id);
		switch_next();
	}

	/* Set the page directory */
	current_directory = current_process->thread.page_directory;
	switch_page_directory(current_directory);
	/* Set the kernel stack in the TSS */
	set_kernel_stack(current_process->image.stack);

	if (current_process->started) {
		if (!current_process->signal_kstack) {
			if (current_process->signal_queue->length > 0) {
				current_process->signal_kstack  = malloc(KERNEL_STACK_SIZE);
				current_process->signal_state.esp = current_process->thread.esp;
				current_process->signal_state.eip = current_process->thread.eip;
				current_process->signal_state.ebp = current_process->thread.ebp;
				memcpy(current_process->signal_kstack, (void *)(current_process->image.stack - KERNEL_STACK_SIZE), KERNEL_STACK_SIZE);
			}
		}
	} else {
		current_process->started = 1;
	}

	current_process->running = 1;

	/* Jump, baby, jump */
	asm volatile (
			"mov %0, %%ebx\n"
			"mov %1, %%esp\n"
			"mov %2, %%ebp\n"
			"mov %3, %%cr3\n"
			"mov $0x10000, %%eax\n" /* read_eip() will return 0x10000 */
			"jmp *%%ebx"
			: : "r" (eip), "r" (esp), "r" (ebp), "r" (current_directory->physical_address)
			: "%ebx", "%esp", "%eax");
}

extern void enter_userspace(uintptr_t location, uintptr_t stack);

/*
 * Enter ring 3 and jump to `location`.
 *
 * @param location Address to jump to in user space
 * @param argc     Argument count
 * @param argv     Argument pointers
 * @param stack    Userspace stack address
 */
void
enter_user_jmp(uintptr_t location, int argc, char ** argv, uintptr_t stack) {
	IRQ_OFF;
	set_kernel_stack(current_process->image.stack);

	PUSH(stack, uintptr_t, (uintptr_t)argv);
	PUSH(stack, int, argc);
	enter_userspace(location, stack);
}

/*
 * Dequeue the current task and set it as finished
 *
 * @param retval Set the return value to this.
 */
void task_exit(int retval) {
	/* Free the image memory */
	if (__builtin_expect(current_process->id == 0,0)) {
		/* This is probably bad... */
		switch_next();
		return;
	}
	cleanup_process((process_t *)current_process, retval);

	process_t * parent = process_get_parent((process_t *)current_process);

	if (parent && !parent->finished) {
		send_signal(parent->group, SIGCHLD, 1);
		wakeup_queue(parent->wait_queue);
	}

	switch_next();
}

/*
 * Call task_exit() and immediately STOP if we can't.
 */
void kexit(int retval) {
	task_exit(retval);
	debug_print(CRITICAL, "Process returned from task_exit! Environment is definitely unclean. Stopping.");
	STOP;
}
