#include "lc_global.h"
#include "lc_context.h"
#include "lc_glextensions.h"
#include "lc_mesh.h"
#include "lc_texture.h"
#include "lc_colors.h"
#include "lc_mainwindow.h"
#include "lc_library.h"
#include "texfont.h"
#include "lc_view.h"
#include "lc_viewsphere.h"
#include "lc_viewmanipulator.h"
#include "lc_stringcache.h"
#include "lc_partselectionwidget.h"
#include <QOpenGLFunctions_3_2_Core>

#ifdef LC_OPENGLES
#define glEnableClientState(...)
#define glDisableClientState(...)
#define glVertexPointer(...)
#define glTexCoordPointer(...)
#define glColorPointer(...)
#define GL_ARRAY_BUFFER_ARB GL_ARRAY_BUFFER
#define GL_ELEMENT_ARRAY_BUFFER_ARB GL_ELEMENT_ARRAY_BUFFER
#define GL_STATIC_DRAW_ARB GL_STATIC_DRAW
#endif

std::unique_ptr<QOpenGLContext> lcContext::mOffscreenContext;
std::unique_ptr<QOffscreenSurface> lcContext::mOffscreenSurface;
std::unique_ptr<lcContext> lcContext::mGlobalOffscreenContext;
lcProgram lcContext::mPrograms[static_cast<int>(lcMaterialType::Count)];

lcContext::lcContext()
{
	mVertexBufferObject = 0;
	mIndexBufferObject = 0;
	mVertexBufferPointer = nullptr;
	mIndexBufferPointer = nullptr;
	mVertexBufferOffset = (char*)~0;

	mNormalEnabled = false;
	mTexCoordEnabled = false;
	mColorEnabled = false;

	mTexture2D = 0;
	mTextureCubeMap = 0;
	mPolygonOffset = lcPolygonOffset::None;
	mDepthWrite = true;
	mDepthFunction = lcDepthFunction::LessEqual;
	mDepthTest = true;
	mColorWrite = true;
	mColorBlend = false;
	mCullFace = false;
	mLineWidth = 1.0f;

	mColor = lcVector4(0.0f, 0.0f, 0.0f, 0.0f);
	mWorldMatrix = lcMatrix44Identity();
	mViewMatrix = lcMatrix44Identity();
	mProjectionMatrix = lcMatrix44Identity();
	mViewProjectionMatrix = lcMatrix44Identity();
	mHighlightParams[0] = lcVector4(0.0f, 0.0f, 0.0f, 0.0f);
	mHighlightParams[1] = lcVector4(0.0f, 0.0f, 0.0f, 0.0f);
	mHighlightParams[2] = lcVector4(0.0f, 0.0f, 0.0f, 0.0f);
	mHighlightParams[3] = lcVector4(0.0f, 0.0f, 0.0f, 0.0f);
	mColorDirty = false;
	mWorldMatrixDirty = false;
	mViewMatrixDirty = false;
	mProjectionMatrixDirty = false;
	mViewProjectionMatrixDirty = false;
	mHighlightParamsDirty = false;

	mMaterialType = lcMaterialType::Count;
}

lcContext::~lcContext()
{
}

bool lcContext::InitializeRenderer()
{
	if (!CreateOffscreenContext())
		return false;

	mGlobalOffscreenContext = std::unique_ptr<lcContext>(new(lcContext));

	lcContext* Context = mGlobalOffscreenContext.get();
	Context->SetOffscreenContext();

	lcInitializeGLExtensions(mOffscreenContext.get());

	gStringCache.Initialize(Context);
	gTexFont.Initialize(Context);

	Context->CreateResources();
	lcView::CreateResources(Context);
	lcViewManipulator::CreateResources(Context);
	lcViewSphere::CreateResources(Context);

	if (!gSupportsShaderObjects && lcGetPreferences().mShadingMode == lcShadingMode::DefaultLights)
		lcGetPreferences().mShadingMode = lcShadingMode::Flat;

	if (!gSupportsShaderObjects && lcGetPreferences().mDrawConditionalLines)
		lcGetPreferences().mDrawConditionalLines = false;

	if (!gSupportsFramebufferObject)
		gMainWindow->GetPartSelectionWidget()->DisableIconMode();

	return true;
}

void lcContext::ShutdownRenderer()
{
	if (!mGlobalOffscreenContext)
		return;

	mGlobalOffscreenContext->MakeCurrent();
	lcContext* Context = mGlobalOffscreenContext.get();

	gStringCache.Reset();
	gTexFont.Reset();

	lcView::DestroyResources(Context);
	Context->DestroyResources();
	lcViewManipulator::DestroyResources(Context);
	lcViewSphere::DestroyResources(Context);

	mGlobalOffscreenContext.reset();

	lcContext::DestroyOffscreenContext();
}

lcContext* lcContext::GetGlobalOffscreenContext()
{
	return mGlobalOffscreenContext.get();
}

bool lcContext::CreateOffscreenContext()
{
	std::unique_ptr<QOpenGLContext> OffscreenContext(new QOpenGLContext());

	if (!OffscreenContext)
		return false;

	OffscreenContext->setShareContext(QOpenGLContext::globalShareContext());

	if (!OffscreenContext->create() || !OffscreenContext->isValid())
		return false;

	std::unique_ptr<QOffscreenSurface> OffscreenSurface(new QOffscreenSurface());

	if (!OffscreenSurface)
		return false;

	OffscreenSurface->create();

	if (!OffscreenSurface->isValid())
		return false;

	if (!OffscreenContext->makeCurrent(OffscreenSurface.get()))
		return false;

	mOffscreenContext = std::move(OffscreenContext);
	mOffscreenSurface = std::move(OffscreenSurface);

	return true;
}

void lcContext::DestroyOffscreenContext()
{
	mOffscreenSurface.reset();
	mOffscreenContext.reset();
}

void lcContext::CreateShaderPrograms()
{
	const char* ShaderPrefix =
	{
#ifndef LC_OPENGLES
"#version 110\n"
"#define mediump\n"
"#define LC_VERTEX_INPUT attribute\n"
"#define LC_VERTEX_OUTPUT varying\n"
"#define LC_PIXEL_INPUT varying\n"
"#define LC_PIXEL_OUTPUT\n"
"#define LC_SHADER_PRECISION\n"
#else
"#version 300 es\n"
"#define texture2D texture\n"
"#define LC_VERTEX_INPUT in\n"
"#define LC_VERTEX_OUTPUT out\n"
"#define LC_PIXEL_INPUT in mediump\n"
"#define gl_FragColor FragColor\n"
"#define LC_PIXEL_OUTPUT out mediump vec4 gl_FragColor;\n"
"#define LC_SHADER_PRECISION mediump\n"
#endif

"#define LC_PIXEL_FAKE_LIGHTING \\\n"
"		LC_SHADER_PRECISION vec3 Normal = normalize(PixelNormal); \\\n"
"		LC_SHADER_PRECISION vec3 LightDirection = normalize(PixelPosition - LightPosition); \\\n"
"		LC_SHADER_PRECISION vec3 VertexToEye = normalize(EyePosition - PixelPosition); \\\n"
"		LC_SHADER_PRECISION vec3 LightReflect = normalize(reflect(-LightDirection, Normal)); \\\n"
"		LC_SHADER_PRECISION float Specular = abs(dot(VertexToEye, LightReflect)); \\\n"
"		Specular = min(pow(Specular, 8.0), 1.0) * 0.25; \\\n"
"		LC_SHADER_PRECISION vec3 SpecularColor = vec3(Specular, Specular, Specular); \\\n"
"		LC_SHADER_PRECISION float Diffuse = min(abs(dot(Normal, LightDirection)) * 0.6 + 0.65, 1.0);\n"
	};

	const char* const VertexShaders[] =
	{
		":/resources/shaders/unlit_color_vs.glsl",             // UnlitColor
		":/resources/shaders/unlit_color_conditional_vs.glsl", // UnlitColorConditional
		":/resources/shaders/unlit_texture_modulate_vs.glsl",  // UnlitTextureModulate
		":/resources/shaders/unlit_texture_decal_vs.glsl",     // UnlitTextureDecal
		":/resources/shaders/unlit_vertex_color_vs.glsl",      // UnlitVertexColor
		":/resources/shaders/unlit_view_sphere_vs.glsl",       // UnlitViewSphere
		":/resources/shaders/fakelit_color_vs.glsl",           // FakeLitColor
		":/resources/shaders/fakelit_texture_decal_vs.glsl"    // FakeLitTextureDecal
	};

	LC_ARRAY_SIZE_CHECK(VertexShaders, lcMaterialType::Count);

	const char* const FragmentShaders[] =
	{
		":/resources/shaders/unlit_color_ps.glsl",             // UnlitColor
		":/resources/shaders/unlit_color_conditional_ps.glsl", // UnlitColorConditional
		":/resources/shaders/unlit_texture_modulate_ps.glsl",  // UnlitTextureModulate
		":/resources/shaders/unlit_texture_decal_ps.glsl",     // UnlitTextureDecal
		":/resources/shaders/unlit_vertex_color_ps.glsl",      // UnlitVertexColor
		":/resources/shaders/unlit_view_sphere_ps.glsl",       // UnlitViewSphere
		":/resources/shaders/fakelit_color_ps.glsl",           // FakeLitColor
		":/resources/shaders/fakelit_texture_decal_ps.glsl"    // FakeLitTextureDecal
	};

	LC_ARRAY_SIZE_CHECK(FragmentShaders, lcMaterialType::Count);

	const auto LoadShader = [this, ShaderPrefix](const char* FileName, GLuint ShaderType) -> GLuint
	{
		QFile ShaderFile(FileName);

		if (!ShaderFile.open(QIODevice::ReadOnly))
			return 0;

		QByteArray Data = ShaderPrefix + ShaderFile.readAll();
		const char* Source = Data.constData();

		const GLuint Shader = glCreateShader(ShaderType);
		glShaderSource(Shader, 1, &Source, nullptr);
		glCompileShader(Shader);

#ifndef QT_NO_DEBUG
		GLint ShaderCompiled = 0;
		glGetShaderiv(Shader, GL_COMPILE_STATUS, &ShaderCompiled);

		if (ShaderCompiled == GL_FALSE)
		{
			GLint Length = 0;
			glGetShaderiv(Shader, GL_INFO_LOG_LENGTH, &Length);

			QByteArray InfoLog;
			InfoLog.resize(Length);
			glGetShaderInfoLog(Shader, Length, &Length, InfoLog.data());

			qDebug() << InfoLog;
		}
#endif

		return Shader;
	};

	for (int MaterialType = 0; MaterialType < static_cast<int>(lcMaterialType::Count); MaterialType++)
	{
		const GLuint VertexShader = LoadShader(VertexShaders[MaterialType], GL_VERTEX_SHADER);
		const GLuint FragmentShader = LoadShader(FragmentShaders[MaterialType], GL_FRAGMENT_SHADER);

		GLuint Program = glCreateProgram();

		glAttachShader(Program, VertexShader);
		glAttachShader(Program, FragmentShader);

		glBindAttribLocation(Program, static_cast<int>(lcProgramAttrib::Position), "VertexPosition");
		glBindAttribLocation(Program, static_cast<int>(lcProgramAttrib::Normal), "VertexNormal");
		glBindAttribLocation(Program, static_cast<int>(lcProgramAttrib::TexCoord), "VertexTexCoord");
		glBindAttribLocation(Program, static_cast<int>(lcProgramAttrib::Color), "VertexColor");

		glBindAttribLocation(Program, static_cast<int>(lcProgramAttrib::ControlPoint1), "VertexPosition1");
		glBindAttribLocation(Program, static_cast<int>(lcProgramAttrib::ControlPoint2), "VertexPosition2");
		glBindAttribLocation(Program, static_cast<int>(lcProgramAttrib::ControlPoint3), "VertexPosition3");
		glBindAttribLocation(Program, static_cast<int>(lcProgramAttrib::ControlPoint4), "VertexPosition4");

		glLinkProgram(Program);

		glDetachShader(Program, VertexShader);
		glDetachShader(Program, FragmentShader);
		glDeleteShader(VertexShader);
		glDeleteShader(FragmentShader);

		GLint IsLinked = 0;
		glGetProgramiv(Program, GL_LINK_STATUS, &IsLinked);

		if (IsLinked == GL_FALSE)
		{
			GLint Length = 0;
			glGetProgramiv(Program, GL_INFO_LOG_LENGTH, &Length);

			QByteArray InfoLog;
			InfoLog.resize(Length);
			glGetProgramInfoLog(Program, Length, &Length, InfoLog.data());

			glDeleteProgram(Program);
			Program = 0;
		}

		mPrograms[MaterialType].Object = Program;
		mPrograms[MaterialType].WorldViewProjectionMatrixLocation = glGetUniformLocation(Program, "WorldViewProjectionMatrix");
		mPrograms[MaterialType].WorldMatrixLocation = glGetUniformLocation(Program, "WorldMatrix");
		mPrograms[MaterialType].MaterialColorLocation = glGetUniformLocation(Program, "MaterialColor");
		mPrograms[MaterialType].LightPositionLocation = glGetUniformLocation(Program, "LightPosition");
		mPrograms[MaterialType].EyePositionLocation = glGetUniformLocation(Program, "EyePosition");
		mPrograms[MaterialType].HighlightParamsLocation = glGetUniformLocation(Program, "HighlightParams");

		const GLint TextureLocation = glGetUniformLocation(Program, "Texture");

		if (TextureLocation != -1)
		{
			glUseProgram(Program);
			glUniform1i(TextureLocation, 0);
			glUseProgram(0);
		}
	}
}

void lcContext::CreateResources()
{
	if (!gSupportsShaderObjects)
		return;

	CreateShaderPrograms();
}

void lcContext::DestroyResources()
{
	if (!gSupportsShaderObjects)
		return;

	for (int MaterialType = 0; MaterialType < static_cast<int>(lcMaterialType::Count); MaterialType++)
	{
		glDeleteProgram(mPrograms[MaterialType].Object);
		mPrograms[MaterialType].Object = 0;
	}
}

void lcContext::MakeCurrent()
{
	if (mWidget)
		mWidget->makeCurrent();
	else
		mOffscreenContext->makeCurrent(mOffscreenSurface.get());
}

void lcContext::SetGLContext(QOpenGLContext* Context, QOpenGLWidget* Widget)
{
	mContext = Context;
	mWidget = Widget;

	MakeCurrent();
	initializeOpenGLFunctions();
}

void lcContext::SetOffscreenContext()
{
	SetGLContext(mOffscreenContext.get(), nullptr);
}

void lcContext::SetDefaultState()
{
#ifndef LC_OPENGLES
	if (QSurfaceFormat::defaultFormat().samples() > 1)
		glEnable(GL_LINE_SMOOTH);
#endif

	glEnable(GL_DEPTH_TEST);
	mDepthTest = true;

	glDepthFunc(GL_LEQUAL);
	mDepthFunction = lcDepthFunction::LessEqual;

	mColorWrite = true;
	glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

	mColorBlend = false;
	glDisable(GL_BLEND);

	if (gSupportsBlendFuncSeparate)
		glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_ONE);
	else
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

	if (gSupportsVertexBufferObject)
	{
		glBindBuffer(GL_ARRAY_BUFFER_ARB, 0);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
	}

	if (gSupportsShaderObjects)
	{
		SetVertexAttribPointer(lcProgramAttrib::Position, 3, GL_FLOAT, false, 0, nullptr);
		EnableVertexAttrib(lcProgramAttrib::Position);

		DisableVertexAttrib(lcProgramAttrib::Normal);
		SetVertexAttribPointer(lcProgramAttrib::Normal, 4, GL_BYTE, true, 0, nullptr);
		DisableVertexAttrib(lcProgramAttrib::TexCoord);
		SetVertexAttribPointer(lcProgramAttrib::TexCoord, 2, GL_FLOAT, false, 0, nullptr);
		DisableVertexAttrib(lcProgramAttrib::Color);
		SetVertexAttribPointer(lcProgramAttrib::Color, 4, GL_FLOAT, false, 0, nullptr);
	}

	mVertexBufferObject = 0;
	mIndexBufferObject = 0;
	mVertexBufferPointer = nullptr;
	mIndexBufferPointer = nullptr;
	mVertexBufferOffset = (char*)~0;

	for (int AttribIndex = 0; AttribIndex < static_cast<int>(lcProgramAttrib::Count); AttribIndex++)
		mVertexAttribState[AttribIndex] = lcVertexAttribState();

	glBindTexture(GL_TEXTURE_2D, 0);
	mTexture2D = 0;
	glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
	mTextureCubeMap = 0;

	glDisable(GL_POLYGON_OFFSET_FILL);
	mPolygonOffset = lcPolygonOffset::None;

	mDepthWrite = true;
	glDepthMask(GL_TRUE);

	glDisable(GL_CULL_FACE);
	mCullFace = false;

	glLineWidth(1.0f);
	mLineWidth = 1.0f;

	if (gSupportsShaderObjects)
	{
		glUseProgram(0);
		mMaterialType = lcMaterialType::Count;
	}
}

void lcContext::ClearColorAndDepth(const lcVector4& ClearColor)
{
	glClearColor(ClearColor[0], ClearColor[1], ClearColor[2], ClearColor[3]);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
}

void lcContext::ClearDepth()
{
	glClear(GL_DEPTH_BUFFER_BIT);
}

void lcContext::ClearResources()
{
	ClearVertexBuffer();
	ClearIndexBuffer();
	ClearTexture2D();
}

void lcContext::SetMaterial(lcMaterialType MaterialType)
{
	if (MaterialType == mMaterialType)
		return;

	mMaterialType = MaterialType;

	if (gSupportsShaderObjects)
	{
		glUseProgram(mPrograms[static_cast<int>(MaterialType)].Object);
		mColorDirty = true;
		mWorldMatrixDirty = true; // todo: change dirty to a bitfield and set the lighting constants dirty here
		mViewMatrixDirty = true;
		mHighlightParamsDirty = true;
	}
}

void lcContext::SetViewport(int x, int y, int Width, int Height)
{
	glViewport(x, y, Width, Height);
}

void lcContext::SetPolygonOffset(lcPolygonOffset PolygonOffset)
{
	if (mPolygonOffset == PolygonOffset)
		return;

	switch (PolygonOffset)
	{
	case lcPolygonOffset::None:
		glDisable(GL_POLYGON_OFFSET_FILL);
		break;

	case lcPolygonOffset::Opaque:
		glPolygonOffset(0.5f, 0.1f);
		glEnable(GL_POLYGON_OFFSET_FILL);
		break;

	case lcPolygonOffset::Translucent:
		glPolygonOffset(0.25f, 0.1f);
		glEnable(GL_POLYGON_OFFSET_FILL);
		break;
	}

	mPolygonOffset = PolygonOffset;
}

void lcContext::SetDepthWrite(bool Enable)
{
	if (Enable == mDepthWrite)
		return;

	glDepthMask(Enable ? GL_TRUE : GL_FALSE);
	mDepthWrite = Enable;
}

void lcContext::SetDepthFunction(lcDepthFunction DepthFunction)
{
	if (DepthFunction == mDepthFunction)
		return;

	switch (DepthFunction)
	{
	case lcDepthFunction::Always:
		glDepthFunc(GL_ALWAYS);
		break;

	case lcDepthFunction::LessEqual:
		glDepthFunc(GL_LEQUAL);
		break;
	}

	mDepthFunction = DepthFunction;
}

void lcContext::EnableDepthTest(bool Enable)
{
	if (Enable == mDepthTest)
		return;

	if (Enable)
		glEnable(GL_DEPTH_TEST);
	else
		glDisable(GL_DEPTH_TEST);

	mDepthTest = Enable;
}

void lcContext::EnableColorWrite(bool Enable)
{
	if (Enable == mColorWrite)
		return;

	if (Enable)
		glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
	else
		glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);

	mColorWrite = Enable;
}

void lcContext::EnableColorBlend(bool Enable)
{
	if (Enable == mColorBlend)
		return;

	if (Enable)
		glEnable(GL_BLEND);
	else
		glDisable(GL_BLEND);

	mColorBlend = Enable;
}

void lcContext::EnableCullFace(bool Enable)
{
	if (Enable == mCullFace)
		return;

	if (Enable)
		glEnable(GL_CULL_FACE);
	else
		glDisable(GL_CULL_FACE);

	mCullFace = Enable;
}

void lcContext::SetLineWidth(float LineWidth)
{
	if (LineWidth == mLineWidth)
		return;

	glLineWidth(LineWidth);
	mLineWidth = LineWidth;
}

void lcContext::BindTexture2D(const lcTexture* Texture)
{
	GLuint TextureObject = Texture->mTexture;

	if (mTexture2D == TextureObject)
		return;

	glBindTexture(GL_TEXTURE_2D, TextureObject);
	mTexture2D = TextureObject;
}

void lcContext::BindTextureCubeMap(const lcTexture* Texture)
{
	GLuint TextureObject = Texture->mTexture;

	if (mTextureCubeMap == TextureObject)
		return;

	glBindTexture(GL_TEXTURE_CUBE_MAP, TextureObject);
	mTextureCubeMap = TextureObject;
}

void lcContext::ClearTexture2D()
{
	if (mTexture2D == 0)
		return;

	glBindTexture(GL_TEXTURE_2D, 0);
	mTexture2D = 0;
}

void lcContext::ClearTextureCubeMap()
{
	if (mTexture2D == 0)
		return;

	glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
	mTextureCubeMap = 0;
}

void lcContext::UploadTexture(lcTexture* Texture)
{
	if (!Texture->mTexture)
		glGenTextures(1, &Texture->mTexture);

	constexpr int Filters[2][5] =
	{
		{ GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR },
		{ GL_NEAREST, GL_LINEAR, GL_LINEAR, GL_LINEAR, GL_LINEAR  },
	};

	const int Flags = Texture->GetFlags();
	const int FilterFlags = Flags & LC_TEXTURE_FILTER_MASK;
	const int FilterIndex = FilterFlags >> LC_TEXTURE_FILTER_SHIFT;
	const int MipIndex = Flags & LC_TEXTURE_MIPMAPS ? 0 : 1;

	unsigned int Faces, Target;

	if ((Flags & LC_TEXTURE_CUBEMAP) == 0)
	{
		Faces = 1;
		Target = GL_TEXTURE_2D;
		BindTexture2D(Texture);
	}
	else
	{
		Faces = 6;
		Target = GL_TEXTURE_CUBE_MAP;
		BindTextureCubeMap(Texture);
	}

	glTexParameteri(Target, GL_TEXTURE_WRAP_S, (Flags & LC_TEXTURE_WRAPU) ? GL_REPEAT : GL_CLAMP_TO_EDGE);
	glTexParameteri(Target, GL_TEXTURE_WRAP_T, (Flags & LC_TEXTURE_WRAPV) ? GL_REPEAT : GL_CLAMP_TO_EDGE);
	glTexParameteri(Target, GL_TEXTURE_MIN_FILTER, Filters[MipIndex][FilterIndex]);
	glTexParameteri(Target, GL_TEXTURE_MAG_FILTER, Filters[1][FilterIndex]);
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
	glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

	if (gSupportsAnisotropic && FilterFlags == LC_TEXTURE_ANISOTROPIC)
		glTexParameterf(Target, GL_TEXTURE_MAX_ANISOTROPY_EXT, lcMin(4.0f, gMaxAnisotropy));

	int Format;
	switch (Texture->GetImage(0).mFormat)
	{
	default:
	case lcPixelFormat::Invalid:
		Format = 0;
		break;
	case lcPixelFormat::A8:
		Format = GL_ALPHA;
		break;
	case lcPixelFormat::L8A8:
		Format = GL_LUMINANCE_ALPHA;
		break;
	case lcPixelFormat::R8G8B8:
		Format = GL_RGB;
		break;
	case lcPixelFormat::R8G8B8A8:
		Format = GL_RGBA;
		break;
	}

	int CurrentImage = 0;
	if (Flags & LC_TEXTURE_CUBEMAP)
		Target = GL_TEXTURE_CUBE_MAP_POSITIVE_X;

	for (size_t FaceIdx = 0; FaceIdx < Faces; FaceIdx++)
	{
		void* Data = Texture->GetImage(CurrentImage).mData;
		glTexImage2D(Target, 0, Format, Texture->mWidth, Texture->mHeight, 0, Format, GL_UNSIGNED_BYTE, Data);

		if (Flags & LC_TEXTURE_MIPMAPS || FilterFlags >= LC_TEXTURE_BILINEAR)
		{
			int Width = Texture->mWidth;
			int Height = Texture->mHeight;
			int Components = Texture->GetImage(CurrentImage).GetBPP();

			for (int Level = 1; ((Width != 1) || (Height != 1)); Level++)
			{
				int RowStride = Width * Components;

				Width = lcMax(1, Width >> 1);
				Height = lcMax(1, Height >> 1);

				if (Texture->GetImageCount() == Faces)
				{
					GLubyte* Out, * In;

					In = Out = (GLubyte*)Data;

					for (int y = 0; y < Height; y++, In += RowStride)
						for (int x = 0; x < Width; x++, Out += Components, In += 2 * Components)
							for (int c = 0; c < Components; c++)
								Out[c] = (In[c] + In[c + Components] + In[RowStride] + In[c + RowStride + Components]) / 4;
				}
				else
					Data = Texture->GetImage(++CurrentImage).mData;

				glTexImage2D(Target, Level, Format, Width, Height, 0, Format, GL_UNSIGNED_BYTE, Data);
			}

			if (Texture->GetImageCount() == Faces)
				CurrentImage++;
		}
		else
			CurrentImage++;

		Target++;
	}

	if ((Flags & LC_TEXTURE_CUBEMAP) == 0)
		ClearTexture2D();
	else
		ClearTextureCubeMap();
}

void lcContext::SetColor(float Red, float Green, float Blue, float Alpha)
{
	SetColor(lcVector4(Red, Green, Blue, Alpha));
}

void lcContext::SetColorIndex(int ColorIndex)
{
	SetColor(gColorList[ColorIndex].Value);
}

void lcContext::SetColorIndexTinted(int ColorIndex, const lcVector4& Tint, float Weight)
{
	const lcVector3 Color(gColorList[ColorIndex].Value * Weight + Tint * (1.0f - Weight));
	SetColor(lcVector4(Color, gColorList[ColorIndex].Value.w));
}

void lcContext::SetColorIndexTinted(int ColorIndex, const lcVector4& Tint)
{
	SetColor(gColorList[ColorIndex].Value * Tint);
}

void lcContext::SetEdgeColorIndex(int ColorIndex)
{
	SetColor(gColorList[ColorIndex].Edge);
}

void lcContext::SetEdgeColorIndexTinted(int ColorIndex, const lcVector4& Tint)
{
	SetColor(gColorList[ColorIndex].Edge * Tint);
}

lcVertexBuffer lcContext::CreateVertexBuffer(int Size, const void* Data)
{
	lcVertexBuffer VertexBuffer;

	if (gSupportsVertexBufferObject)
	{
		glGenBuffers(1, &VertexBuffer.Object);
		glBindBuffer(GL_ARRAY_BUFFER_ARB, VertexBuffer.Object);
		glBufferData(GL_ARRAY_BUFFER_ARB, Size, Data, GL_STATIC_DRAW_ARB);

		glBindBuffer(GL_ARRAY_BUFFER_ARB, 0); // context remove
		mVertexBufferObject = 0;
	}
	else
	{
		VertexBuffer.Pointer = malloc(Size);
		if (VertexBuffer.Pointer)
			memcpy(VertexBuffer.Pointer, Data, Size);
	}

	return VertexBuffer;
}

void lcContext::DestroyVertexBuffer(lcVertexBuffer& VertexBuffer)
{
	if (!VertexBuffer.IsValid())
		return;

	if (gSupportsVertexBufferObject)
	{
		if (mVertexBufferObject == VertexBuffer.Object)
		{
			glBindBuffer(GL_ARRAY_BUFFER_ARB, 0);
			mVertexBufferObject = 0;
		}

		glDeleteBuffers(1, &VertexBuffer.Object);
	}
	else
	{
		free(VertexBuffer.Pointer);
	}

	VertexBuffer.Pointer = nullptr;
}

lcIndexBuffer lcContext::CreateIndexBuffer(int Size, const void* Data)
{
	lcIndexBuffer IndexBuffer;

	if (gSupportsVertexBufferObject)
	{
		glGenBuffers(1, &IndexBuffer.Object);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, IndexBuffer.Object);
		glBufferData(GL_ELEMENT_ARRAY_BUFFER_ARB, Size, Data, GL_STATIC_DRAW_ARB);

		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, 0); // context remove
		mIndexBufferObject = 0;
	}
	else
	{
		IndexBuffer.Pointer = malloc(Size);
		if (IndexBuffer.Pointer)
			memcpy(IndexBuffer.Pointer, Data, Size);
	}

	return IndexBuffer;
}

void lcContext::DestroyIndexBuffer(lcIndexBuffer& IndexBuffer)
{
	if (!IndexBuffer.IsValid())
		return;

	if (gSupportsVertexBufferObject)
	{
		if (mIndexBufferObject == IndexBuffer.Object)
		{
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
			mIndexBufferObject = 0;
		}

		glDeleteBuffers(1, &IndexBuffer.Object);
	}
	else
	{
		free(IndexBuffer.Pointer);
	}

	IndexBuffer.Pointer = nullptr;
}

void lcContext::ClearVertexBuffer()
{
	mVertexBufferPointer = nullptr;
	mVertexBufferOffset = (char*)~0;

	if (mVertexBufferObject)
	{
		glBindBuffer(GL_ARRAY_BUFFER_ARB, 0);
		mVertexBufferObject = 0;
	}

	if (gSupportsShaderObjects)
	{
		SetVertexAttribPointer(lcProgramAttrib::Position, 3, GL_FLOAT, false, 0, nullptr);

		DisableVertexAttrib(lcProgramAttrib::Normal);
		SetVertexAttribPointer(lcProgramAttrib::Normal, 4, GL_BYTE, true, 0, nullptr);
		DisableVertexAttrib(lcProgramAttrib::TexCoord);
		SetVertexAttribPointer(lcProgramAttrib::TexCoord, 2, GL_FLOAT, false, 0, nullptr);
		DisableVertexAttrib(lcProgramAttrib::Color);
		SetVertexAttribPointer(lcProgramAttrib::Color, 4, GL_FLOAT, false, 0, nullptr);
	}
}

void lcContext::SetVertexBuffer(lcVertexBuffer VertexBuffer)
{
	if (gSupportsVertexBufferObject)
	{
		const GLuint VertexBufferObject = VertexBuffer.Object;
		mVertexBufferPointer = nullptr;

		if (VertexBufferObject != mVertexBufferObject)
		{
			glBindBuffer(GL_ARRAY_BUFFER_ARB, VertexBufferObject);
			mVertexBufferObject = VertexBufferObject;
			mVertexBufferOffset = (char*)~0;
		}
	}
	else
	{
		mVertexBufferPointer = (char*)VertexBuffer.Pointer;
		mVertexBufferOffset = (char*)~0;
	}
}

void lcContext::SetVertexBufferPointer(const void* VertexBuffer)
{
	if (gSupportsVertexBufferObject && mVertexBufferObject)
	{
		glBindBuffer(GL_ARRAY_BUFFER_ARB, 0);
		mVertexBufferObject = 0;
	}

	mVertexBufferPointer = (char*)VertexBuffer;
	mVertexBufferOffset = (char*)~0;
}

void lcContext::SetVertexAttribPointer(lcProgramAttrib Attrib, GLint Size, GLenum Type, GLboolean Normalized, GLsizei Stride, const void* Pointer)
{
	const int Index = static_cast<int>(Attrib);
	lcVertexAttribState& State = mVertexAttribState[Index];

	if (State.Size != Size || State.Type != Type || State.Normalized != Normalized || State.Stride != Stride || State.Pointer != Pointer || State.VertexBufferObject != mVertexBufferObject)
	{
		glVertexAttribPointer(Index, Size, Type, Normalized, Stride, Pointer);

		State.Size = Size;
		State.Type = Type;
		State.Normalized = Normalized;
		State.Stride = Stride;
		State.Pointer = Pointer;
		State.VertexBufferObject = mVertexBufferObject;
	}
}

void lcContext::EnableVertexAttrib(lcProgramAttrib Attrib)
{
	const int Index = static_cast<int>(Attrib);
	lcVertexAttribState& State = mVertexAttribState[Index];

	if (!State.Enabled)
	{
		glEnableVertexAttribArray(Index);
		State.Enabled = true;
	}
}

void lcContext::DisableVertexAttrib(lcProgramAttrib Attrib)
{
	const int Index = static_cast<int>(Attrib);
	lcVertexAttribState& State = mVertexAttribState[Index];

	if (State.Enabled)
	{
		glDisableVertexAttribArray(Index);
		State.Enabled = false;
	}
}

void lcContext::SetVertexFormatPosition(int PositionSize)
{
	const int VertexSize = PositionSize * sizeof(float);
	const char* VertexBufferPointer = mVertexBufferPointer;

	if (gSupportsShaderObjects)
	{
		SetVertexAttribPointer(lcProgramAttrib::Position, PositionSize, GL_FLOAT, false, VertexSize, VertexBufferPointer);
		DisableVertexAttrib(lcProgramAttrib::Normal);
		DisableVertexAttrib(lcProgramAttrib::TexCoord);
		DisableVertexAttrib(lcProgramAttrib::Color);
	}
}

void lcContext::SetVertexFormatConditional(int BufferOffset)
{
	constexpr int VertexSize = 12 * sizeof(float);
	const char* VertexBufferPointer = mVertexBufferPointer + BufferOffset;

	if (gSupportsShaderObjects)
	{
		SetVertexAttribPointer(lcProgramAttrib::ControlPoint1, 3, GL_FLOAT, false, VertexSize, VertexBufferPointer);
		EnableVertexAttrib(lcProgramAttrib::ControlPoint1);
		SetVertexAttribPointer(lcProgramAttrib::ControlPoint2, 3, GL_FLOAT, false, VertexSize, VertexBufferPointer + 3 * sizeof(float));
		EnableVertexAttrib(lcProgramAttrib::ControlPoint2);
		SetVertexAttribPointer(lcProgramAttrib::ControlPoint3, 3, GL_FLOAT, false, VertexSize, VertexBufferPointer + 6 * sizeof(float));
		EnableVertexAttrib(lcProgramAttrib::ControlPoint3);
		SetVertexAttribPointer(lcProgramAttrib::ControlPoint4, 3, GL_FLOAT, false, VertexSize, VertexBufferPointer + 9 * sizeof(float));
		EnableVertexAttrib(lcProgramAttrib::ControlPoint4);
	}
}

void lcContext::SetVertexFormat(int BufferOffset, int PositionSize, int NormalSize, int TexCoordSize, int ColorSize, bool EnableNormals)
{
	const int VertexSize = (PositionSize + TexCoordSize) * sizeof(float) + NormalSize * sizeof(quint32) + ColorSize;
	const char* VertexBufferPointer = mVertexBufferPointer + BufferOffset;

	if (gSupportsShaderObjects)
	{
		int Offset = 0;

		SetVertexAttribPointer(lcProgramAttrib::Position, PositionSize, GL_FLOAT, false, VertexSize, VertexBufferPointer);
		EnableVertexAttrib(lcProgramAttrib::Position);

		Offset += PositionSize * sizeof(float);

		if (NormalSize && EnableNormals)
		{
			SetVertexAttribPointer(lcProgramAttrib::Normal, 4, GL_BYTE, true, VertexSize, VertexBufferPointer + Offset);
			EnableVertexAttrib(lcProgramAttrib::Normal);
		}
		else
			DisableVertexAttrib(lcProgramAttrib::Normal);

		Offset += NormalSize * sizeof(quint32);

		if (TexCoordSize)
		{
			SetVertexAttribPointer(lcProgramAttrib::TexCoord, TexCoordSize, GL_FLOAT, false, VertexSize, VertexBufferPointer + Offset);
			EnableVertexAttrib(lcProgramAttrib::TexCoord);
		}
		else
			DisableVertexAttrib(lcProgramAttrib::TexCoord);

		Offset += TexCoordSize * sizeof(float);

		if (ColorSize)
		{
			SetVertexAttribPointer(lcProgramAttrib::Color, ColorSize, GL_UNSIGNED_BYTE, true, VertexSize, VertexBufferPointer + Offset);
			EnableVertexAttrib(lcProgramAttrib::Color);
		}
		else
			DisableVertexAttrib(lcProgramAttrib::Color);
	}
}

void lcContext::ClearIndexBuffer()
{
	if (mIndexBufferObject)
	{
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
		mIndexBufferObject = 0;
	}
}

void lcContext::SetIndexBuffer(lcIndexBuffer IndexBuffer)
{
	if (gSupportsVertexBufferObject)
	{
		const GLuint IndexBufferObject = IndexBuffer.Object;
		mIndexBufferPointer = nullptr;

		if (IndexBufferObject != mIndexBufferObject)
		{
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, IndexBufferObject);
			mIndexBufferObject = IndexBufferObject;
		}
	}
	else
	{
		mIndexBufferPointer = (char*)IndexBuffer.Pointer;
	}
}

void lcContext::SetIndexBufferPointer(const void* IndexBuffer)
{
	if (mIndexBufferObject)
	{
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
		mIndexBufferObject = 0;
	}

	mIndexBufferPointer = (char*)IndexBuffer;
}

void lcContext::BindMesh(const lcMesh* Mesh)
{
	const lcPiecesLibrary* const Library = lcGetPiecesLibrary();

	if (Mesh->mVertexCacheOffset != -1)
	{
		const GLuint VertexBufferObject = Library->mVertexBuffer.Object;
		const GLuint IndexBufferObject = Library->mIndexBuffer.Object;

		if (VertexBufferObject != mVertexBufferObject)
		{
			glBindBuffer(GL_ARRAY_BUFFER_ARB, VertexBufferObject);
			mVertexBufferObject = VertexBufferObject;
			mVertexBufferPointer = nullptr;
			mVertexBufferOffset = (char*)~0;
		}

		if (IndexBufferObject != mIndexBufferObject)
		{
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, IndexBufferObject);
			mIndexBufferObject = IndexBufferObject;
			mIndexBufferPointer = nullptr;
		}
	}
	else
	{
		if (mVertexBufferObject)
		{
			glBindBuffer(GL_ARRAY_BUFFER_ARB, 0);
			mVertexBufferObject = 0;
		}

		if (mIndexBufferObject)
		{
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER_ARB, 0);
			mIndexBufferObject = 0;
		}

		mVertexBufferPointer = (char*)Mesh->mVertexData;
		mIndexBufferPointer = (char*)Mesh->mIndexData;
		mVertexBufferOffset = (char*)~0;
	}
}

void lcContext::FlushState()
{
	if (gSupportsShaderObjects)
	{
		const lcProgram& Program = mPrograms[static_cast<int>(mMaterialType)];

		if (mWorldMatrixDirty || mViewMatrixDirty || mProjectionMatrixDirty)
		{
			if (mViewProjectionMatrixDirty)
			{
				mViewProjectionMatrix = lcMul(mViewMatrix, mProjectionMatrix);
				mViewProjectionMatrixDirty = false;
			}

			if (mWorldMatrixDirty)
			{
				if (Program.WorldMatrixLocation != -1)
					glUniformMatrix4fv(Program.WorldMatrixLocation, 1, false, mWorldMatrix.GetFloats());
			}

			if (mViewMatrixDirty)
			{
				const lcMatrix44 InverseViewMatrix = lcMatrix44AffineInverse(mViewMatrix);
				lcVector3 ViewPosition = lcMul30(-mViewMatrix.GetTranslation(), InverseViewMatrix);

				if (Program.LightPositionLocation != -1)
				{
					lcVector3 LightPosition = ViewPosition + lcMul30(lcVector3(300.0f, 300.0f, 0.0f), InverseViewMatrix);
					glUniform3fv(Program.LightPositionLocation, 1, LightPosition.GetFloats());
				}

				if (Program.EyePositionLocation != -1)
					glUniform3fv(Program.EyePositionLocation, 1, ViewPosition.GetFloats());
			}

			glUniformMatrix4fv(Program.WorldViewProjectionMatrixLocation, 1, false, lcMul(mWorldMatrix, mViewProjectionMatrix).GetFloats());
			mWorldMatrixDirty = false;
			mViewMatrixDirty = false;
			mProjectionMatrixDirty = false;
		}

		if (mColorDirty && Program.MaterialColorLocation != -1)
		{
			glUniform4fv(Program.MaterialColorLocation, 1, mColor.GetFloats());
			mColorDirty = false;
		}

		if (mHighlightParamsDirty && Program.HighlightParamsLocation != -1)
		{
			glUniform4fv(Program.HighlightParamsLocation, 4, mHighlightParams[0].GetFloats());
			mHighlightParamsDirty = false;
		}
	}
}

void lcContext::DrawPrimitives(GLenum Mode, GLint First, GLsizei Count)
{
	FlushState();
	glDrawArrays(Mode, First, Count);
}

void lcContext::DrawIndexedPrimitives(GLenum Mode, GLsizei Count, GLenum Type, int Offset)
{
	FlushState();
	glDrawElements(Mode, Count, Type, mIndexBufferPointer + Offset);
}
