// Copyright 2007-2023 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>.

// Needs to be included before GL/gl.h
#include "lib.h"
// Needs to be included before glext.h
#include <GL/gl.h>

#include <ctime>

#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 = nullptr

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) {                                                           \
			o##name = reinterpret_cast< t##name >(GetProcAddress(handle, #name)); \
			if (!o##name) {                                                       \
				ods("OpenGL: Could not resolve symbol %s in %s", #name, #handle); \
				return false;                                                     \
			}                                                                     \
		}                                                                         \
	}

	if (!hGL) {
		return false;
	}

	HMODULE hGDI = GetModuleHandle("GDI32.DLL");
	if (!hGDI) {
		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(nullptr, 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 = nullptr;
			GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, reinterpret_cast< LPCTSTR >(&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;
}
