// This file was copied from the bsnes project.

// This is the license info, from ruby.hpp:

/*
  ruby
  version: 0.08 (2011-11-25)
  license: public domain
 */

#ifndef __NO_OPENGL

#include "OpenGL.h"
#include <SDL.h>
#include <yaml-cpp/yaml.h>
#include <fstream>

#include "Logger.h"
#include "Surface.h"

namespace OpenXcom
{

bool OpenGL::checkErrors = true;

std::string strGLError(GLenum glErr)
{
	std::string err;

	switch(glErr)
	{
	case GL_INVALID_ENUM:
		err = "GL_INVALID_ENUM";
		break;
	case GL_INVALID_VALUE:
		err = "GL_INVALID_VALUE";
		break;
	case GL_INVALID_OPERATION:
		err = "GL_INVALID_OPERATION";
		break;
	case GL_STACK_OVERFLOW:
		err = "GL_STACK_OVERFLOW";
		break;
	case GL_STACK_UNDERFLOW:
		err = "GL_STACK_UNDERFLOW";
		break;
	case GL_OUT_OF_MEMORY:
		err = "GL_OUT_OF_MEMORY";
		break;
	case GL_NO_ERROR:
		err = "No error! How did you even reach this code?";
		break;
	default:
		err = "Unknown error code!";
		break;
	}

	return err;
}

/* Helper types to convert between object pointers and function pointers.
   Although ignored by some compilers, this conversion is an extension
   and not guaranteed to be sane for every architecture.
 */
typedef void (*GenericFunctionPointer)();
typedef union
{
	GenericFunctionPointer FunctionPointer;
	void *ObjectPointer;
} UnsafePointerContainer;

inline static GenericFunctionPointer glGetProcAddress(const char *name)
{
	UnsafePointerContainer pc;
	pc.ObjectPointer = SDL_GL_GetProcAddress(name);
	return pc.FunctionPointer;
}

#ifndef __APPLE__
PFNGLCREATEPROGRAMPROC glCreateProgram = 0;
PFNGLDELETEPROGRAMPROC glDeleteProgram = 0;
PFNGLUSEPROGRAMPROC glUseProgram = 0;
PFNGLISPROGRAMPROC glIsProgram = 0;
PFNGLISSHADERPROC glIsShader = 0;
PFNGLCREATESHADERPROC glCreateShader = 0;
PFNGLDELETESHADERPROC glDeleteShader = 0;
PFNGLSHADERSOURCEPROC glShaderSource = 0;
PFNGLCOMPILESHADERPROC glCompileShader = 0;
PFNGLATTACHSHADERPROC glAttachShader = 0;
PFNGLDETACHSHADERPROC glDetachShader = 0;
PFNGLGETATTACHEDSHADERSPROC glGetAttachedShaders = 0;
PFNGLGETPROGRAMINFOLOGPROC glGetProgramInfoLog = 0;
PFNGLGETPROGRAMIVPROC glGetProgramiv = 0;
PFNGLGETSHADERINFOLOGPROC glGetShaderInfoLog = 0;
PFNGLGETSHADERIVPROC glGetShaderiv = 0;
PFNGLLINKPROGRAMPROC glLinkProgram = 0;
PFNGLGETUNIFORMLOCATIONPROC glGetUniformLocation = 0;
PFNGLUNIFORM1IPROC glUniform1i = 0;
PFNGLUNIFORM2FVPROC glUniform2fv = 0;
PFNGLUNIFORM4FVPROC glUniform4fv = 0;
#endif

void * (APIENTRYP glXGetCurrentDisplay)() = 0;
Uint32 (APIENTRYP glXGetCurrentDrawable)() = 0;
void (APIENTRYP glXSwapIntervalEXT)(void *display, Uint32 GLXDrawable, int interval);

Uint32 (APIENTRYP wglSwapIntervalEXT)(int interval);



void OpenGL::resize(unsigned width, unsigned height)
{
	if (gltexture == 0)
	{
		glGenTextures(1, &gltexture);
		glErrorCheck();
	}

	iwidth = width;
	iheight = height;
	if (buffer_surface) delete buffer_surface;
	buffer_surface = new Surface(iwidth, iheight, 0, 0, ibpp); // use OpenXcom's Surface class to get an aligned buffer with bonus SDL_Surface
	buffer = (uint32_t*) buffer_surface->getSurface()->pixels;

	glBindTexture(GL_TEXTURE_2D, gltexture);
	glErrorCheck();
	glPixelStorei(GL_UNPACK_ROW_LENGTH, iwidth);
	glErrorCheck();
	glTexImage2D(GL_TEXTURE_2D,
		/* mip-map level = */ 0, /* internal format = */ GL_RGB16_EXT,
		width, height, /* border = */ 0, /* format = */ GL_BGRA,
		iformat, buffer);
	glErrorCheck();

}

bool OpenGL::lock(uint32_t *&data, unsigned &pitch)
{
	pitch = iwidth * ibpp;
	return (data = buffer);
}

void OpenGL::clear() {
	//memset(buffer, 0, iwidth * iheight * ibpp);
	glClearColor(0.0, 0.0, 0.0, 1.0);
	glErrorCheck();
	glClear(GL_COLOR_BUFFER_BIT);
	glErrorCheck();
}

void OpenGL::refresh(bool smooth, unsigned inwidth, unsigned inheight, unsigned outwidth, unsigned outheight, int topBlackBand, int bottomBlackBand, int leftBlackBand, int rightBlackBand)
{
	while (glGetError() != GL_NO_ERROR); // clear possible error from who knows where
	clear();
	if (shader_support && glprogram)
	{
		glUseProgram(glprogram);
		glErrorCheck();
		GLint location;

		float inputSize[2] = { (float)inwidth, (float)inheight };
		location = glGetUniformLocation(glprogram, "rubyInputSize");
		glUniform2fv(location, 1, inputSize);

		float outputSize[2] = { (float)outwidth, (float)outheight };
		location = glGetUniformLocation(glprogram, "rubyOutputSize");
		glUniform2fv(location, 1, outputSize);

		float textureSize[2] = { (float)iwidth, (float)iheight };
		location = glGetUniformLocation(glprogram, "rubyTextureSize");
		glUniform2fv(location, 1, textureSize);
		glErrorCheck();
    }

	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, smooth ? GL_LINEAR : GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, smooth ? GL_LINEAR : GL_NEAREST);

	glErrorCheck();

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glOrtho(0, outwidth, 0, outheight, -1.0, 1.0);
	glViewport(0, 0, outwidth, outheight);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	glErrorCheck();

	glPixelStorei(GL_UNPACK_ROW_LENGTH, buffer_surface->getSurface()->pitch / buffer_surface->getSurface()->format->BytesPerPixel);

	glErrorCheck();

	glTexSubImage2D(GL_TEXTURE_2D,
		/* mip-map level = */ 0, /* x = */ 0, /* y = */ 0,
		iwidth, iheight, GL_BGRA, iformat, buffer);


	//OpenGL projection sets 0,0 as *bottom-left* of screen.
	//therefore, below vertices flip image to support top-left source.
	//texture range = x1:0.0, y1:0.0, x2:1.0, y2:1.0
	//vertex range = x1:0, y1:0, x2:width, y2:height
	if (leftBlackBand + rightBlackBand + topBlackBand + bottomBlackBand == 0)
	{
		double w = double(inwidth)  / double(iwidth)  * 2;
		double h = double(inheight) / double(iheight) * 2;
		int u1 = 0;
		int u2 = outwidth * 2;
		int v1 = outheight;
		int v2 = - outheight;

		glBegin(GL_TRIANGLES);
		glTexCoord2f(0, 0); glVertex3i(u1, v1, 0);
		glTexCoord2f(w, 0); glVertex3i(u2, v1, 0);
		glTexCoord2f(0, h); glVertex3i(u1, v2, 0);
		glEnd();
	}
	else
	{
		double w = double(inwidth)  / double(iwidth);
		double h = double(inheight) / double(iheight);
		int u1 = leftBlackBand;
		int u2 = outwidth - rightBlackBand;
		int v1 = outheight - topBlackBand;
		int v2 = bottomBlackBand;

		glBegin(GL_TRIANGLE_STRIP);
		glTexCoord2f(0, 0); glVertex3i(u1, v1, 0);
		glTexCoord2f(w, 0); glVertex3i(u2, v1, 0);
		glTexCoord2f(0, h); glVertex3i(u1, v2, 0);
		glTexCoord2f(w, h); glVertex3i(u2, v2, 0);
		glEnd();
	}
	glErrorCheck();

    if (shader_support)
	{
		glUseProgram(0);
		glErrorCheck();
	}
}

bool OpenGL::set_shader(const char *source_yaml_filename)
{
	if (!shader_support) return false;

	if (glprogram)
	{
		glDeleteProgram(glprogram);
		glprogram = 0;
	}

	if (source_yaml_filename && source_yaml_filename[0] != '\0')
	{
		glprogram = glCreateProgram();
		if (glprogram == 0)
		{
			Log(LOG_ERROR) << "Failed to create GLSL shader program";
			return false;
		}
		try
		{
			YAML::Node document = YAML::LoadFile(source_yaml_filename);

			bool is_glsl;
			std::string language = document["language"].as<std::string>();
			is_glsl = (language == "GLSL");


			linear = document["linear"].as<bool>(false); // some shaders want texture linear interpolation and some don't
			std::string fragment_source = document["fragment"].as<std::string>("");
			std::string vertex_source = document["vertex"].as<std::string>("");

			if (is_glsl)
			{
				if (!fragment_source.empty()) set_fragment_shader(fragment_source.c_str());
				if (!vertex_source.empty()) set_vertex_shader(vertex_source.c_str());
			}
			else
			{
				Log(LOG_ERROR) << "Unexpected shader language \"" <<
					document["language"].as<std::string>() << "\"";
			}
		}
		catch (YAML::Exception &e)
		{
			Log(LOG_ERROR) << source_yaml_filename << ": " << e.what();
			glDeleteProgram(glprogram);
			glprogram = 0;
			return false;
		}

		glLinkProgram(glprogram);
		glErrorCheck();
		GLint linkStatus;
		glGetProgramiv(glprogram, GL_LINK_STATUS, &linkStatus);
		glErrorCheck();
		if (linkStatus != GL_TRUE)
		{
			GLint infoLogLength;
			glGetProgramiv(glprogram, GL_INFO_LOG_LENGTH, &infoLogLength);
			glErrorCheck();
			if (infoLogLength == 0)
			{
				Log(LOG_ERROR) << "OpenGL shader link failed: No log returned from driver";
			}
			else
			{
				GLchar *infoLog = new GLchar[infoLogLength];
				glGetProgramInfoLog(glprogram, infoLogLength, NULL, infoLog);
				glErrorCheck();

				Log(LOG_ERROR) << "OpenGL shader link failed \"" << infoLog << "\"";

				delete[] infoLog;
			}
			glDeleteProgram(glprogram);
			glErrorCheck();
			glprogram = 0;
		}
	}
	return glprogram != 0;
}

static GLuint createShader(GLenum type, const char *source)
{
	GLuint shader = glCreateShader(type);
	glErrorCheck();
	glShaderSource(shader, 1, &source, 0);
	glErrorCheck();
	glCompileShader(shader);
	glErrorCheck();

	GLint compileSuccess;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess);
	glErrorCheck();
	if (compileSuccess != GL_TRUE)
	{
		GLint infoLogLength;
		glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLogLength);
		glErrorCheck();
		if (infoLogLength == 0)
		{
			Log(LOG_ERROR) << "OpenGL shader compilation failed: No log returned from driver";
		}
		else
		{
			GLchar *infoLog = new GLchar[infoLogLength];
			glGetShaderInfoLog(shader, infoLogLength, NULL, infoLog);
			glErrorCheck();

			Log(LOG_ERROR) << "OpenGL shader compilation failed: \"" << infoLog << "\"";

			delete[] infoLog;
		}
		glDeleteShader(shader);
		glErrorCheck();
		shader = 0;
	}

	return shader;
}

void OpenGL::set_fragment_shader(const char *source)
{
	GLint fragmentshader = createShader(GL_FRAGMENT_SHADER, source);
	if (fragmentshader)
	{
		glAttachShader(glprogram, fragmentshader);
		glErrorCheck();
		glDeleteShader(fragmentshader);
	}
}

void OpenGL::set_vertex_shader(const char *source)
{
	GLint vertexshader = createShader(GL_VERTEX_SHADER, source);
	if (vertexshader)
	{
		glAttachShader(glprogram, vertexshader);
		glErrorCheck();
		glDeleteShader(vertexshader);
	}
}

void OpenGL::init(int w, int h)
{
	//disable unused features
	glDisable(GL_ALPHA_TEST);
	glDisable(GL_BLEND);
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_POLYGON_SMOOTH);
	glDisable(GL_STENCIL_TEST);
	glErrorCheck();

	//enable useful and required features
	glEnable(GL_DITHER);
	glEnable(GL_TEXTURE_2D);
	glErrorCheck();

    //bind shader functions
#ifndef __APPLE__
	glCreateProgram = (PFNGLCREATEPROGRAMPROC)glGetProcAddress("glCreateProgram");
	glDeleteProgram = (PFNGLDELETEPROGRAMPROC)glGetProcAddress("glDeleteProgram");
	glUseProgram = (PFNGLUSEPROGRAMPROC)glGetProcAddress("glUseProgram");
	glIsProgram = (PFNGLISPROGRAMPROC)glGetProcAddress("glIsProgram");
	glIsShader = (PFNGLISSHADERPROC)glGetProcAddress("glIsShader");
	glCreateShader = (PFNGLCREATESHADERPROC)glGetProcAddress("glCreateShader");
	glDeleteShader = (PFNGLDELETESHADERPROC)glGetProcAddress("glDeleteShader");
	glShaderSource = (PFNGLSHADERSOURCEPROC)glGetProcAddress("glShaderSource");
	glCompileShader = (PFNGLCOMPILESHADERPROC)glGetProcAddress("glCompileShader");
	glAttachShader = (PFNGLATTACHSHADERPROC)glGetProcAddress("glAttachShader");
	glDetachShader = (PFNGLDETACHSHADERPROC)glGetProcAddress("glDetachShader");
	glGetAttachedShaders = (PFNGLGETATTACHEDSHADERSPROC)glGetProcAddress("glGetAttachedShaders");
	glGetProgramiv = (PFNGLGETPROGRAMIVPROC)glGetProcAddress("glGetProgramiv");
	glGetProgramInfoLog = (PFNGLGETPROGRAMINFOLOGPROC)glGetProcAddress("glGetProgramInfoLog");
	glGetShaderiv = (PFNGLGETSHADERIVPROC)glGetProcAddress("glGetShaderiv");
	glGetShaderInfoLog = (PFNGLGETSHADERINFOLOGPROC)glGetProcAddress("glGetShaderInfoLog");
	glLinkProgram = (PFNGLLINKPROGRAMPROC)glGetProcAddress("glLinkProgram");
	glGetUniformLocation = (PFNGLGETUNIFORMLOCATIONPROC)glGetProcAddress("glGetUniformLocation");
	glUniform1i = (PFNGLUNIFORM1IPROC)glGetProcAddress("glUniform1i");
	glUniform2fv = (PFNGLUNIFORM2FVPROC)glGetProcAddress("glUniform2fv");
	glUniform4fv = (PFNGLUNIFORM4FVPROC)glGetProcAddress("glUniform4fv");

	shader_support = glCreateProgram && glDeleteProgram && glUseProgram && glCreateShader
	&& glDeleteShader && glShaderSource && glCompileShader && glAttachShader
	&& glDetachShader && glLinkProgram && glGetUniformLocation && glIsProgram && glIsShader
	&& glUniform1i && glUniform2fv && glUniform4fv && glGetAttachedShaders
	&& glGetShaderiv && glGetShaderInfoLog && glGetProgramiv && glGetProgramInfoLog;
#else
	shader_support = true;
#endif
	glXGetCurrentDisplay = (void* (APIENTRYP)())glGetProcAddress("glXGetCurrentDisplay");
	glXGetCurrentDrawable = (Uint32 (APIENTRYP)())glGetProcAddress("glXGetCurrentDrawable");
	glXSwapIntervalEXT = (void (APIENTRYP)(void*, Uint32, int))glGetProcAddress("glXSwapIntervalEXT");

	wglSwapIntervalEXT = (Uint32 (APIENTRYP)(int))glGetProcAddress("wglSwapIntervalEXT");

	if (shader_support)
	{
		if (glprogram) {
			if (glIsProgram(glprogram))
			{
				glDeleteProgram(glprogram);
				glErrorCheck();
			}
		}
		glprogram = glCreateProgram();
		glErrorCheck();
	}

	//create surface texture
	resize(w, h);
}

void OpenGL::setVSync(bool sync)
{
	const int interval = sync ? 1 : 0;
	if (glXGetCurrentDisplay && glXGetCurrentDrawable && glXSwapIntervalEXT)
	{
		void *dpy = glXGetCurrentDisplay();
		glErrorCheck();
		Uint32 drawable = glXGetCurrentDrawable();
		glErrorCheck();

		if (drawable) {
			glXSwapIntervalEXT(dpy, drawable, interval);
			glErrorCheck();
			// Log(LOG_INFO) << "Made an attempt to set vsync via GLX.";
		}
	} else if (wglSwapIntervalEXT)
	{
		wglSwapIntervalEXT(interval);
		glErrorCheck();
		// Log(LOG_INFO) << "Made an attempt to set vsync via WGL.";
	}
}

void OpenGL::term()
{
	if (gltexture) {
		glDeleteTextures(1, &gltexture);
		glErrorCheck();
		gltexture = 0;
	}

	if (glprogram) {
		glDeleteProgram(glprogram);
		glprogram = 0;
	}

	if (buffer) {
		buffer = 0;
		iwidth = 0;
		iheight = 0;
	}

	delete buffer_surface;
}

  OpenGL::OpenGL() : gltexture(0), glprogram(0), linear(false), shader_support(false),
                     buffer(NULL), buffer_surface(NULL), iwidth(0), iheight(0),
                     iformat(GL_UNSIGNED_INT_8_8_8_8_REV), // this didn't seem to be set anywhere before...
                     ibpp(32)                              // ...nor this
  { }

  OpenGL::~OpenGL()
	{
		term();
	}

}

#endif
