#include "gxgraphics.hpp"
#include "gxruntime.hpp"
#include "gxcanvas.hpp"
#include "gxmesh.hpp"
#include "gxfont.hpp"
#include "gxscene.hpp"

#include <stdutil.hpp>

extern gxRuntime* gx_runtime;

gxGraphics::gxGraphics(gxRuntime* rt, IDirectDraw7* dd, IDirectDrawSurface7* fs, IDirectDrawSurface7* bs, bool d3d)
	: runtime(rt), dirDraw(dd), dir3d(0), dir3dDev(0), def_font(0), gfx_lost(false), dummy_mesh(0)
{
	dirDraw->QueryInterface(IID_IDirectDraw, (void**)&ds_dirDraw);

	front_canvas = new gxCanvas(this, fs, 0);
	back_canvas  = new gxCanvas(this, bs, 0);

	front_canvas->cls();
	back_canvas->cls();

	def_font = loadFont("courier", 12, 0);

	front_canvas->setFont(def_font);
	back_canvas->setFont(def_font);

	memset(&primFmt, 0, sizeof(primFmt));
	primFmt.dwSize = sizeof(primFmt);
	fs->GetPixelFormat(&primFmt);

	//are we fullscreen?
	_gamma = 0;
	if (fs != bs) {
		if (fs->QueryInterface(IID_IDirectDrawGammaControl, (void**)&_gamma) >= 0) {
			if (_gamma->GetGammaRamp(0, &_gammaRamp) < 0)
				_gamma = 0;
		}
	}
	if (!_gamma) {
		for (int k = 0; k < 256; ++k)
			_gammaRamp.red[k] = _gammaRamp.blue[k] = _gammaRamp.green[k] = k;
	}
}

gxGraphics::~gxGraphics()
{
	if (_gamma)
		_gamma->Release();
	while (scene_set.size())
		freeScene(*scene_set.begin());
	while (movie_set.size())
		closeMovie(*movie_set.begin());
	while (font_set.size())
		freeFont(*font_set.begin());
	while (canvas_set.size())
		freeCanvas(*canvas_set.begin());

	std::set<std::string>::iterator it;
	for (it = font_res.begin(); it != font_res.end(); ++it)
		RemoveFontResource((*it).c_str());
	font_res.clear();

	delete back_canvas;
	delete front_canvas;

	ds_dirDraw->Release();

	dirDraw->RestoreDisplayMode();
	dirDraw->Release();
}

void gxGraphics::setGamma(int r, int g, int b, float dr, float dg, float db)
{
	_gammaRamp.red[r & 255]   = dr * 257.0f;
	_gammaRamp.green[g & 255] = dg * 257.0f;
	_gammaRamp.blue[b & 255]  = db * 257.0f;
}

void gxGraphics::updateGamma(bool calibrate)
{
	if (!_gamma)
		return;
	_gamma->SetGammaRamp(calibrate ? DDSGR_CALIBRATE : 0, &_gammaRamp);
}

void gxGraphics::getGamma(int r, int g, int b, float* dr, float* dg, float* db)
{
	*dr = _gammaRamp.red[r & 255] / 257.0f;
	*dg = _gammaRamp.green[g & 255] / 257.0f;
	*db = _gammaRamp.blue[b & 255] / 257.0f;
}

void gxGraphics::backup() {}

bool gxGraphics::restore()
{
	while (dirDraw->TestCooperativeLevel() != DD_OK) {
		if (dirDraw->TestCooperativeLevel() == DDERR_WRONGMODE)
			return false;

		Sleep(100);
	}

	if (back_canvas->getSurface()->IsLost() == DD_OK)
		return true;

	dirDraw->RestoreAllSurfaces();

	//restore all canvases
	std::set<gxCanvas*>::iterator it;
	for (it = canvas_set.begin(); it != canvas_set.end(); ++it) {
		(*it)->restore();
	}

	//restore all meshes (b3d surfaces)
	std::set<gxMesh*>::iterator mesh_it;
	for (mesh_it = mesh_set.begin(); mesh_it != mesh_set.end(); ++mesh_it) {
		(*mesh_it)->restore();
	}
	if (dir3d)
		dir3d->EvictManagedTextures();

	return true;
}

gxCanvas* gxGraphics::getFrontCanvas() const
{
	return front_canvas;
}

gxCanvas* gxGraphics::getBackCanvas() const
{
	return back_canvas;
}

gxFont* gxGraphics::getDefaultFont() const
{
	return def_font;
}

void gxGraphics::vwait()
{
	dirDraw->WaitForVerticalBlank(DDWAITVB_BLOCKBEGIN, 0);
}

void gxGraphics::flip(bool v)
{
	runtime->flip(v);
}

void gxGraphics::copy(gxCanvas* dest, int dx, int dy, int dw, int dh, gxCanvas* src, int sx, int sy, int sw, int sh)
{
	RECT r = {dx, dy, dx + dw, dy + dh};
	ddUtil::copy(dest->getSurface(), dx, dy, dw, dh, src->getSurface(), sx, sy, sw, sh);
	dest->damage(r);
}

int gxGraphics::getScanLine() const
{
	DWORD t = 0;
	dirDraw->GetScanLine(&t);
	return t;
}

int gxGraphics::getTotalVidmem() const
{
	DDCAPS caps = {sizeof(caps)};
	dirDraw->GetCaps(&caps, 0);
	return caps.dwVidMemTotal;
}

int gxGraphics::getAvailVidmem() const
{
	DDCAPS caps = {sizeof(caps)};
	dirDraw->GetCaps(&caps, 0);
	return caps.dwVidMemFree;
}

gxMovie* gxGraphics::openMovie(const std::string& file, int flags)
{
	/*IAMMultiMediaStream *iam_stream;

	if (CoCreateInstance(
		CLSID_AMMultiMediaStream, NULL, CLSCTX_INPROC_SERVER,
		IID_IAMMultiMediaStream, (void **)&iam_stream) == S_OK) {

		if (iam_stream->Initialize(STREAMTYPE_READ, AMMSF_NOGRAPHTHREAD, NULL) == S_OK) {

			if (iam_stream->AddMediaStream(ds_dirDraw, &MSPID_PrimaryVideo, 0, NULL) == S_OK) {

				iam_stream->AddMediaStream(NULL, &MSPID_PrimaryAudio, AMMSF_ADDDEFAULTRENDERER, NULL);

				WCHAR *path = new WCHAR[file.size() + 1];
				MultiByteToWideChar(CP_ACP, 0, file.c_str(), -1, path, sizeof(WCHAR)*(file.size() + 1));
				int n = iam_stream->OpenFile(path, 0);
				delete path;

				if (n == S_OK) {
					gxMovie *movie = new gxMovie(this, iam_stream);
					movie_set.insert(movie);
					return movie;
				}
			}
		}
		iam_stream->Release();
	}*/
	return 0;
}

gxMovie* gxGraphics::verifyMovie(gxMovie* m)
{
	return movie_set.count(m) ? m : 0;
}

void gxGraphics::closeMovie(gxMovie* m)
{
	if (movie_set.erase(m))
		delete m;
}

gxCanvas* gxGraphics::createCanvas(int w, int h, int flags)
{
	IDirectDrawSurface7* s = ddUtil::createSurface(w, h, flags, this);
	if (!s)
		return 0;
	gxCanvas* c = new gxCanvas(this, s, flags);
	canvas_set.insert(c);
	c->cls();
	return c;
}

gxCanvas* gxGraphics::loadCanvas(const std::string& f, int flags)
{
	IDirectDrawSurface7* s = ddUtil::loadSurface(f, flags, this);
	if (!s)
		return 0;
	gxCanvas* c = new gxCanvas(this, s, flags);
	canvas_set.insert(c);
	return c;
}

gxCanvas* gxGraphics::verifyCanvas(gxCanvas* c)
{
	return canvas_set.count(c) || c == front_canvas || c == back_canvas ? c : 0;
}

void gxGraphics::freeCanvas(gxCanvas* c)
{
	if (canvas_set.erase(c))
		delete c;
}

int gxGraphics::getWidth() const
{
	return front_canvas->getWidth();
}

int gxGraphics::getHeight() const
{
	return front_canvas->getHeight();
}

int gxGraphics::getDepth() const
{
	return front_canvas->getDepth();
}

gxFont* gxGraphics::loadFont(const std::string& f, int height, int flags)
{
	int bold      = flags & gxFont::FONT_BOLD ? FW_BOLD : FW_REGULAR;
	int italic    = flags & gxFont::FONT_ITALIC ? 1 : 0;
	int underline = flags & gxFont::FONT_UNDERLINE ? 1 : 0;
	int strikeout = 0;

	std::string t;
	int    n = f.find('.');
	if (n != std::string::npos) {
		t = fullfilename(f);
		if (!font_res.count(t) && AddFontResource(t.c_str()))
			font_res.insert(t);
		t = filenamefile(f.substr(0, n));
	} else {
		t = f;
	}

	//save and turn off font smoothing....
	BOOL smoothing = FALSE;
	SystemParametersInfo(SPI_GETFONTSMOOTHING, 0, &smoothing, 0);
	SystemParametersInfo(SPI_SETFONTSMOOTHING, FALSE, 0, 0);

	HFONT hfont = CreateFont(height, 0, 0, 0, bold, italic, underline, strikeout, ANSI_CHARSET, OUT_DEFAULT_PRECIS,
							 CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, t.c_str());

	if (!hfont) {
		//restore font smoothing
		SystemParametersInfo(SPI_SETFONTSMOOTHING, smoothing, 0, 0);
		return 0;
	}

	HDC   hdc   = CreateCompatibleDC(0);
	HFONT pfont = (HFONT)SelectObject(hdc, hfont);

	TEXTMETRIC tm = {0};
	if (!GetTextMetrics(hdc, &tm)) {
		SelectObject(hdc, pfont);
		DeleteDC(hdc);
		DeleteObject(hfont);
		SystemParametersInfo(SPI_SETFONTSMOOTHING, smoothing, 0, 0);
		return 0;
	}
	height = tm.tmHeight;

	int  first = tm.tmFirstChar, last = tm.tmLastChar;
	int  sz     = last - first + 1;
	int* offs   = new int[sz];
	int* widths = new int[sz];
	int* as     = new int[sz];

	//calc size of canvas to hold font.
	int x = 0, y = 0, max_x = 0;
	for (int k = 0; k < sz; ++k) {
		char t = k + first;

		SIZE sz;
		GetTextExtentPoint32(hdc, &t, 1, &sz);
		int w = sz.cx;

		as[k] = 0;

		ABC abc;
		if (GetCharABCWidths(hdc, t, t, &abc)) {
			if (abc.abcA < 0) {
				as[k] = ceil(-abc.abcA);
				w += as[k];
			}
			if (abc.abcC < 0)
				w += (int)ceil(-abc.abcC);
		}

		if (x && x + w > getWidth()) {
			x = 0;
			y += height;
		}
		offs[k]   = (x << 16) | y;
		widths[k] = w;
		x += w;
		if (x > max_x)
			max_x = x;
	}
	SelectObject(hdc, pfont);
	DeleteDC(hdc);

	int cw = max_x, ch = y + height;

	if (gxCanvas* c = createCanvas(cw, ch, 0)) {
		IDirectDrawSurface7* surf = c->getSurface();

		HDC surf_hdc;
		if (surf->GetDC(&surf_hdc) >= 0) {
			HFONT pfont = (HFONT)SelectObject(surf_hdc, hfont);

			SetBkColor(surf_hdc, 0x000000);
			SetTextColor(surf_hdc, 0xffffff);

			for (int k = 0; k < sz; ++k) {
				int  x = (offs[k] >> 16) & 0xffff, y = offs[k] & 0xffff;
				char t    = k + first;
				RECT rect = {x, y, x + widths[k], y + height};
				ExtTextOut(surf_hdc, x + as[k], y, ETO_CLIPPED, &rect, &t, 1, 0);
			}

			SelectObject(surf_hdc, pfont);
			surf->ReleaseDC(surf_hdc);
			DeleteObject(hfont);
			delete[] as;

			c->backup();
			gxFont* font =
				new gxFont(this, c, tm.tmMaxCharWidth, height, first, last + 1, tm.tmDefaultChar, offs, widths);
			font_set.insert(font);

			//restore font smoothing
			SystemParametersInfo(SPI_SETFONTSMOOTHING, smoothing, 0, 0);
			return font;
		} else {
		}
		freeCanvas(c);
	} else {
	}
	DeleteObject(hfont);
	delete[] as;
	delete[] widths;
	delete[] offs;

	//restore font smoothing
	SystemParametersInfo(SPI_SETFONTSMOOTHING, smoothing, 0, 0);
	return 0;
}

gxFont* gxGraphics::verifyFont(gxFont* f)
{
	return font_set.count(f) ? f : 0;
}

void gxGraphics::freeFont(gxFont* f)
{
	if (font_set.erase(f))
		delete f;
}

//////////////
// 3D STUFF //
//////////////

static int maxDevType;

static HRESULT CALLBACK enumDevice(char* desc, char* name, D3DDEVICEDESC7* devDesc, void* context)
{
	gxGraphics* g    = (gxGraphics*)context;
	int         t    = 0;
	GUID        guid = devDesc->deviceGUID;
	if (guid == IID_IDirect3DRGBDevice)
		t = 1;
	else if (guid == IID_IDirect3DHALDevice)
		t = 2;
	else if (guid == IID_IDirect3DTnLHalDevice)
		t = 3;
	if (t > maxDevType) {
		g->dir3dDevDesc = *devDesc;
		maxDevType      = t;
	}
	return D3DENUMRET_OK;
}

static HRESULT CALLBACK enumZbuffFormat(LPDDPIXELFORMAT format, void* context)
{
	gxGraphics* g = (gxGraphics*)context;
	if (format->dwZBufferBitDepth == g->primFmt.dwRGBBitCount) {
		g->zbuffFmt = *format;
		return D3DENUMRET_CANCEL;
	}
	if (format->dwZBufferBitDepth > g->zbuffFmt.dwZBufferBitDepth) {
		if (format->dwZBufferBitDepth < g->primFmt.dwRGBBitCount) {
			g->zbuffFmt = *format;
		}
	}
	return D3DENUMRET_OK;
}

struct TexFmt {
	DDPIXELFORMAT fmt;
	int           bits, a_bits, rgb_bits;
};

static int cntBits(int mask)
{
	int n = 0;
	for (int k = 0; k < 32; ++k) {
		if (mask & (1 << k))
			++n;
	}
	return n;
}

static std::vector<TexFmt> tex_fmts;

static HRESULT CALLBACK enumTextureFormat(DDPIXELFORMAT* fmt, void* p)
{
	TexFmt t;
	t.fmt      = *fmt;
	t.bits     = fmt->dwRGBBitCount;
	t.a_bits   = (fmt->dwFlags & DDPF_ALPHAPIXELS) ? cntBits(fmt->dwRGBAlphaBitMask) : 0;
	t.rgb_bits = (fmt->dwFlags & DDPF_RGB) ? cntBits(fmt->dwRBitMask | fmt->dwGBitMask | fmt->dwBBitMask) : 0;

	tex_fmts.push_back(t);

	return D3DENUMRET_OK;
}

static std::string itobin(int n)
{
	std::string t;
	for (int k = 0; k < 32; n <<= 1, ++k) {
		t += (n & 0x80000000) ? '1' : '0';
	}
	return t;
}

static void debugPF(const DDPIXELFORMAT& pf)
{
	std::string t;
	t = "Bits:" + itoa(pf.dwRGBBitCount);
	gx_runtime->debugLog(t.c_str());
	t = "R Mask:" + itobin(pf.dwRBitMask);
	gx_runtime->debugLog(t.c_str());
	t = "G Mask:" + itobin(pf.dwGBitMask);
	gx_runtime->debugLog(t.c_str());
	t = "B Mask:" + itobin(pf.dwBBitMask);
	gx_runtime->debugLog(t.c_str());
	t = "A Mask:" + itobin(pf.dwRGBAlphaBitMask);
	gx_runtime->debugLog(t.c_str());
}

static void pickTexFmts(gxGraphics* g, int hi)
{
	//texRGBFmt.
	{
		int pick = -1, max = 0, bits;
		for (int d = g->primFmt.dwRGBBitCount; d <= 32; d += 8) {
			for (int k = 0; k < tex_fmts.size(); ++k) {
				const TexFmt& t = tex_fmts[k];
				if (t.bits > d || !t.rgb_bits || t.rgb_bits < max)
					continue;
				if (t.rgb_bits == max && t.bits >= bits)
					continue;
				pick = k;
				max  = t.rgb_bits;
				bits = t.bits;
			}
			if (!hi && pick >= 0)
				break;
		}
		if (pick < 0)
			g->texRGBFmt[hi] = g->primFmt;
		else
			g->texRGBFmt[hi] = tex_fmts[pick].fmt;
	}
	//texAlphaFmt
	{
		int pick = -1, max = 0, bits;
		for (int d = g->primFmt.dwRGBBitCount; d <= 32; d += 8) {
			for (int k = 0; k < tex_fmts.size(); ++k) {
				const TexFmt& t = tex_fmts[k];
				if (t.bits > d || !t.a_bits || t.a_bits < max)
					continue;
				if (t.a_bits == max && t.bits >= bits)
					continue;
				pick = k;
				max  = t.a_bits;
				bits = t.bits;
			}
			if (!hi && pick >= 0)
				break;
		}
		if (pick < 0)
			g->texAlphaFmt[hi] = g->primFmt;
		else
			g->texAlphaFmt[hi] = tex_fmts[pick].fmt;
	}
	//texRGBAlphaFmt
	{
		int pick = -1, a8rgb8 = -1, max = 0, bits;
		for (int d = g->primFmt.dwRGBBitCount; d <= 32; d += 8) {
			for (int k = 0; k < tex_fmts.size(); ++k) {
				const TexFmt& t = tex_fmts[k];
				if (t.a_bits == 8 && t.bits == 16) {
					a8rgb8 = k;
					continue;
				}
				if (t.bits > d || !t.a_bits || !t.rgb_bits || t.a_bits < max)
					continue;
				if (t.a_bits == max && t.bits >= bits)
					continue;
				pick = k;
				max  = t.a_bits;
				bits = t.bits;
			}
			if (!hi && pick >= 0)
				break;
		}
		if (pick < 0)
			pick = a8rgb8;
		if (pick < 0)
			g->texRGBAlphaFmt[hi] = g->primFmt;
		else
			g->texRGBAlphaFmt[hi] = tex_fmts[pick].fmt;
	}
	//texRGBMaskFmt...
	{
		int pick = -1, max = 0, bits;
		for (int d = g->primFmt.dwRGBBitCount; d <= 32; d += 8) {
			for (int k = 0; k < tex_fmts.size(); ++k) {
				const TexFmt& t = tex_fmts[k];
				if (!t.a_bits || !t.rgb_bits || t.rgb_bits < max)
					continue;
				if (t.rgb_bits == max && t.bits >= bits)
					continue;
				pick = k;
				max  = t.rgb_bits;
				bits = t.bits;
			}
			if (!hi && pick >= 0)
				break;
		}
		if (pick < 0)
			g->texRGBMaskFmt[hi] = g->primFmt;
		else
			g->texRGBMaskFmt[hi] = tex_fmts[pick].fmt;
	}
}

gxScene* gxGraphics::createScene(int flags)
{
	if (scene_set.size())
		return 0;

	//get d3d
	if (dirDraw->QueryInterface(IID_IDirect3D7, (void**)&dir3d) >= 0) {
		//enum devices
		maxDevType = 0;
		if (dir3d->EnumDevices(enumDevice, this) >= 0 && maxDevType > 1) {
			//enum zbuffer formats
			zbuffFmt.dwZBufferBitDepth = 0;
			if (dir3d->EnumZBufferFormats(dir3dDevDesc.deviceGUID, enumZbuffFormat, this) >= 0) {
				//create zbuff for back buffer
				if (back_canvas->attachZBuffer()) {
					//create 3d device
					if (dir3d->CreateDevice(dir3dDevDesc.deviceGUID, back_canvas->getSurface(), &dir3dDev) >= 0) {
						//enum texture formats
						tex_fmts.clear();
						if (dir3dDev->EnumTextureFormats(enumTextureFormat, this) >= 0) {
							pickTexFmts(this, 0);
							pickTexFmts(this, 1);
							tex_fmts.clear();
#ifdef BETA
							gx_runtime->debugLog("Texture RGB format:");
							debugPF(texRGBFmt);
							gx_runtime->debugLog("Texture Alpha format:");
							debugPF(texAlphaFmt);
							gx_runtime->debugLog("Texture RGB Alpha format:");
							debugPF(texRGBAlphaFmt);
							gx_runtime->debugLog("Texture RGB Mask format:");
							debugPF(texRGBMaskFmt);
							gx_runtime->debugLog("Texture Primary format:");
							debugPF(primFmt);
							string ts = "ZBuffer Bit Depth:" + itoa(zbuffFmt.dwZBufferBitDepth);
							gx_runtime->debugLog(ts.c_str());
#endif
							gxScene* scene = new gxScene(this, back_canvas);
							scene_set.insert(scene);

							dummy_mesh = createMesh(8, 12, 0);

							return scene;
						}
						dir3dDev->Release();
						dir3dDev = 0;
					}
					back_canvas->releaseZBuffer();
				}
			}
		}
		dir3d->Release();
		dir3d = 0;
	}
	return 0;
}

gxScene* gxGraphics::verifyScene(gxScene* s)
{
	return scene_set.count(s) ? s : 0;
}

void gxGraphics::freeScene(gxScene* scene)
{
	if (!scene_set.erase(scene))
		return;
	dummy_mesh = 0;
	while (mesh_set.size())
		freeMesh(*mesh_set.begin());
	back_canvas->releaseZBuffer();
	if (dir3dDev) {
		dir3dDev->Release();
		dir3dDev = 0;
	}
	if (dir3d) {
		dir3d->Release();
		dir3d = 0;
	}
	delete scene;
}

gxMesh* gxGraphics::createMesh(int max_verts, int max_tris, int flags)
{
	static const int VTXFMT =
		D3DFVF_XYZ | D3DFVF_NORMAL | D3DFVF_DIFFUSE | D3DFVF_TEX2 | D3DFVF_TEXCOORDSIZE2(0) | D3DFVF_TEXCOORDSIZE2(1);

	int vbflags = 0;

	//XP or less?
	/*if (runtime->osinfo.dwMajorVersion < 6) {
		vbflags |= D3DVBCAPS_WRITEONLY;
	}*/

	D3DVERTEXBUFFERDESC desc = {sizeof(desc), vbflags, VTXFMT, max_verts};

	IDirect3DVertexBuffer7* buff;
	if (dir3d->CreateVertexBuffer(&desc, &buff, 0) < 0)
		return 0;
	WORD*   indices = new WORD[max_tris * 3];
	gxMesh* mesh    = new gxMesh(this, buff, indices, max_verts, max_tris);
	mesh_set.insert(mesh);
	return mesh;
}

gxMesh* gxGraphics::verifyMesh(gxMesh* m)
{
	return mesh_set.count(m) ? m : 0;
}

void gxGraphics::freeMesh(gxMesh* mesh)
{
	if (mesh_set.erase(mesh))
		delete mesh;
}