#include "WMRimpl.hpp"

#include <..\um\SpatialInteractionManagerInterop.h>
#include <..\um\HolographicSpaceInterop.h>
#include <windows.storage.streams.h>
#include <Robuffer.h>

#include <ppltasks.h>
#include <cassert>

#include <tiny_gltf.h>

using namespace Windows::Graphics::DirectX::Direct3D11;
using namespace ABI::Windows::Storage::Streams;
using namespace ABI::Windows::UI::Input::Spatial;

bool WMRimpl::init(HWND hwnd)
{
	if (!_initWrl())
		return false;
	
	//Fix
	//Window has to be hidden in order the following call succeeds
	ShowWindow(hwnd, SW_HIDE);
	bool retval = _createHolographicSpace(hwnd);
	if (!retval)
		return false;

	if (!_createHolographicDisplay())
		return false;

	if (!_createSpatialLocator())
		return false;

	if (!_createRenderer())
		return false;

	if (!_createSpatialInteractionManager(hwnd))
		return false;

	_registerCallbacks();

	if (!_getCameraParams())
		return false;
	
	return true;
}

bool WMRimpl::_initWrl()
{
	HRESULT hr = CoInitializeEx(nullptr, 0);
	if (!SUCCEEDED(hr))
		return false;

	return true;
}

bool WMRimpl::_createHolographicSpace(HWND hwnd)
{
	
	ComPtr<IHolographicSpaceStatics> holographicSpaceFactory;
		
	auto hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_Graphics_Holographic_HolographicSpace).Get(), &holographicSpaceFactory);
	if (!SUCCEEDED(hr))
		return false;

	ComPtr<IHolographicSpaceInterop> holographicSpaceInterop;
	hr = holographicSpaceFactory.As(&holographicSpaceInterop);
	if (!SUCCEEDED(hr))
		return false;

	hr = holographicSpaceInterop->CreateForWindow(hwnd, IID_PPV_ARGS(&_holographicSpace));	
	if (!SUCCEEDED(hr))
		return false;
	
	return true;
}

bool WMRimpl::_createHolographicDisplay()
{
	assert(_holographicSpace);
	
	ComPtr<IHolographicDisplayStatics> holoDisplayfactory;
	auto hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_Graphics_Holographic_HolographicDisplay).Get(), &holoDisplayfactory);
	if (!SUCCEEDED(hr))
		return false;

	hr = holoDisplayfactory->GetDefault(&_display);
	if (!SUCCEEDED(hr))
		return false;
 
	return true;
}

bool WMRimpl::_createSpatialLocator()
{
	assert(_holographicSpace);
	assert(_display);

	auto hr = _display->get_SpatialLocator(&_spatialLocator);
	if (!SUCCEEDED(hr))
		return false;

	hr = _spatialLocator->CreateStationaryFrameOfReferenceAtCurrentLocation(&_stationaryReferenceFrame);
	if (!SUCCEEDED(hr))
		return false;

	return true;
}

bool WMRimpl::_createSpatialInteractionManager(HWND hwnd)
{
	ComPtr<ISpatialInteractionManagerStatics> statics;

	auto hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_UI_Input_Spatial_SpatialInteractionManager).Get(), &statics);
	if (FAILED(hr))
		return false;

	ComPtr<ISpatialInteractionManagerInterop> interop;
	hr = statics.As(&interop);
	if (FAILED(hr))
		return false;

	hr = interop->GetForWindow(hwnd, IID_PPV_ARGS(&_spatialInteractionManager));
	if (FAILED(hr))
		return false;

	return true;
}

bool WMRimpl::_createRenderer()
{
	LUID adapterID;
	_holographicSpace->get_PrimaryAdapterId(reinterpret_cast<HolographicAdapterId*>(&adapterID));

	uint32_t w = 0, h = 0;
	getPerEyeResolution(w, h);

	_renderer = std::make_unique<Dx11OglRenderer>();
	if (!_renderer->init(adapterID, w, h))
		return false;

	ComPtr<IDXGIDevice> dev = _getRendererDxgiDevice();
	ComPtr<IDirect3DDevice> d3ddev = _convertDxgiDeviceToIDirect3DDevice(dev);

	auto hr = _holographicSpace->SetDirect3D11Device(d3ddev.Get());
	if (!SUCCEEDED(hr))
		return false;

	return true;
}

ComPtr<IDXGIDevice> WMRimpl::_getRendererDxgiDevice()
{
	ComPtr<IDXGIDevice> dev;
	ComPtr<ID3D11Device> device = _renderer->getDevice();

	auto hr = device->QueryInterface(__uuidof(IDXGIDevice), (void **)&dev);
	if (!SUCCEEDED(hr))
		return nullptr;

	return dev;
}

ComPtr<IDirect3DDevice> WMRimpl::_convertDxgiDeviceToIDirect3DDevice(ComPtr<IDXGIDevice> dxgiDevice)
{
	ComPtr<IInspectable> inspectableSurface;
	ComPtr<IDirect3DDevice> device;

	if (SUCCEEDED(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.Get(), &inspectableSurface)))
	{
		auto hr = inspectableSurface.As(&device);
		if (!SUCCEEDED(hr))
			return nullptr;
	}

	return device;
}

void WMRimpl::_registerCallbacks()
{
	assert(_holographicSpace);
	assert(_spatialLocator);
	assert(_spatialInteractionManager);

	//Camera added
	auto callback_camAdded = Callback< ITypedEventHandler<HolographicSpace*, HolographicSpaceCameraAddedEventArgs*>>(std::bind(&WMRimpl::_onCameraAdded, this, std::placeholders::_1, std::placeholders::_2));
	_holographicSpace->add_CameraAdded(callback_camAdded.Get(), &_cameraAddedToken);

	//Camera removed
	auto callback_camRemoved = Callback< ITypedEventHandler<HolographicSpace*, HolographicSpaceCameraRemovedEventArgs*>>(std::bind(&WMRimpl::_onCameraRemoved, this, std::placeholders::_1, std::placeholders::_2));
	_holographicSpace->add_CameraRemoved(callback_camRemoved.Get(), &_cameraRemovedToken);

	//Tracking state change (is working / positional not working /...)
	//TODO mozno zbytocne
	auto callback_trackingState = Callback< ITypedEventHandler<SpatialLocator*, IInspectable*>>(std::bind(&WMRimpl::_onTrackingStateChanged, this, std::placeholders::_1, std::placeholders::_2));
	_spatialLocator->add_LocatabilityChanged(callback_trackingState.Get(), &_trackingChangedToken);

	//Display state changed
	//auto callback_displayAvailable = Callback< IEventHandler<IInspectable*>>(std::bind(&WMRimpl::_onHolographicDisplayChanged, this, std::placeholders::_1, std::placeholders::_2));
	//_holographicStateStatics2->add_IsAvailableChanged(callback_displayAvailable.Get(), &_displayAvailableToken);

	//Controller added
	auto callback_controllerDetected = Callback< ITypedEventHandler<SpatialInteractionManager*, SpatialInteractionSourceEventArgs*> >(std::bind(&WMRimpl::_onControllerDetected, this, std::placeholders::_1, std::placeholders::_2));
	_spatialInteractionManager->add_SourceDetected(callback_controllerDetected.Get(), &_controllerAdded);

	//Controller removed/lost
	auto callback_controllerLost = Callback< ITypedEventHandler<SpatialInteractionManager*, SpatialInteractionSourceEventArgs*> >(std::bind(&WMRimpl::_onControllerLost, this, std::placeholders::_1, std::placeholders::_2));
	_spatialInteractionManager->add_SourceLost(callback_controllerLost.Get(), &_controllerRemovedLost);

	//Controller button pressed
	auto callback_controllerPressed = Callback< ITypedEventHandler<SpatialInteractionManager*, SpatialInteractionSourceEventArgs*> >(std::bind(&WMRimpl::_onControllerPressed, this, std::placeholders::_1, std::placeholders::_2));
	_spatialInteractionManager->add_SourcePressed(callback_controllerPressed.Get(), &_controllerPressed);

	//Controller transform
	auto callback_controllerUpdated = Callback< ITypedEventHandler<SpatialInteractionManager*, SpatialInteractionSourceEventArgs*> >(std::bind(&WMRimpl::_onControllerUpdated, this, std::placeholders::_1, std::placeholders::_2));
	_spatialInteractionManager->add_SourceUpdated(callback_controllerUpdated.Get(), &_controllerTransformUpdated);
}

ComPtr<IHolographicFrame> WMRimpl::_beginFrame()
{
	assert(_holographicSpace);
	
	ComPtr<IHolographicFrame> holographicFrame;
	
	auto hr = _holographicSpace->CreateNextFrame(&holographicFrame);
	if (!SUCCEEDED(hr))
		return nullptr;

	return holographicFrame;

	return nullptr;
}

HRESULT WMRimpl::_onCameraAdded(IHolographicSpace* holographicSpace, IHolographicSpaceCameraAddedEventArgs* args)
{
	ComPtr<IDeferral> deferral;
	args->GetDeferral(&deferral);
	
	ComPtr<IHolographicCamera> camera;
	args->get_Camera(&camera);
		
	Concurrency::create_task([this, deferral, camera]()
	{
		boolean isStereo = false;
		camera->get_IsStereo(&isStereo);

		UINT32 camID;
		camera->get_Id(&camID);

		double val;
		camera->get_ViewportScaleFactor(&val);

		deferral->Complete();
	});

	return 0;
}

HRESULT WMRimpl::_onCameraRemoved(IHolographicSpace* holographicSpace, IHolographicSpaceCameraRemovedEventArgs* args)
{
	ComPtr<IHolographicCamera> camera;
	args->get_Camera(&camera);
	UINT id = 0;
	camera->get_Id(&id);

	_removeCameraByID(id);

	return 0;
}

HRESULT WMRimpl::_onTrackingStateChanged(ISpatialLocator* locator, IInspectable* object)
{
	return 0;
}

HRESULT WMRimpl::_onHolographicDisplayChanged(IInspectable* object1, IInspectable* object2)
{
	return 0;
}

HRESULT WMRimpl::_onControllerDetected(ISpatialInteractionManager* manager, ISpatialInteractionSourceEventArgs* args)
{
	ComPtr<ISpatialInteractionSourceState> state;
	args->get_State(&state);

	ComPtr<ISpatialInteractionSource> source;
	state->get_Source(&source);

	std::vector<unsigned char> controllerGltf;
	_acquireControllerGLTFData(source, controllerGltf);
	
	//TODO - dorobit, cez tiny_GLTF sparsovat a vytahat geometriu
	//plus nejaka mapa ze ci uz nahodou nebol model nacitany
	//resp. cez ID kontrolera to spravit (zistit ci kontroler dostava IDcka)
	
	return 0;
}

void WMRimpl::_acquireControllerGLTFData(ComPtr<ISpatialInteractionSource> source, std::vector<unsigned char>& gltfData)
{
	ComPtr<ISpatialInteractionSource2> source2;
	source.As(&source2);

	ComPtr<ISpatialInteractionController> controller;
	source2->get_Controller(&controller);

	ComPtr<ISpatialInteractionController2> controller2;
	controller.As(&controller2);

	ComPtr<IAsyncOperation<IRandomAccessStreamWithContentType*>> operation;
	controller2->TryGetRenderableModelAsync(&operation);;

	//Async load inspired by
	//http://resemblances.click3.org/product_list/index.cgi/files/55/TestWinRT/TestWinRT.cpp
	Event evt(CreateEventEx(nullptr, nullptr, CREATE_EVENT_MANUAL_RESET, EVENT_ALL_ACCESS));
	auto c = Callback< IAsyncOperationCompletedHandler<IRandomAccessStreamWithContentType*>>(
		[&evt,&gltfData](IAsyncOperation<IRandomAccessStreamWithContentType*>* operation, AsyncStatus status)->HRESULT
	{
		if (status == Completed)
		{
			//The controller model is returned in glTF format
			ComPtr<IRandomAccessStreamWithContentType> result;
			auto hr = operation->GetResults(&result);
			
			//Get data size
			ComPtr<IRandomAccessStream> istream;
			hr = result.As(&istream);
			UINT64 dataSize = 0;
			istream->get_Size(&dataSize);
			
			//Prepare buffer for the data
			ComPtr<IBuffer> buffer;
			ComPtr<IBufferFactory> bufferFactory;
			hr = GetActivationFactory(HStringReference(RuntimeClass_Windows_Storage_Streams_Buffer).Get(), &bufferFactory);
			if (FAILED(hr))
				return S_OK;

			hr = bufferFactory->Create(dataSize, &buffer);
			if (FAILED(hr))
				return S_OK;

			ComPtr<IInputStream> readStream;
			result.As(&readStream);
			ComPtr<IAsyncOperationWithProgress<IBuffer*, UINT32>> readOp;
			hr = readStream->ReadAsync(buffer.Get(), dataSize, InputStreamOptions::InputStreamOptions_None, &readOp);
			if (FAILED(hr))
				return S_OK;
			
			//Read the data finally (async again...)
			Event et(CreateEventEx(nullptr, nullptr, CREATE_EVENT_MANUAL_RESET, EVENT_ALL_ACCESS));
			auto cb = Callback<IAsyncOperationWithProgressCompletedHandler<IBuffer*, UINT32>>(
				[&et, &gltfData, &dataSize](IAsyncOperationWithProgress<IBuffer*, UINT32>* operation, AsyncStatus status)->HRESULT
			{
				if (status == Completed)
				{
					ComPtr<IBuffer> b;
					operation->GetResults(&b);
					
					ComPtr<Windows::Storage::Streams::IBufferByteAccess> bufferByteAccess;
					reinterpret_cast<IInspectable*>(b.Get())->QueryInterface(IID_PPV_ARGS(&bufferByteAccess));
					unsigned char* data = nullptr;

					bufferByteAccess->Buffer(&data);

					gltfData.resize(dataSize);
					memcpy(gltfData.data(), data, dataSize);

					SetEvent(et.Get());
				}

				return S_OK;
			});

			hr = readOp->put_Completed(cb.Get());
			if (FAILED(hr))
				return S_OK;

			WaitForSingleObjectEx(et.Get(), INFINITE, FALSE);

			SetEvent(evt.Get());
		}

		return S_OK;
	});

	operation->put_Completed(c.Get());

	WaitForSingleObjectEx(evt.Get(), INFINITE, FALSE);
}

HRESULT WMRimpl::_onControllerLost(ISpatialInteractionManager* manager, ISpatialInteractionSourceEventArgs* args)
{
	return 0;
}

HRESULT WMRimpl::_onControllerPressed(ISpatialInteractionManager* manager, ISpatialInteractionSourceEventArgs* args)
{
	return 0;
}

HRESULT WMRimpl::_onControllerUpdated(ISpatialInteractionManager* manager, ISpatialInteractionSourceEventArgs* args)
{
	return 0;
}

void WMRimpl::_unregisterCallbacks()
{
	if (_holographicSpace)
	{
		_holographicSpace->remove_CameraAdded(_cameraAddedToken);
		_holographicSpace->remove_CameraRemoved(_cameraRemovedToken);
	}

	if (_spatialLocator)
		_spatialLocator->remove_LocatabilityChanged(_trackingChangedToken);

	if (_spatialInteractionManager)
	{
		_spatialInteractionManager->remove_SourceDetected(_controllerAdded);
		_spatialInteractionManager->remove_SourceLost(_controllerRemovedLost);
		_spatialInteractionManager->remove_SourcePressed(_controllerPressed);
		_spatialInteractionManager->remove_SourceUpdated(_controllerTransformUpdated);
	}

	//if (_holographicStateStatics2)
	//	_holographicStateStatics2->remove_IsAvailableChanged(_displayAvailableToken);
}

void WMRimpl::_presentFrame(ComPtr<IHolographicFrame> holographicFrame)
{
	HolographicFramePresentResult r;
	
	holographicFrame->PresentUsingCurrentPrediction(&r);
}

void WMRimpl::getPerEyeResolution(uint32_t& width, uint32_t& height) const
{
	assert(_display);

	Size s;
	_display->get_MaxViewportSize(&s);

	width = static_cast<uint32_t>(s.Width);
	height = static_cast<uint32_t>(s.Height);
}

bool WMRimpl::_getCameraParams()
{
	//A dirty hack
	//In order to get camera params, an empty frame(s) must be drawn
	//so we get our hands on the back buffer
	//Then - OGL buffers may be hooked up
	while (1)
	{
		auto frame = _beginFrame();

		ComPtr<IHolographicFramePrediction> prediction;
		auto hr = frame->get_CurrentPrediction(&prediction);
		if (!SUCCEEDED(hr))
			return false;

		ComPtr<ABI::Windows::Foundation::Collections::IVectorView<HolographicCameraPose*> > cameraPoses;
		hr = prediction->get_CameraPoses(&cameraPoses);
		if (!SUCCEEDED(hr))
			return false;

		unsigned int size = 0;
		cameraPoses->get_Size(&size);
		if (!size)
		{
			//Usually camera pose appears in the second frame for some reason...
			//Thats the reason for the while loop
			_presentFrame(frame);
			continue;
		}

		for (unsigned int i = 0; i < size; ++i)
		{
			ComPtr<IHolographicCameraPose> cameraPose;
			hr = cameraPoses->GetAt(i, &cameraPose);
			if (!SUCCEEDED(hr))
				return false;

			ComPtr<IHolographicCamera> camera;
			hr = cameraPose->get_HolographicCamera(&camera);
			if (!SUCCEEDED(hr))
				return false;

			unsigned int camId = 0;
			camera->get_Id(&camId);

			ComPtr<IHolographicCameraRenderingParameters> renderParams;
			hr = frame->GetRenderingParameters(cameraPose.Get(), &renderParams);
			if (!SUCCEEDED(hr))
				return false;

			if (!_updateRendererCameraById(renderParams, camId))
				return false;
		}

		_presentFrame(frame);

		break;
	}

	if (!_renderer->initSharedTexturesAfterLoadingCameraData())
		return false;

	return true;
}

void WMRimpl::submitFrame()
{
	_renderer->unlockTexturesFromGL();

	_renderer->copyTextures();

	_presentFrame(_currentFrame);

	_renderer->lockTexturesForGL();
}

void WMRimpl::frameStart()
{
	_currentFrame = _beginFrame();
}

void WMRimpl::getLeftRightTextures(GLuint& left, GLuint& right)
{
	_renderer->getGlTextures(left, right);
}

void WMRimpl::getTrackingData(float(&viewL)[16], float(&viewR)[16], float(&projL)[16], float(&projR)[16])
{
	assert(_currentFrame);
	
	ComPtr<IHolographicFramePrediction> prediction;
	auto hr = _currentFrame->get_CurrentPrediction(&prediction);
	if (!SUCCEEDED(hr))
		return;
	
	ComPtr<ABI::Windows::Foundation::Collections::IVectorView<HolographicCameraPose*> > cameraPoses;
	hr = prediction->get_CameraPoses(&cameraPoses);
	if (!SUCCEEDED(hr))
		return;
	
	unsigned int size = 0;
	cameraPoses->get_Size(&size);

	for (unsigned int i = 0; i < size; ++i)
	{
		ComPtr<IHolographicCameraPose> cameraPose;
		hr = cameraPoses->GetAt(i, &cameraPose);
		if (!SUCCEEDED(hr))
			return;
		
		//Get view transforms
		ComPtr<ISpatialCoordinateSystem> coordSystem;
		hr = _stationaryReferenceFrame->get_CoordinateSystem(&coordSystem);
		if (!SUCCEEDED(hr))
			return;
		
		ComPtr<IReference<HolographicStereoTransform>> vtransforms;
		hr = cameraPose->TryGetViewTransform(coordSystem.Get(), &vtransforms);
		if (!SUCCEEDED(hr))
			return;

		if (vtransforms == nullptr)
			return;

		HolographicStereoTransform viewTransform;
		vtransforms->get_Value(&viewTransform);

		_convertMatrix(viewTransform.Left, viewL);
		_convertMatrix(viewTransform.Right, viewR);

		//Get projection transforms
		HolographicStereoTransform projectionTransform;
		hr = cameraPose->get_ProjectionTransform(&projectionTransform);
		if (!SUCCEEDED(hr))
			return;
		
		_convertMatrix(projectionTransform.Left, projL);
		_convertMatrix(projectionTransform.Right, projR);

		ComPtr<IHolographicCamera> camera;
		hr = cameraPose->get_HolographicCamera(&camera);
		if (!SUCCEEDED(hr))
			return;
		
		//Adjust near and far distance
		camera->SetNearPlaneDistance(_nearZ);
		camera->SetFarPlaneDistance(_farZ);

		//Adjust rendering params
		unsigned int camId = 0;
		camera->get_Id(&camId);

		ComPtr<IHolographicCameraRenderingParameters> renderParams;
		hr = _currentFrame->GetRenderingParameters(cameraPose.Get(), &renderParams);
		if (!SUCCEEDED(hr))
			return;

		if (!_updateRendererCameraById(renderParams, camId))
			return;
	}
}

bool WMRimpl::_updateRendererCameraById(ComPtr<IHolographicCameraRenderingParameters> renderParams, unsigned int camId)
{
	ComPtr<IDirect3DSurface> surface;
	auto hr = renderParams->get_Direct3D11BackBuffer(&surface);
	if (!SUCCEEDED(hr))
		return false;

	ComPtr<IDirect3DDxgiInterfaceAccess> iface;
	hr = surface.As(&iface);
	if (!SUCCEEDED(hr))
		return false;

	ComPtr<ID3D11Texture2D> cameraBackBuffer;
	hr = iface->GetInterface(IID_PPV_ARGS(&cameraBackBuffer));
	if (!SUCCEEDED(hr))
		return false;

	if (!_renderer->createOrRecreateCameraData(cameraBackBuffer, camId))
		return false;

	return true;
}

void WMRimpl::_convertMatrix(const Numerics::Matrix4x4& matrix, float(&destMatrix)[16]) const
{
	destMatrix[0] = matrix.M11;
	destMatrix[1] = matrix.M12;
	destMatrix[2] = matrix.M13;
	destMatrix[3] = matrix.M14;
	destMatrix[4] = matrix.M21;
	destMatrix[5] = matrix.M22;
	destMatrix[6] = matrix.M23;
	destMatrix[7] = matrix.M24;
	destMatrix[8] = matrix.M31;
	destMatrix[9] = matrix.M32;
	destMatrix[10] = matrix.M33;
	destMatrix[11] = matrix.M34;
	destMatrix[12] = matrix.M41;
	destMatrix[13] = matrix.M42;
	destMatrix[14] = matrix.M43;
	destMatrix[15] = matrix.M44;
}

void WMRimpl::_removeCameraByID(unsigned int id)
{
	assert(_renderer);

	_renderer->removeCameraByID(id);
}

void WMRimpl::setNearFarDistance(float nearZ, float farZ)
{
	_nearZ = nearZ;
	_farZ = farZ;
}