#include "system.h"
#include "platform.h"
#include "scenefile.h"
#include "sys_log.h"
#include "sys_render.h"
#include <string.h>

#define save_interval (900000000ull)

static yrSystem_init_func init[sys_end] = {NULL};
static yrSystem_save_func save[sys_end] = {NULL};
static yrSystem_canfreeze_func canfreeze[sys_end] = {NULL};
static yrSystem_freeze_func freeze[sys_end] = {NULL};
static yrSystem_unfreeze_func unfreeze[sys_end] = {NULL};
static yrSystem_tick_func tick[sys_end] = {NULL};
static yrSystem_shutdown_func shutdown[sys_end] = {NULL};
static unsigned dependencies[sys_end] = {0};
static enum yrSysId order[sys_end];
static unsigned initlevel = 0;
static unsigned requested_level = 0;
static size_t visible_quads = 0;
static size_t tilestore_bytes = 0;
static yrSceneFile* scenefile = NULL;
static char* scenefilename = NULL;
static int quitting = 0;
static int vr_from_start = 0;

static uint64_t			save_next = 0;
static yrEvent			save_event = NULL;
static yrWriteHandle*	save_handle = NULL;
static int				save_failed = 0;
static int				save_cursys = 0;
static int64_t			save_sysptr[sys_end] = {0};

static int sysinit(void);
static int systick(void);
static int sysshutdown(void);
static int syssave(void);

int yrSystem_run(size_t p_visible_quads, size_t p_tilestore_bytes, int start_vr)
{
	visible_quads = (p_visible_quads > 2) ? p_visible_quads : 2;
	if(visible_quads > 8192) visible_quads = 8192;
	tilestore_bytes = p_tilestore_bytes;
	vr_from_start = start_vr;

	int err = sysinit();
	while(err == 0) 
		err = systick();
	int serr = sysshutdown();
	if(err && err != 1) yr_msgbox("An error has occured and the program will be closed. Please check the error log for more details.");
	else if(serr) yr_msgbox("An error has occured while closing the program. Please check the error log for more details.");
	return err ? err : serr;
}

void yrSystem_register(enum yrSysId id,
					   yrSystem_init_func my_init,
					   yrSystem_tick_func my_tick,
					   yrSystem_save_func my_save,
					   yrSystem_canfreeze_func my_canfreeze,
					   yrSystem_freeze_func my_freeze,
					   yrSystem_unfreeze_func my_unfreeze,
					   yrSystem_shutdown_func my_shutdown,
					   unsigned int my_dependencies)
{
	YR_ASSERT(id < sys_end);
	YR_ASSERT(my_init);
	YR_ASSERT(my_save);
	YR_ASSERT(my_canfreeze);
	YR_ASSERT(my_freeze);
	YR_ASSERT(my_unfreeze);
	YR_ASSERT(my_tick);
	YR_ASSERT(my_shutdown);
	YR_ASSERT(my_dependencies < (1<<sys_end));
	init[id] = my_init;
	save[id] = my_save;
	canfreeze[id] = my_canfreeze;
	freeze[id] = my_freeze;
	unfreeze[id] = my_unfreeze;
	tick[id] = my_tick;
	shutdown[id] = my_shutdown;
	dependencies[id] = my_dependencies;
}

size_t yrSystem_visible_quads(void) { return visible_quads; }
size_t yrSystem_get_tilestore_bytes(void) { return tilestore_bytes ? tilestore_bytes : (1*1024*1024); }
void yrSystem_set_tilestore_bytes(size_t bytes) { if(!tilestore_bytes) {tilestore_bytes = (bytes < (1*1024*1024)) ? (1*1024*1024) : bytes;} }
void* yrSystem_scenefile(void) { return scenefile; }
int yrSystem_debugmode(void) {return 0;/*0;*/}

static int sysraise(const char* savepath);
static int syslower(void);

static int sysinit(void)
{
	//prepare save event
	quitting = 0;
	save_next = yr_get_microtime() + save_interval;
	save_event = yrEvent_create();
	if(!save_event) return -2;

	//register all systems
	yrRender_reg();
	yrVR_reg();
	yrQuadStore_reg();
	yrTileStore_reg();
	yrInteraction_reg();
	yrPlatform_reg();
	yrLog_reg();
	yrUndo_reg();
	yrSteam_reg();
	yrDesktop_reg();

	//check whether all functions are there
	for(size_t i = 0; i < sys_end; ++i) {
		if(!init[i] || !tick[i] || !shutdown[i] || !save[i] || !canfreeze[i] || !freeze[i] || !unfreeze[i]) {
			YR_ASSERT(0 && "Incomplete system detected.");
			yrEvent_destroy(save_event);
			save_event = NULL;
			return -1;
		}
	}

	//determine load order
	unsigned loaded = 0;
	unsigned load_target = (1 << sys_end) - 1;
	unsigned load_idx = 0;
	for(size_t h = 0; h < sys_end; ++h)
	for(size_t i = 0; i < sys_end; ++i) {
		/*in the worst case each iteration only adds a single system,
		if after the double loop the system is not fully loaded there is a dependency failure*/
		if(loaded & (1 << i)) continue;
		if((dependencies[i] & loaded) == dependencies[i]) {
			order[load_idx] = i;
			loaded |= (1 << i);
			load_idx += 1;
		}
	}
	if(loaded != load_target) {
		YR_ASSERT(0 && "Failed to establish system load order.");
		yrEvent_destroy(save_event);
		save_event = NULL;
		return -1;
	}

	//initialize the log
	int err = 0;
	YR_ASSERT(order[0] == sysLog);
	err = init[sysLog](NULL);
	initlevel = 1;

	//set the requested loadlevel for sysDesktop, if we want go straight to VR then sysDesktop should do that
	yrSystem_vr_stop();

	//init
	err = sysraise(NULL);

	//done
	if(!err) return 0;
	else {
		sysshutdown();
		return err;
	}
}

static int sysraise(const char* savepath)
{
	int err = 0;
	//open the scenefile and get load tokens
	yrReadHandle* rh = NULL;
	if(savepath) {
		scenefile = yrSceneFile_open(savepath, 0);
		if(scenefile) rh = yrSFRead_start(scenefile);
		if(!rh) err = -1;
		else {
			size_t rw = yrSFRead_read(rh, sizeof(save_sysptr), &save_sysptr);
			if(rw != sizeof(save_sysptr)) err = -1;
		}
	}
	//load the rest of the systems
	for(; !err && initlevel < requested_level; ++initlevel) {
		if(rh) err = yrSFRead_seek(rh, save_sysptr[order[initlevel]], 0);
		if(err) break;
		err = init[order[initlevel]](rh);
	}
	if(rh) yrSFRead_end(rh);
	return err;
}

static int syslower(void)
{
	int ret = 0;
	//wind down and return the first errorcode
	for(; initlevel > requested_level; --initlevel) {
		int err = shutdown[order[initlevel - 1]]();
		if(!ret && err) ret = err;
	}
	return ret;
}

static int sysshutdown(void)
{
	requested_level = 1; //shut down everything but logging
	int ret = syslower();

	//close scenefile
	free(scenefilename);
	scenefilename = NULL;
	if(scenefile) yrSceneFile_close(scenefile);
	scenefile = NULL;
	//shut down logging
	int err = shutdown[sysLog]();
	if(!ret && err) ret = err;

	yrEvent_destroy(save_event);
	save_event = NULL;
	save_next = 0;
	return ret;
}

static int systick(void)
{
	//handle the (auto-)save
	int savedone = syssave();
	if(quitting && savedone) return 1;
	//raise level
	if(savedone && initlevel < requested_level)
	{
		unsigned oldlevel = initlevel;
		if(!scenefilename) scenefilename = _strdup("default.ysf");
		int err = sysraise(scenefilename);
		if(err) requested_level = oldlevel;
	}
	//lower level
	if(savedone && initlevel > requested_level)
	{
		syslower();
		if(scenefile) yrSceneFile_close(scenefile);
		scenefile = NULL;
	}

	uint64_t t_full_a, t_interact_a, t_interact_b, t_full_b;
	t_full_a = yr_get_microtime();
	//call the tick functions, we do this manually because the order somewhat matters, should more systems be made this could possibly be automated as well
	int err, quit;
	//basic system ticks
	quit = tick[sysPlatform]();
	err = tick[sysSteam](); 		if(err) return err;
	err = tick[sysDesktop](); 		if(err) return err;
	t_interact_a = yr_get_microtime();
	if(initlevel != sys_end) {
		yr_sleep(15000); //waitgetposes doesn't force us to wait because vr isn't running so this will have to do
	}
	//vr and related systems ticks
	else {
		err = tick[sysInteraction]();	if(err) return err;
		t_interact_b = yr_get_microtime();
		err = tick[sysQuadStore]();		if(err) return err;
		err = tick[sysVR](); 			if(err) {
			if(err > 0) quit = 1;
			else return err; //calls WaitGetPoses() and sets an event when this finishes
		}
		t_full_b = yr_get_microtime();
		err = tick[sysTileStore](); 	if(err) return err; //must run on thread with the OpenGL context, uses sysVR's finish event to stop
		err = tick[sysRender]();		if(err) return err; //after all other ticks finish we render with the latest poses
		
		//report timings
		float t_interact = (t_interact_b - t_interact_a)/1000000.0f;
		float t_other = (t_full_b - t_full_a - t_interact_b + t_interact_a)/1000000.0f;
		yrRender_barstat(bsTimeInteraction, t_interact);
		yrRender_barstat(bsTimeOther, t_other);
	}
	if(quit) {
		quitting = 1;
		save_next = 0;
		yrSystem_freezenow();
	}
	return 0;
}

static int syssave(void)
{
	//save in progress
	if(save_handle != NULL) {
		//if a subsystem finished saving
		if(yrEvent_wait(save_event, 0) == 0) {
			yrEvent_set(save_event, 0);
			save_cursys += 1;
			//failure
			if(save_failed) {
				yrSFWrite_abort(save_handle);
				save_handle = NULL;
				for(size_t i = 0; i < sys_end; ++i)
					unfreeze[i]();
				save_next = yr_get_microtime() + save_interval;
				yrLogAlert(0, 0xFF8080FF, 7000000, "Save failed.");
				return 1;
			}
			//all subsystems done
			else if(save_cursys == (int)sys_end) {
				int err = yrSFWrite_end(save_handle, save_sysptr);
				save_handle = NULL;
				for(size_t i = 0; i < sys_end; ++i)
					unfreeze[i]();
				save_next = yr_get_microtime() + save_interval;
				if(err) yrLogAlert(0, 0xFF8080FF, 7000000, "Save failed.");
				else yrLogAlert(0, 0xFFFFFFFF, 4000000, "Save complete.");
				return 1;
			}
			//next subsystem
			else {
				save_sysptr[order[save_cursys]] = yrSFWrite_getptr(save_handle);
				save[order[save_cursys]](save_handle, save_event, &save_failed);
			}
		}
	}
	//perform the autosave
	else if(yr_get_microtime() > save_next) {
		if(!scenefile) return 1;
		save_cursys = 0;
		int freeze_ok = 1;
		for(size_t i = 0; freeze_ok && i < sys_end; ++i)
			freeze_ok = canfreeze[i]();
		//if all systems are ready for it
		if(freeze_ok) {
			for(size_t i = 0; i < sys_end; ++i)
				freeze[i]();
			save_handle = yrSFWrite_start(scenefile, sizeof(save_sysptr));
			if(!save_handle) {
				for(size_t i = 0; i < sys_end; ++i)
					unfreeze[i]();
				yrLogAlert(0, 0xFF8080FF, 7000000, "Failed to start save.");
			}
			save_failed = 0;
			save_sysptr[order[0]] = 0;
			save[order[0]](save_handle, save_event, &save_failed);
			yrLogAlert(0, 0xFFFFFFFF, 4000000, "Saving...");
		}
	}
	return 0;
}

void yrSystem_vr_init(void) {
	save_next = 0;
	requested_level = sys_end;
}
void yrSystem_vr_stop(void) {
	save_next = 0; //also triggers an autosave
	unsigned lv = 0;
	while(order[lv]!=sysDesktop)
		lv += 1;
	requested_level = lv + 1;
	yrSystem_freezenow();
} 
int yrSystem_vr_state(void) { return (initlevel == sys_end); }
int yrSystem_vr_fromstart(void) { return vr_from_start; }

void yrSystem_set_scenefilename(const char* scenec) { free(scenefilename); scenefilename = scenec ? _strdup(scenec) : NULL;}
const char*	yrSystem_get_scenefilename(void) { return scenefilename; }

int yrSystem_seeksys(yrReadHandle* rh, enum yrSysId sys)
{
	int err = 0;
	size_t rw;
	int64_t sysoff;
	err = yrSFRead_seek(rh, sys * 8, 0);	if(err) return err;
	rw  = yrSFRead_read(rh, 8, &sysoff);	if(rw != 8) return -1;
	err = yrSFRead_seek(rh, sysoff, 0);		if(err) return err;
	return 0;
}

int yrSystem_initinsert(yrSceneFile* sf, yrReadHandle** rh, yrWriteHandle** wh, enum yrSysId sys)
{
	size_t rw, pre;
	const size_t spsize = sizeof(save_sysptr);

	*wh = yrSFWrite_start(sf, spsize);					if(!*wh) goto onerror;
	*rh = yrSFRead_start(sf);							if(!*rh) goto onerror;
	rw  = yrSFRead_read(*rh, spsize, &save_sysptr);		if(rw != spsize) goto onerror;
	pre = save_sysptr[sys] - spsize;
	rw = yrSFWrite_copy(*wh, *rh, pre);					if(rw != pre) goto onerror;
	return 0;
onerror:
	if(*rh) yrSFRead_end(*rh);
	if(*wh) yrSFWrite_abort(*wh);
	*rh = NULL;
	*wh = NULL;
	return -1;
}

int yrSystem_endinsert(yrReadHandle* rh, yrWriteHandle* wh, enum yrSysId sys)
{
	int64_t pos, adjust, foo;
	//get offset
	pos = yrSFWrite_getptr(wh);
	//find system right after sys in the stream
	foo = INT64_MAX;
	for(size_t i = 0; i < sys_end; i += 1)
		if(save_sysptr[i] > save_sysptr[sys] && save_sysptr[i] < foo)
			foo = save_sysptr[i];
	adjust = pos - foo;
	//update ptr table
	for(size_t i = 0; i < sys_end; i += 1)
		if(save_sysptr[i] > save_sysptr[sys])
			save_sysptr[i] += adjust;	
	//copy rest of stream
	yrSFWrite_copy(wh, rh, INT64_MAX);
	//close handles
	yrSFRead_end(rh);
	return yrSFWrite_end(wh, save_sysptr);
}

void yrSystem_abortinsert(yrReadHandle* rh, yrWriteHandle* wh)
{
	yrSFRead_end(rh);
	yrSFWrite_abort(wh);
}

void yrSystem_freezenow(void)
{
	for(size_t i = 0; i < sys_end; ++i)
		freeze[i]();
}

void yrSystem_unfreeze(void)
{
	for(size_t i = 0; i < sys_end; ++i)
		unfreeze[i]();
}

int yrSystem_reached_runlevel(void) { return initlevel == requested_level; }