#include "BstmDevice.h"

namespace Brainstem
{
	BstmDevice::BstmDevice(IKernel* kernel, HWND hwnd, int width, int height, bool fullscreen) 
	{ 
		Kernel = kernel;
		hWnd = hwnd;
		Width = width;
		Height = height;
		Fullscreen = fullscreen;
	}
	BstmDevice::~BstmDevice() { }

	bool BstmDevice::Init()
	{
		HRESULT hr = 0;
		D3DADAPTER_IDENTIFIER9 id = {0};
		D3DDISPLAYMODE desktop = {0};
		bool supportCSAA = FALSE;

		IDirect3D9* Direct3D;
		Direct3D = Direct3DCreate9(D3D_SDK_VERSION);

		if(!Direct3D)
			return ReportInitialisationError("Could not create device.");

		hr = Direct3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &desktop);

		REPORT_ERRORS(hr);

		D3DCAPS9 DeviceCaps;
		Direct3D->GetDeviceCaps(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &DeviceCaps);

		if (!(DeviceCaps.VertexShaderVersion >= D3DVS_VERSION(3,0) && DeviceCaps.PixelShaderVersion >= D3DPS_VERSION(2,0)))
			return ReportInitialisationError("Shader model 3.0 or higher is required.");

		if (SUCCEEDED(Direct3D->GetAdapterIdentifier(D3DADAPTER_DEFAULT, 0, &id)))
			supportCSAA = (id.VendorId == 0x10DE);
		else
			supportCSAA = false;

		D3DPRESENT_PARAMETERS DeviceParams;

		DeviceParams.hDeviceWindow = hWnd;
		DeviceParams.BackBufferWidth = Width;
		DeviceParams.BackBufferHeight = Height;
		DeviceParams.BackBufferFormat = desktop.Format;
		DeviceParams.BackBufferCount = 1;
		DeviceParams.Windowed = !Fullscreen;
		DeviceParams.EnableAutoDepthStencil = TRUE; 
		DeviceParams.AutoDepthStencilFormat = D3DFMT_D24S8;
		DeviceParams.SwapEffect = D3DSWAPEFFECT_DISCARD; 
		DeviceParams.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
		DeviceParams.Flags = 0;

		WaitForVerticalSync = true;

		if (!WaitForVerticalSync)
			DeviceParams.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE;
		else
			DeviceParams.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;

		if (supportCSAA)
		{
			ChooseBestCSAAMode(Direct3D, DeviceParams.BackBufferFormat, DeviceParams.AutoDepthStencilFormat,
				DeviceParams.Windowed, DeviceParams.MultiSampleType, DeviceParams.MultiSampleQuality);

			if (DeviceParams.MultiSampleType == D3DMULTISAMPLE_NONE)
				supportCSAA = false;
		}

		if (!supportCSAA)
		{
			ChooseBestMSAAMode(Direct3D, DeviceParams.BackBufferFormat, DeviceParams.AutoDepthStencilFormat,
				DeviceParams.Windowed, DeviceParams.MultiSampleType, DeviceParams.MultiSampleQuality,
				MSAASamples);
		}  

		//DeviceParams.MultiSampleQuality = 0;
		//DeviceParams.MultiSampleType = D3DMULTISAMPLE_NONE;

		hr = Direct3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
            D3DCREATE_HARDWARE_VERTEXPROCESSING | D3DCREATE_PUREDEVICE,
            &DeviceParams, &Device);

		REPORT_ERRORS(hr);

		if (FAILED(hr))
			hr = Direct3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hWnd,
                D3DCREATE_SOFTWARE_VERTEXPROCESSING, &DeviceParams, &Device);

		REPORT_ERRORS(hr);

		hr = D3DXCreateFont(Device, 17, 0, 500, 0, FALSE, 
			DEFAULT_CHARSET, 
			OUT_DEFAULT_PRECIS, 
			CLEARTYPE_NATURAL_QUALITY, 
			DEFAULT_PITCH | FF_DONTCARE, 
			"ExpresswayRg-Regular", &Font);

		REPORT_ERRORS(hr);

		hr = Device->GetRenderTarget(0, &SurfaceBuffer);

		D3DSURFACE_DESC desc;
		hr = SurfaceBuffer->GetDesc(&desc);

		hr = Device->CreateTexture(desc.Width, 
			desc.Height, 
			1, 
			D3DUSAGE_RENDERTARGET, 
			desc.Format, 
			D3DPOOL_DEFAULT, 
			&TextureBuffer, 
			NULL);

		REPORT_ERRORS(hr);

		hr = CreateDisplayCanvas();

		REPORT_ERRORS(hr);

		Root = new Display(Device);

		if (Root == NULL)
			REPORT_ERRORS(hr);

		hr = CompileEffectFromResources(ID_RENDER_SHADER, Effect);

		REPORT_ERRORS(hr);

		Device->SetSamplerState(0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR);
		Device->SetSamplerState(0, D3DSAMP_MIPFILTER, D3DTEXF_LINEAR);
		Device->SetSamplerState(0, D3DSAMP_MINFILTER, D3DTEXF_ANISOTROPIC);

		SAFE_RELEASE(Direct3D);

		return true;
	}

	void BstmDevice::ChooseBestCSAAMode(IDirect3D9* d3d, D3DFORMAT backBufferFmt, D3DFORMAT depthStencilFmt,
							BOOL windowed, D3DMULTISAMPLE_TYPE &type,
							DWORD &qualityLevels)
	{    
		HRESULT hrBackBufferFmt = 0;
		HRESULT hrDepthStencilFmt = 0;
		DWORD qualityLevelsBackBuffer = 0;
		DWORD qualityLevelsDepthStencil = 0;

		D3DMULTISAMPLE_TYPE multiSampleTypes[] =
		{
			D3DMULTISAMPLE_8_SAMPLES,
			D3DMULTISAMPLE_4_SAMPLES
		};

		type = D3DMULTISAMPLE_NONE;
		qualityLevels = 0;

		for (int i = 0; i < 2; ++i)
		{
			hrBackBufferFmt = d3d->CheckDeviceMultiSampleType(
							  D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
							  backBufferFmt, windowed,
							  multiSampleTypes[i],
							  &qualityLevelsBackBuffer);

			hrDepthStencilFmt = d3d->CheckDeviceMultiSampleType(
								D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
								depthStencilFmt, windowed,
								multiSampleTypes[i],
								&qualityLevelsDepthStencil);

			if (SUCCEEDED(hrBackBufferFmt) && SUCCEEDED(hrDepthStencilFmt))
			{
				if (qualityLevelsBackBuffer == qualityLevelsDepthStencil)
				{
					if (multiSampleTypes[i] == D3DMULTISAMPLE_8_SAMPLES)
					{
						if (qualityLevelsBackBuffer > 2)
						{
							type = D3DMULTISAMPLE_8_SAMPLES;
							qualityLevels = 2;
							break;
						}
						else if (qualityLevelsBackBuffer > 0)
						{
							type = D3DMULTISAMPLE_8_SAMPLES;
							qualityLevels = 0;
							break;
						}
					}
					else if (multiSampleTypes[i] == D3DMULTISAMPLE_4_SAMPLES)
					{
						if (qualityLevelsBackBuffer > 4)
						{
							type = D3DMULTISAMPLE_4_SAMPLES;
							qualityLevels = 4;
							break;
						}
						else if (qualityLevelsBackBuffer > 2)
						{
							type = D3DMULTISAMPLE_4_SAMPLES;
							qualityLevels = 2;
							break;
						}
					}
				}
			}
		}
	}

	void BstmDevice::ChooseBestMSAAMode(IDirect3D9* d3d, D3DFORMAT backBufferFmt, D3DFORMAT depthStencilFmt,
							BOOL windowed, D3DMULTISAMPLE_TYPE &type,
							DWORD &qualityLevels, DWORD &samplesPerPixel)
	{
		bool supported = false;

		struct MSAAMode
		{
			D3DMULTISAMPLE_TYPE type;
			DWORD samples;
		}
		multsamplingTypes[15] =
		{
			{ D3DMULTISAMPLE_16_SAMPLES,  16 },
			{ D3DMULTISAMPLE_15_SAMPLES,  15 },
			{ D3DMULTISAMPLE_14_SAMPLES,  14 },
			{ D3DMULTISAMPLE_13_SAMPLES,  13 },
			{ D3DMULTISAMPLE_12_SAMPLES,  12 },
			{ D3DMULTISAMPLE_11_SAMPLES,  11 },
			{ D3DMULTISAMPLE_10_SAMPLES,  10 },
			{ D3DMULTISAMPLE_9_SAMPLES,   9  },
			{ D3DMULTISAMPLE_8_SAMPLES,   8  },
			{ D3DMULTISAMPLE_7_SAMPLES,   7  },
			{ D3DMULTISAMPLE_6_SAMPLES,   6  },
			{ D3DMULTISAMPLE_5_SAMPLES,   5  },
			{ D3DMULTISAMPLE_4_SAMPLES,   4  },
			{ D3DMULTISAMPLE_3_SAMPLES,   3  },
			{ D3DMULTISAMPLE_2_SAMPLES,   2  }
		};

		for (int i = 0; i < 15; ++i)
		{
			type = multsamplingTypes[i].type;

			supported = MSAAModeSupported(d3d, type, backBufferFmt, depthStencilFmt,
							windowed, qualityLevels);

			if (supported)
			{
				samplesPerPixel = multsamplingTypes[i].samples;
				return;
			}
		}

		type = D3DMULTISAMPLE_NONE;
		qualityLevels = 0;
		samplesPerPixel = 1;
	}

	bool BstmDevice::MSAAModeSupported(IDirect3D9* d3d, D3DMULTISAMPLE_TYPE type, D3DFORMAT backBufferFmt,
                       D3DFORMAT depthStencilFmt, BOOL windowed,
                       DWORD &qualityLevels)
	{
		DWORD backBufferQualityLevels = 0;
		DWORD depthStencilQualityLevels = 0;

		HRESULT hr = d3d->CheckDeviceMultiSampleType(D3DADAPTER_DEFAULT,
						D3DDEVTYPE_HAL, backBufferFmt, windowed, type,
						&backBufferQualityLevels);

		if (SUCCEEDED(hr))
		{
			hr = d3d->CheckDeviceMultiSampleType(D3DADAPTER_DEFAULT,
					D3DDEVTYPE_HAL, depthStencilFmt, windowed, type,
					&depthStencilQualityLevels);

			if (SUCCEEDED(hr))
			{
				if (backBufferQualityLevels == depthStencilQualityLevels)
				{
					if (backBufferQualityLevels > 0)
						qualityLevels = backBufferQualityLevels - 1;
					else
						qualityLevels = backBufferQualityLevels;

					return true;
				}
			}
		}

		return false;
	}

	HRESULT BstmDevice::CreateDisplayCanvas()
	{
		HRESULT hr = 0;

		hr = D3DXCreateMeshFVF(2, 4, D3DXMESH_MANAGED, Vertex::FVF, Device, &Canvas);

		Vertex* v = 0;
		hr = Canvas->LockVertexBuffer(0, (void**)&v);

		v[0] = Vertex(Vector3<float>(-1.0f, -1.0f, 0.0f), 0.0f, 1.0f);
		v[1] = Vertex(Vector3<float>(-1.0f,  1.0f, 0.0f), 0.0f, 0.0f);
		v[2] = Vertex(Vector3<float>(1.0f,  1.0f, 0.0f), 1.0f, 0.0f);
		v[3] = Vertex(Vector3<float>(1.0f, -1.0f, 0.0f), 1.0f, 1.0f);

		hr = Canvas->UnlockVertexBuffer();

		WORD* i = 0;
		hr = Canvas->LockIndexBuffer(0, (void**)&i);

		i[0] = 0; i[1] = 1; i[2] = 2;
		i[3] = 0; i[4] = 2; i[5] = 3;

		hr = Canvas->UnlockIndexBuffer();

		std::vector<DWORD> adjacencyBuffer(Canvas->GetNumFaces() * 3);
		hr = Canvas->GenerateAdjacency(0, &adjacencyBuffer[0]);

		return hr;
	}

	bool BstmDevice::ReportInitialisationError(HRESULT hr)
	{
		WRITE_ERROR(this, "Error Initialising Direct3D Device - " << DXGetErrorString(hr));
		WRITELN(DXGetErrorDescription(hr));
		return false;
	}

	bool BstmDevice::ReportInitialisationError(const char* message)
	{
		WRITE_ERROR(this, "Error Initialising Direct3D Device..");
		WRITELN(message);
		return false;
	}

	void* BstmDevice::GetDevicePtr()
	{
		return Device;
	}

	HRESULT BstmDevice::CompileEffectFromResources(int resource, LPD3DXEFFECT &effect)
	{
		HRESULT hr = 0;
		HMODULE instance = reinterpret_cast<HMODULE>(&__ImageBase);

		ID3DXBuffer *pCompilationErrors = 0;
		hr = D3DXCreateEffectFromResource(Device, instance, MAKEINTRESOURCE(resource), 0, 0,
		D3DXSHADER_DEBUG, 0, &effect, &pCompilationErrors);

		if (FAILED(hr))
		{
			if (pCompilationErrors)
			{
				std::string compilationErrors(static_cast<const char *>(pCompilationErrors->GetBufferPointer()));
				WRITE_ERROR(this, compilationErrors.c_str());
			} 
		} 

		SAFE_RELEASE(pCompilationErrors);
		return hr;
	}

	void BstmDevice::Update()
	{
		
	}
	
	void BstmDevice::BeginRender()
	{
		BeginRender(NULL);
	}

	void BstmDevice::BeginRender(IDisplay* display)
	{
		if (Device != NULL)
		{
			Effect->SetFloat("viewportWidth", Width);
			Effect->SetFloat("viewportHeight", Height);

			if (display == NULL)
				display = Root;

			Active = (Display*)display;
			Displays.push_back(Active);

			Device->SetRenderTarget(0, Active->Target);

			Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, Active->ToD3DXColor(Active->Background), 1.0f, 0);
			Device->BeginScene();	

			Device->SetRenderState(D3DRS_FILLMODE, Active->RenderMode);
		}
	}

	void BstmDevice::EndRender()
	{
		if (Device != NULL)
		{
			Device->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);

			if (Active->GetEffectCount() > 0)
				ApplyEffects();

			Device->SetRenderTarget(0, SurfaceBuffer);

			IDirect3DSurface9* Surface = NULL;
			Active->Texture->GetSurfaceLevel(0, &Surface);
			Device->StretchRect(Active->Target, NULL, Surface, NULL, D3DTEXF_NONE);

			SAFE_RELEASE(Surface);

			Device->Clear( 0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f), 1.0f, 0 );
			Device->EndScene();
		}
	}

	void BstmDevice::ApplyEffects()
	{
		IDirect3DSurface9* Surface = NULL;
		TextureBuffer->GetSurfaceLevel(0, &Surface);

		int currentTechnique = 0;
		D3DXHANDLE hTechnique;

		D3DXMATRIX World;
		D3DXMatrixIdentity(&World);
		Device->SetTransform(D3DTS_WORLD, &World);

		for (currentTechnique = 0; currentTechnique < Active->GetEffectCount(); currentTechnique++)
		{
			Device->StretchRect(Active->Target, NULL, Surface, NULL, D3DTEXF_NONE);

			if (currentTechnique == 0)
				Effect->SetTexture("originTexture", TextureBuffer);

			Device->SetRenderTarget(0, Active->Target);
			hTechnique = Effect->GetTechniqueByName(Active->GetEffect(currentTechnique));

			if (hTechnique == NULL)
			{
				WRITE_WARN(Active, "Technique [" << Active->GetEffect(currentTechnique) << "] Not Found");
				Active->RemoveEffect(currentTechnique);
				continue;
			}

			Effect->SetTechnique(hTechnique);
			
			Effect->SetTexture("renderTexture", TextureBuffer);

			UINT Pass, Passes;
			Effect->Begin(&Passes, 0);
			for (Pass = 0; Pass < Passes; Pass++)
			{
				Effect->BeginPass(Pass);
					Canvas->DrawSubset(0);
				Effect->EndPass();
			}

			Effect->End();
		}

		SAFE_RELEASE(Surface);
	}

	void BstmDevice::Present()
	{
		if (Device != NULL)
		{
			Device->BeginScene();

			for(int i = 0; i < Displays.size(); i++)
				DrawDisplay(Displays[i]);

			std::stringstream out;
			out << Kernel->GetFPS() << std::endl;
			DrawString(out.str().c_str(), Vector2<int>(5,5));

			Device->EndScene();

			Device->Present(NULL,NULL,NULL,NULL);
			Displays.clear();
		}
	}

	void BstmDevice::DrawDisplay(Display* display)
	{
		if (Device != NULL)
		{
			D3DXVECTOR4 baseColor = display->ToD3DXVector(display->Filter);

			Effect->SetTexture("renderTexture", display->Texture);
			Effect->SetVector("textureColor", &baseColor);

			D3DXHANDLE hTechnique = Effect->GetTechniqueByName("Default");
			Effect->SetTechnique(hTechnique);

			D3DXMATRIX View;
			D3DXMATRIX Projection;
			D3DXMATRIX World;

			D3DXMATRIX translationMatrix;
			D3DXMATRIX rotationMatrix;
			D3DXMATRIX scaleMatrix;

			D3DXMatrixRotationYawPitchRoll(&rotationMatrix, display->Rotation.Y, display->Rotation.X, display->Rotation.Z);
			D3DXMatrixTranslation(&translationMatrix, display->Position.X,  display->Position.Y,  display->Position.Z);
			D3DXMatrixScaling(&scaleMatrix, display->Scale.X,  display->Scale.Y,  display->Scale.Z);
		
			World = rotationMatrix * translationMatrix * scaleMatrix;

			D3DXVECTOR3 vEyePt = D3DXVECTOR3(0.0f, 0.0f, -2.4f);
			D3DXVECTOR3 vLookatPt = D3DXVECTOR3(0.0f, 0.0f, 0.0f);
			D3DXVECTOR3 vUpVec = D3DXVECTOR3(0.0f, 1.0f, 0.0f);

			D3DXMatrixLookAtLH(&View, &vEyePt, &vLookatPt, &vUpVec);
			D3DXMatrixPerspectiveFovLH(&Projection, D3DX_PI/4.0f, 1.0f, 0.0f, 1000.0f);

			Device->SetTransform(D3DTS_VIEW, &View);
			Device->SetTransform(D3DTS_PROJECTION, &Projection);
			Device->SetTransform(D3DTS_WORLD, &World);

			UINT Pass, Passes;
			Effect->Begin(&Passes, 0);
			for (Pass = 0; Pass < Passes; Pass++)
			{
				Effect->BeginPass(Pass);
					Canvas->DrawSubset(0);
				Effect->EndPass();
			}

			Effect->End();
		}
	}

	Vector3<float> BstmDevice::UnProject(Vector3<float> source, Matrix4<float> view, Matrix4<float> projection)
	{
		D3DVIEWPORT9 Viewport;
		Device->GetViewport(&Viewport);

		D3DXVECTOR3	finalVector;
		D3DXVECTOR3	sourceVector = D3DXVECTOR3(source.X, source.Y, source.Z);

		D3DXMATRIX View = (D3DXMATRIX)(view.Ptr());
		D3DXMATRIX Projection = (D3DXMATRIX)(projection.Ptr());
		D3DXMATRIX World; 
		D3DXMatrixIdentity(&World);

		D3DXVec3Unproject(&finalVector, &sourceVector, &Viewport, &Projection, &View, &World);

		return Vector3<float>(finalVector.x, finalVector.y, finalVector.z);
	}

	void BstmDevice::DrawString(const char* text, Vector2<int> position)
	{
		if (Device != NULL)
		{
			RECT textRect;
			SetRect(&textRect, position.X + 1, position.Y + 1, Width, Height);
			Font->DrawText(NULL, text, -1, &textRect, DT_LEFT, D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f));

			SetRect(&textRect, position.X, position.Y, Width, Height);
			Font->DrawText(NULL, text, -1, &textRect, DT_LEFT, D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f));
		}
	}

	void BstmDevice::Release()
	{
		WRITE_RELEASE(this);

		SAFE_RELEASE(Canvas);
		SAFE_RELEASE(Font);

		for(int i = 0; i < Displays.size(); i++)
			Displays[i]->Release();

		SAFE_RELEASE(SurfaceBuffer);
		SAFE_RELEASE(TextureBuffer);
		SAFE_RELEASE(Effect);
		SAFE_RELEASE(Root);
		SAFE_RELEASE(Active);
		SAFE_RELEASE(Device);
	}
}