// Copyright 2005-2017 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.

#include "lib.h"
#include <time.h>
#include <GL/gl.h>
#include "../3rdparty/GL/glext.h"

#define TDEF(ret, name, arg) typedef ret (__stdcall * t##name) arg
#define GLDEF(ret, name, arg) TDEF(ret, name, arg); t##name o##name = NULL

GLDEF(HGLRC, wglCreateContext, (HDC));
GLDEF(void, glGenTextures, (GLsizei, GLuint *));
GLDEF(void, glDeleteTextures, (GLsizei, GLuint *));
GLDEF(void, glEnable, (GLenum));
GLDEF(void, glDisable, (GLenum));
GLDEF(void, glBlendFunc, (GLenum, GLenum));
GLDEF(void, glViewport, (GLint, GLint, GLsizei, GLsizei));
GLDEF(void, glMatrixMode, (GLenum));
GLDEF(void, glLoadIdentity, (void));
GLDEF(void, glOrtho, (GLdouble, GLdouble, GLdouble, GLdouble, GLdouble, GLdouble));
GLDEF(void, glBindTexture, (GLenum, GLuint));
GLDEF(void, glPushMatrix, (void));
GLDEF(void, glBegin, (GLenum));
GLDEF(void, glEnd, (void));
GLDEF(void, glTexCoord2f, (GLfloat, GLfloat));
GLDEF(void, glVertex2f, (GLfloat, GLfloat));
GLDEF(void, glPopMatrix, (void));
GLDEF(void, glTexParameteri, (GLenum, GLenum, GLint));
GLDEF(void, glTexEnvi, (GLenum, GLenum, GLint));
GLDEF(void, glTexImage2D, (GLenum, GLint, GLint, GLsizei, GLsizei, GLint, GLenum, GLenum, const GLvoid *));
GLDEF(void, glTexSubImage2D, (GLenum, GLint, GLint, GLint, GLsizei, GLsizei, GLenum, GLenum, const GLvoid *));
GLDEF(void, glPixelStorei, (GLenum, GLint));
GLDEF(void, wglMakeCurrent, (HDC, HGLRC));
GLDEF(HGLRC, wglGetCurrentContext, (void));
GLDEF(HDC, wglGetCurrentDC, (void));
GLDEF(int, GetDeviceCaps, (HDC, int));

#define INJDEF(ret, name, arg) GLDEF(ret, name, arg); static HardHook hh##name

INJDEF(BOOL, wglSwapBuffers, (HDC));

static bool bHooked = false;

class Context : protected Pipe {
	public:
		Context(HDC hdc);
		void draw(HDC hdc);

	protected:
		virtual void blit(unsigned int x, unsigned int y, unsigned int w, unsigned int h);
		virtual void setRect();
		virtual void newTexture(unsigned int width, unsigned int height);

	private:
		HGLRC ctx;
		GLuint texture;

		clock_t timeT;
		unsigned int frameCount;

		void initContext();
		void doDraw(HDC hdc);
};

Context::Context(HDC hdc) {
	timeT = clock();
	frameCount = 0;

	texture = ~0;
	ctx = owglCreateContext(hdc);

	HGLRC oldctx = owglGetCurrentContext();
	HDC oldhdc = owglGetCurrentDC();
	owglMakeCurrent(hdc, ctx);

	initContext();

	owglMakeCurrent(oldhdc, oldctx);
}

void Context::initContext() {
	// Here we go. From the top. Where is glResetState?
	oglDisable(GL_ALPHA_TEST);
	oglDisable(GL_AUTO_NORMAL);
	oglEnable(GL_BLEND);
	// Skip clip planes, there are thousands of them.
	oglDisable(GL_COLOR_LOGIC_OP);
	oglDisable(GL_COLOR_MATERIAL);
	oglDisable(GL_COLOR_TABLE);
	oglDisable(GL_CONVOLUTION_1D);
	oglDisable(GL_CONVOLUTION_2D);
	oglDisable(GL_CULL_FACE);
	oglDisable(GL_DEPTH_TEST);
	oglDisable(GL_DITHER);
	oglDisable(GL_FOG);
	oglDisable(GL_HISTOGRAM);
	oglDisable(GL_INDEX_LOGIC_OP);
	oglDisable(GL_LIGHTING);
	// Skip line smmooth
	// Skip map
	oglDisable(GL_MINMAX);
	// Skip polygon offset
	oglDisable(GL_SEPARABLE_2D);
	oglDisable(GL_SCISSOR_TEST);
	oglDisable(GL_STENCIL_TEST);
	oglEnable(GL_TEXTURE_2D);
	oglDisable(GL_TEXTURE_GEN_Q);
	oglDisable(GL_TEXTURE_GEN_R);
	oglDisable(GL_TEXTURE_GEN_S);
	oglDisable(GL_TEXTURE_GEN_T);

	oglBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
}

void Context::blit(unsigned int x, unsigned int y, unsigned int w, unsigned int h) {
	ods("OpenGL: Blit %d %d %d %d -- %d %d : %d", x, y, w, h, uiWidth, uiHeight, texture);

	if (texture == ~0)
		return;

	oglBindTexture(GL_TEXTURE_2D, texture);

	if ((x == 0) && (y == 0) && (w == uiWidth) && (h == uiHeight)) {
		oglTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, uiWidth, uiHeight, 0, GL_BGRA, GL_UNSIGNED_BYTE, a_ucTexture);
	} else {
		if (w != uiWidth)
			oglPixelStorei(GL_UNPACK_ROW_LENGTH, uiWidth);
		oglTexSubImage2D(GL_TEXTURE_2D, 0, x, y, w, h, GL_BGRA, GL_UNSIGNED_BYTE, a_ucTexture + 4 * (y * uiWidth + x));
		if (w != uiWidth)
			oglPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
	}
}

void Context::setRect() {
	ods("OpenGL: setRect");
}

void Context::newTexture(unsigned int width, unsigned int height) {
	ods("OpenGL: newTex");

	if (texture == ~0) {
		oglBindTexture(GL_TEXTURE_2D, 0);
		oglDeleteTextures(1, &texture);
		texture = ~0;
	}
	oglGenTextures(1, &texture);

	oglBindTexture(GL_TEXTURE_2D, texture);
	oglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	oglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	oglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	oglTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

	unsigned char *ptr = new unsigned char[width*height*4];
	memset(ptr, 0, width*height*4);
	oglTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, ptr);
	delete [] ptr;
}

void Context::draw(HDC hdc) {
	HGLRC oldctx = owglGetCurrentContext();
	HDC oldhdc = owglGetCurrentDC();
	owglMakeCurrent(hdc, ctx);

	doDraw(hdc);

	owglMakeCurrent(oldhdc, oldctx);
}

void Context::doDraw(HDC hdc) {
	// DEBUG
	//sm->bDebug = true;

	clock_t t = clock();
	float elapsed = static_cast<float>(t - timeT) / CLOCKS_PER_SEC;
	++frameCount;
	if (elapsed > OVERLAY_FPS_INTERVAL) {
		OverlayMsg om;
		om.omh.uiMagic = OVERLAY_MAGIC_NUMBER;
		om.omh.uiType = OVERLAY_MSGTYPE_FPS;
		om.omh.iLength = sizeof(OverlayMsgFps);
		om.omf.fps = frameCount / elapsed;

		sendMessage(om);

		frameCount = 0;
		timeT = t;
	}

	unsigned int width, height;

	width = oGetDeviceCaps(hdc, HORZRES);
	height = oGetDeviceCaps(hdc, VERTRES);

	HWND hwnd = WindowFromDC(hdc);
	if (hwnd) {
		RECT r;
		if (GetClientRect(hwnd, &r)) {
			width = r.right - r.left;
			height = r.bottom - r.top;
		}
	}

	ods("OpenGL: DrawStart: Screen is %d x %d", width, height);

	checkMessage(width, height);

	oglViewport(0, 0, width, height);

	oglMatrixMode(GL_PROJECTION);
	oglLoadIdentity();
	oglOrtho(0, width, height, 0, -100.0, 100.0);

	oglMatrixMode(GL_MODELVIEW);

	oglBindTexture(GL_TEXTURE_2D, texture);
	oglPushMatrix();
	oglLoadIdentity();

	float w = static_cast<float>(uiWidth);
	float h = static_cast<float>(uiHeight);

	float left   = static_cast<float>(uiLeft);
	float top    = static_cast<float>(uiTop);
	float right  = static_cast<float>(uiRight);
	float bottom = static_cast<float>(uiBottom);

	float xm = (left) / w;
	float ym = (top) / h;
	float xmx = (right) / w;
	float ymx = (bottom) / h;

	oglTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
	oglBegin(GL_QUADS);

	oglTexCoord2f(xm, ymx);
	oglVertex2f(left, bottom);

	oglTexCoord2f(xm, ym);
	oglVertex2f(left, top);

	oglTexCoord2f(xmx, ym);
	oglVertex2f(right, top);

	oglTexCoord2f(xmx, ymx);
	oglVertex2f(right, bottom);
	oglEnd();

	oglPopMatrix();

}

static map<HDC, Context *> contexts;

static void doSwap(HDC hdc) {
	Context *c = contexts[hdc];

	if (!c) {
		ods("OpenGL: New context for device %p", hdc);
		c = new Context(hdc);
		contexts[hdc] = c;
	} else {
		ods("OpenGL: Reusing old context");
	}
	c->draw(hdc);
}

static BOOL __stdcall mywglSwapBuffers(HDC hdc) {
	ods("OpenGL: wglSwapBuffers");
	doSwap(hdc);

	hhwglSwapBuffers.restore();
	BOOL ret=owglSwapBuffers(hdc);
	hhwglSwapBuffers.inject();

	return ret;
}

/// Ensure that all the symbols that the OpenGL overlay requires have been
/// looked up.
/// @return true if all symbols have been looked up and are available.
///         Otherwise false.
static bool lookupSymbols(HMODULE hGL) {
#define FNFIND(handle, name) {\
	if (o##name == NULL) {\
		o##name = reinterpret_cast<t##name>(GetProcAddress(handle, #name));\
		if (o##name == NULL) {\
			ods("OpenGL: Could not resolve symbol %s in %s", #name, #handle);\
			return false;\
		}\
	}\
}

	if (hGL == NULL) {
		return false;
	}

	HMODULE hGDI = GetModuleHandle("GDI32.DLL");
	if (hGDI == NULL) {
		ods("OpenGL: Failed to identify GDI32");
		return false;
	}

	// Lookup OpenGL32.DLL symbols
	FNFIND(hGL, wglCreateContext);
	FNFIND(hGL, glGenTextures);
	FNFIND(hGL, glDeleteTextures);
	FNFIND(hGL, glEnable);
	FNFIND(hGL, glDisable);
	FNFIND(hGL, glBlendFunc);
	FNFIND(hGL, glViewport);
	FNFIND(hGL, glMatrixMode);
	FNFIND(hGL, glLoadIdentity);
	FNFIND(hGL, glOrtho);
	FNFIND(hGL, glBindTexture);
	FNFIND(hGL, glPushMatrix);
	FNFIND(hGL, glBegin);
	FNFIND(hGL, glEnd);
	FNFIND(hGL, glTexCoord2f);
	FNFIND(hGL, glVertex2f);
	FNFIND(hGL, glPopMatrix);
	FNFIND(hGL, glTexParameteri);
	FNFIND(hGL, glTexEnvi);
	FNFIND(hGL, glTexImage2D);
	FNFIND(hGL, glTexSubImage2D);
	FNFIND(hGL, glPixelStorei);
	FNFIND(hGL, wglMakeCurrent);
	FNFIND(hGL, wglGetCurrentContext);
	FNFIND(hGL, wglGetCurrentDC);

	// Lookup GDI32.DLL symbols
	FNFIND(hGDI, GetDeviceCaps);

	return true;
}

void checkOpenGLHook() {
	static bool bCheckHookActive = false;
	if (bCheckHookActive) {
		ods("OpenGL: Recursion in checkOpenGLHook");
		return;
	}

	bCheckHookActive = true;

	if (!bHooked) {
		HMODULE hGL = GetModuleHandle("OpenGL32.DLL");

		if (lookupSymbols(hGL)) {
			char procname[1024];
			GetModuleFileName(NULL, procname, 1024);
			ods("OpenGL: Hooking into OpenGL App %s", procname);

			// Add a ref to ourselves; we do NOT want to get unloaded directly from this process.
			HMODULE hTempSelf = NULL;
			GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, reinterpret_cast<char *>(&checkOpenGLHook), &hTempSelf);

#define INJECT(handle, name) {\
	o##name = reinterpret_cast<t##name>(GetProcAddress(handle, #name));\
	if (o##name) {\
		hh##name.setup(reinterpret_cast<voidFunc>(o##name), reinterpret_cast<voidFunc>(my##name));\
		o##name = (t##name) hh##name.call;\
	} else {\
		ods("OpenGL: Could not resolve symbol %s in %s", #name, #handle);\
	}\
}
			INJECT(hGL, wglSwapBuffers);
		}
	} else {
		hhwglSwapBuffers.check();
	}

	bCheckHookActive = false;
}
