/*** exception.c ***/
/* C exception handling system
 * (c) Paul Field
 */

#include "WimpLib:exception.h"

#include <assert.h>
#include <signal.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#include "kernel.h"
#include "WimpLib:Message.h"
#include "WimpLib:Log.h"

static const _kernel_oserror user_error = {0, "Custom throwed exception"};
static exception current;
_exception_block *_exception_list = NULL;

extern void init_unwindblock(_kernel_unwindblock*, int pc);
extern int clean_pc(int);

typedef struct
{
	int length:24;
	int ff_code:8;
}
fname_info;

static void backtrace(_kernel_unwindblock* pblock)
{
	char* plang;

	while(_kernel_unwind(pblock, &plang) > 0)
	{
		int pc = clean_pc(pblock->pc);
		int fp = pblock->fp ? clean_pc(*(int*)pblock->fp) : 0;
		char* pname = NULL;

		if (fp)
		{
			fp -= 12;
			fname_info* info = (fname_info*) (fp - 4);
			for (int i = 3; i >= 0; i--, info--)
			{
				if ((info->ff_code == 0xff)
				&&  (info->length < 4096))
					pname = ((char*) info) - info->length;
			}
		}

		if (pname)
			Log("  at %08x in %s\n", pc, pname);
		else
			Log("  at %08x in %08x\n", pc, fp);
	}
}

typedef void(* a_signal)(int);

#pragma -s1 // no stack checking

void throw_current(void)
{
	_exception_block* first_block;

	first_block = _exception_list;
	if (first_block == NULL)
	{
		// Log backtrace if nothing to catch the error
		if (!current.bt)
		{
			_kernel_unwindblock block;

			init_unwindblock(&block, 0);
			backtrace(&block);
		}

		exit(EXIT_FAILURE);
	}
	else
	{
		_exception_list = first_block->previous;

		// Log backtrace if only catched by global error catcher
		if (!current.bt && !_exception_list)
		{
			_kernel_unwindblock block;

			init_unwindblock(&block, 0);
			backtrace(&block);
		}

		longjmp(first_block->jmpbuf, 1);
	}
}

static void throw_stacksignal(int sig)
{
	current.file  = NULL;
	current.line  = 0;
	current.bt    = 1;
	current.type  = exception_os;
	strcpy(current.error.errmess, "Stack overflow");
	current.error.errnum = 1;

	signal(sig, throw_stacksignal);  // reinstate signal handler

	throw_current();
}

#pragma -s0 // stack checking

static void throw_log(void)
{
	if (current.file)
		Log("%s [in file %s, at line %d]\n", current.error.errmess, current.file, current.line);
	else
		Log("%s\n", current.error.errmess);
}

void __throw_user(const char* file , int line, unsigned id)
{
	current.file  = file;
	current.line  = line;
	current.bt    = 0;
	current.type  = exception_user;
	current.error = user_error;
	current.error.errnum = id;
	throw_log();
	throw_current();
}

void __throw_os(const char* file , int line, const _kernel_oserror* err)
{
	if (!err) return;

	current.file  = file;
	current.line  = line;
	current.bt    = 0;
	current.type  = exception_os;
	current.error = *err;
	throw_log();
	throw_current();
}

void __throw_last_oserror(const char* file , int line)
{
	current.file  = file;
	current.line  = line;
	current.bt    = 0;
	current.type  = exception_os;
	current.error = *_kernel_last_oserror();
	throw_log();
	throw_current();
}

void __throw_string(const char* file , int line, const char* pformat, ...)
{
	va_list arg;

	current.file  = file;
	current.line  = line;
	current.bt    = 0;
	current.type  = exception_os;

	va_start(arg, pformat);
	vsnprintf(current.error.errmess, sizeof(current.error.errmess), Msg_Lookup(pformat), arg);
	va_end(arg);

	current.error.errnum = 1;

	throw_log();
	throw_current();
}

void __throw_runtimeos(const char* file , int line, const _kernel_oserror* err)
{
	if (!err) return;

	current.file  = file;
	current.line  = line;
	current.bt    = 1;
	current.type  = exception_os;
	current.error = *err;

	throw_log();

	_kernel_unwindblock block;

	init_unwindblock(&block, 0);
	backtrace(&block);

	throw_current();
}

void __throw_runtime(const char* file , int line, const char* pformat, ...)
{
	va_list arg;

	current.file  = file;
	current.line  = line;
	current.bt    = 1;
	current.type  = exception_os;

	va_start(arg, pformat);
	vsnprintf(current.error.errmess, sizeof(current.error.errmess), Msg_Lookup(pformat), arg);
	va_end(arg);

	current.error.errnum = 1;

	throw_log();

	_kernel_unwindblock block;

	init_unwindblock(&block, 0);
	backtrace(&block);

	throw_current();
}

const exception* exception_current(void)
{
	return &current;
}

static void throw_signal(int sig)
{
	const _kernel_oserror* perr = _kernel_last_oserror();

	signal(sig, throw_signal);  // reinstate signal handler

	if (perr)
	{
		const _kernel_oserror err = *perr;

		Log("Signal %d. Guess is -> ", sig);

		__throw_runtimeos(NULL, 0, &err);
	}
	else
	{
		__throw_runtime(NULL, 0, "ErrSignal%d", sig);
	}
}

void exception_initialise(void)
{
	signal(SIGABRT, throw_signal);
	signal(SIGFPE,  throw_signal);
	signal(SIGILL,  throw_signal);
	signal(SIGINT,  throw_signal);
	signal(SIGSEGV, throw_signal);
	signal(SIGTERM, throw_signal);
	signal(SIGSTAK, throw_stacksignal);
	signal(SIGOSERROR, throw_signal);
}
