extern "C" {
#include "sys_vr.h"
#include "sys_vr_passthru.h"
#include "sys_log.h"
#include "system.h"
#include "sys_interaction.h"
}

#include <openvr.h>

extern "C" static int yrVR_init(void* loadtoken);
extern "C" static int yrVR_tick(void);
extern "C" static int yrVR_save(void* savetoken, void* saveevent, int* savefailed);
extern "C" static int yrVR_canfreeze(void) {return 1;}
extern "C" static int yrVR_freeze(void) {return 0;}
extern "C" static int yrVR_unfreeze(void) {return 0;}
extern "C" static int yrVR_shutdown(void);

extern "C" void yrVR_reg(void)
{
	yrSystem_register(sysVR,
					  yrVR_init,
					  yrVR_tick,
					  yrVR_save,
					  yrVR_canfreeze,
					  yrVR_freeze,
					  yrVR_unfreeze,
					  yrVR_shutdown,
					  (1<<sysLog)|(1<<sysPlatform)|(1<<sysSteam)|(1<<sysDesktop));
}

/************
* Static vars
*************/
static vr::IVRSystem* vrsystem = NULL;
static vr::IVRCompositor* vrcomp = NULL;
static vr::IVRChaperone* vrchap = NULL;

static vec4i coord_offset = {0};
static mat4f hmd_from_world = {0};
static const mat4f world_from_room = {{
	{{1.0, 0.0, 0.0, 0.0}},
	{{0.0, 0.0, 1.0, 0.0}},
	{{0.0, -1.0, 0.0, 0.0}},
	{{0.0, 0.0, 0.0, 1.0}}}
};
static const mat4f identity = {{
	{{1.0, 0.0, 0.0, 0.0}},
	{{0.0, 1.0, 0.0, 0.0}},
	{{0.0, 0.0, 1.0, 0.0}},
	{{0.0, 0.0, 0.0, 1.0}}}
};

static int interaction_ready = 0;

static yrThread poses_thread = NULL;
static yrEvent poses_ready = NULL;
static yrEvent poses_go = NULL;
static uint8_t poses_stop = 0;
static vr::TrackedDevicePose_t poses[vr::k_unMaxTrackedDeviceCount];

/*****************
* Public functions
******************/
extern "C" yrEvent	yrVR_sync_waitgetposes(void) { return poses_ready; }
extern "C" void		yrVR_set_coord_offset(vec4i pos) { coord_offset = pos; }
extern "C" vec4i	yrVR_get_coord_offset(void) { return coord_offset; }
extern "C" mat4f	yrVR_hmd_from_world(void) { return hmd_from_world; }
extern "C" mat4f	yrVR_world_from_tracked(uint32_t idx)
{
	YR_ASSERT(idx < k_unMaxTrackedDeviceCount)
	if(poses[idx].bPoseIsValid) {
		mat4f devicepos = mat34f_rows_convert((const float*) &poses[idx].mDeviceToAbsoluteTracking.m);
		return mat4f_combine(world_from_room, devicepos);
	}
	else {
		return identity;
	}
}

extern "C" void yrVR_ready_interaction(int on_off)
{
	interaction_ready = on_off;
	if(on_off) {
		//detect currently connected controllers and notify interaction of them
		for(uint32_t idx = 0; idx < vr::k_unMaxTrackedDeviceCount; ++idx)
		{
			if(!vrsystem->IsTrackedDeviceConnected(idx)) continue;
			if(vrsystem->GetTrackedDeviceClass(idx) == vr::TrackedDeviceClass_Controller)
				yrInteraction_notifycontroller(idx, 1);
		}
	}
}

extern "C" vec4i yrVR_cold_get_coord_offset(yrSceneFile* sf)
{
	vec4i out;
	vec4i zero = {0,0,0,0};
	yrReadHandle* rh = yrSFRead_start(sf);				if(!rh) return zero;
	int err = yrSystem_seeksys(rh, sysVR);				if(err) { yrSFRead_end(rh); return zero;}
	size_t rw = yrSFRead_read(rh, sizeof(out), &out);	if(rw != sizeof(out)) { yrSFRead_end(rh); return zero;};
	yrSFRead_end(rh);
	return out;
}

/*****************
* Poses threadfunc
******************/
extern "C" static void poses_threadfunc(void* param)
{
	for(;;) {
		yrEvent_wait(poses_go, 0xFFFFFFFFFFFFFFFFull);
		yrEvent_set(poses_go, 0);
		if(poses_stop) break;
		vrcomp->WaitGetPoses(poses, vr::k_unMaxTrackedDeviceCount, NULL, 0);

		if(poses[vr::k_unTrackedDeviceIndex_Hmd].bPoseIsValid) {
			mat4f room_from_hmd = mat34f_rows_convert((const float*) poses[k_unTrackedDeviceIndex_Hmd].mDeviceToAbsoluteTracking.m);
			hmd_from_world = mat4f_invert(mat4f_combine(world_from_room, room_from_hmd));
		}
		yrEvent_set(poses_ready, 1);
	}
}

/****************************************
* System functions (init, tick, shutdown)
*****************************************/
extern "C" static int yrVR_init(void* loadtoken)
{
	interaction_ready = 0;
	//OpenVR init
	{
		//check hmd
		if(!vr::VR_IsHmdPresent()) { yrLogAlert(0, 0xFFFFFFFFul, 5000000ull, "No HMD detected. Please connect an OpenVR-compatible head-mounted display to your computer."); return -1;}

		//init openvr
		vr::EVRInitError eError;
		vrsystem = vr::VR_Init(&eError, vr::VRApplication_Scene);
		if(eError != EVRInitError_VRInitError_None) {
			{ yrLog(0, "OpenVR initialization failed.\n%s", vr::VR_GetVRInitErrorAsEnglishDescription(eError)); return -1; }
		}

		//get interfaces
		vrcomp = vr::VRCompositor();	if(!vrcomp) { yrLog(0, "OpenVR Compositor not available."); yrVR_shutdown(); return -1; }
		vrchap = vr::VRChaperone();		if(!vrchap) { yrLog(0, "OpenVR Chaperone not available."); yrVR_shutdown(); return -1; }
	}

	//default coords
	{
		const vec4i zero = {{0,0,0,0}};
		coord_offset = zero;
		hmd_from_world = identity;
	}

	//WaitGetPoses thread
	{
		poses_stop = 0;
		poses_ready = yrEvent_create();		if(!poses_ready)	{ yrLog(0, "OS failed to create a synchonization event (WaitGetPoses)."); yrVR_shutdown(); return -1; }
		poses_go = yrEvent_create();		if(!poses_go)		{ yrLog(0, "OS failed to create a synchonization event (WaitGetPoses)."); yrVR_shutdown(); return -1; }
		poses_thread = yrThread_create(poses_threadfunc, NULL);	if(!poses_thread)	{ yrLog(0, "OS failed to create a thread (WaitGetPoses)."); yrVR_shutdown(); return -1; }
	}

	//load from stream
	{
		size_t rw = yrSFRead_read((yrReadHandle*)loadtoken, sizeof(coord_offset), &coord_offset);
		if(rw != sizeof(coord_offset)) {yrVR_shutdown(); return -1;}
	}
	return 0;
}

extern "C" static int yrVR_shutdown(void)
{
	if(poses_thread)
	{
		poses_stop = 1;
		yrEvent_set(poses_go, 1);
		int err = yrThread_join(poses_thread, 111000); //wait up to 111ms, after that we leak the thread
		if(err > 0) yrLog(1, "Thread join timeout (WaitGetPoses).");
	}
	poses_thread = NULL;

	if(poses_go) yrEvent_destroy(poses_go);
	if(poses_ready) yrEvent_destroy(poses_ready);
	poses_go = NULL;
	poses_ready = NULL;

	if(vrsystem) vr::VR_Shutdown();
	vrsystem = NULL;
	vrcomp = NULL;
	vrchap = NULL;
	return 0;
}

extern "C" static int yrVR_tick(void)
{
	int exit = 0;
	vr::VREvent_t vrevent;
	while(vrsystem->PollNextEvent(&vrevent, sizeof(vrevent))) {
		switch(vrevent.eventType) {
		case vr::VREvent_TrackedDeviceActivated:
		{
			if(interaction_ready) {
				if(vrsystem->GetTrackedDeviceClass(vrevent.trackedDeviceIndex) == vr::TrackedDeviceClass_Controller)
					yrInteraction_notifycontroller(vrevent.trackedDeviceIndex, 1);
			}
			break;
		}
		case vr::VREvent_TrackedDeviceDeactivated:
		{
			if(interaction_ready)
				if(vrsystem->GetTrackedDeviceClass(vrevent.trackedDeviceIndex) == vr::TrackedDeviceClass_Controller)
					yrInteraction_notifycontroller(vrevent.trackedDeviceIndex, 0);
			break;
		}
		case vr::VREvent_Quit:
			exit = 1;
			break;
		case vr::VREvent_InputFocusCaptured: //deprecated event but what is the alternative?
			if(vrevent.data.process.oldPid == 0)
				yrSystem_freezenow();
			break;
		case vr::VREvent_InputFocusReleased:
			if(vrevent.data.process.pid == 0)
				yrSystem_unfreeze();
			break;
		}
	}
	yrEvent_set(poses_go, 1);
	return exit;
}

extern "C" static int yrVR_save(void* savetoken, void* saveevent, int* savefailed)
{
	size_t rw = yrSFWrite_write((yrWriteHandle*) savetoken, sizeof(coord_offset), &coord_offset);
	if(rw != sizeof(coord_offset)) *savefailed = 1;
	yrEvent_set(saveevent, 1);
	return 0;
}

/****************************
* OpenVR function passthrough
*****************************/

extern "C" void ovrSystem_GetRecommendedRenderTargetSize(uint32_t* width, uint32_t* height)
{        vrsystem->GetRecommendedRenderTargetSize(width, height); }

extern "C" struct HmdMatrix44_t ovrSystem_GetProjectionMatrix(EVREye eEye, float fNearZ, float fFarZ)
{ return *(struct HmdMatrix44_t*) &vrsystem->GetProjectionMatrix((vr::EVREye)eEye, fNearZ, fFarZ); }

extern "C" struct HmdMatrix34_t ovrSystem_GetEyeToHeadTransform(EVREye eEye)
{ return *(struct HmdMatrix34_t*) &vrsystem->GetEyeToHeadTransform((vr::EVREye)eEye); }

extern "C" int ovrSystem_GetControllerState(TrackedDeviceIndex_t unControllerDeviceIndex, VRControllerState_t * pControllerState)
{ return vrsystem->GetControllerState(unControllerDeviceIndex, (vr::VRControllerState_t*) pControllerState, sizeof(VRControllerState_t)); }

extern "C" void ovrSystem_TriggerHapticPulse(TrackedDeviceIndex_t unControllerDeviceIndex, uint32_t unAxisId, unsigned short usDurationMicroSec)
{        vrsystem->TriggerHapticPulse(unControllerDeviceIndex, unAxisId, usDurationMicroSec); }

extern "C" EVRCompositorError ovrCompositor_Submit(EVREye eEye, struct Texture_t * pTexture, struct VRTextureBounds_t * pBounds, EVRSubmitFlags nSubmitFlags)
{ return (EVRCompositorError)vrcomp->Submit((vr::EVREye)eEye, (vr::Texture_t*) pTexture, (vr::VRTextureBounds_t*)pBounds, (vr::EVRSubmitFlags)nSubmitFlags); }

extern "C" void ovrCompositor_PostPresentHandoff(void)
{ if(vrcomp) vrcomp->PostPresentHandoff(); }

extern "C" int ovrChaperone_GetPlayAreaSize(float * pSizeX, float * pSizeZ)
{ return vrchap->GetPlayAreaSize(pSizeX, pSizeZ); }

extern "C" void ovrCompositor_FadeToColor(float seconds, float r, float g, float b, float a)
{ vrcomp->FadeToColor(seconds, r, g, b, a); }

extern "C" HiddenAreaMesh_t ovrSystem_GetHiddenAreaMesh(EVREye eEye)
{ return *(HiddenAreaMesh_t*) &vrsystem->GetHiddenAreaMesh((vr::EVREye)eEye); }