#include "UniformBlock.h"
#include "../Config.h"
#include "../Textures.h"

static
const char * strTextureUniforms[UniformBlock::tuTotal] = {
	"uTexScale",
	"uTexOffset",
	"uCacheScale",
	"uCacheOffset",
	"uCacheShiftScale",
	"uCacheFrameBuffer"
};

static
const char * strColorUniforms[UniformBlock::cuTotal] = {
	"uFogColor",
	"uCenterColor",
	"uScaleColor",
	"uBlendColor",
	"uEnvColor",
	"uPrimColor",
	"uPrimLod",
	"uK4",
	"uK5"
};

static
const char * strLightUniforms[UniformBlock::luTotal] = {
	"uLightDirection",
	"uLightColor"
};

UniformBlock::UniformBlock() : m_currentBuffer(0), m_renderer(video().getRender().getRenderer())
{
}

UniformBlock::~UniformBlock()
{
}

void UniformBlock::_initTextureBuffer(GLuint _program)
{
	const GLint blockSize = m_textureBlock.initBuffer(_program, "TextureBlock", strTextureUniforms);
	if (blockSize == 0)
		return;
	m_textureBlockData.resize(blockSize);
	GLbyte * pData = m_textureBlockData.data();
	memset(pData, 0, blockSize);
	glBindBuffer(GL_UNIFORM_BUFFER, m_textureBlock.m_buffer);
	glBufferData(GL_UNIFORM_BUFFER, blockSize, 0, GL_DYNAMIC_DRAW);
	glBindBufferBase(GL_UNIFORM_BUFFER, m_textureBlock.m_blockBindingPoint, m_textureBlock.m_buffer);
	updateTextureParameters();
}

void UniformBlock::_initColorsBuffer(GLuint _program)
{
	const GLint blockSize = m_colorsBlock.initBuffer(_program, "ColorsBlock", strColorUniforms);
	if (blockSize == 0)
		return;
	m_colorsBlockData.resize(blockSize);
	GLbyte * pData = m_colorsBlockData.data();
	memset(pData, 0, blockSize);
	memcpy(pData + m_colorsBlock.m_offsets[cuFogColor], &gDP.fogColor.r, sizeof(float)* 4);
	memcpy(pData + m_colorsBlock.m_offsets[cuCenterColor], &gDP.key.center.r, sizeof(float)* 4);
	memcpy(pData + m_colorsBlock.m_offsets[cuScaleColor], &gDP.key.scale.r, sizeof(float)* 4);
	memcpy(pData + m_colorsBlock.m_offsets[cuEnvColor], &gDP.envColor.r, sizeof(float)* 4);
	memcpy(pData + m_colorsBlock.m_offsets[cuPrimColor], &gDP.primColor.r, sizeof(float)* 4);
	*(float*)(pData + m_colorsBlock.m_offsets[cuPrimLod]) = gDP.primColor.l;
	*(float*)(pData + m_colorsBlock.m_offsets[cuK4]) = gDP.convert.k4*0.0039215689f;
	*(float*)(pData + m_colorsBlock.m_offsets[cuK5]) = gDP.convert.k5*0.0039215689f;

	glBindBuffer(GL_UNIFORM_BUFFER, m_colorsBlock.m_buffer);
	glBufferData(GL_UNIFORM_BUFFER, blockSize, pData, GL_DYNAMIC_DRAW);
	glBindBufferBase(GL_UNIFORM_BUFFER, m_colorsBlock.m_blockBindingPoint, m_colorsBlock.m_buffer);
	m_currentBuffer = m_colorsBlock.m_buffer;
}

void UniformBlock::_initLightBuffer(GLuint _program)
{
	const GLint blockSize = m_lightBlock.initBuffer(_program, "LightBlock", strLightUniforms);
	if (blockSize == 0)
		return;
	m_lightBlockData.resize(blockSize);
	GLbyte * pData = m_lightBlockData.data();
	memset(pData, 0, blockSize);
	glBindBuffer(GL_UNIFORM_BUFFER, m_lightBlock.m_buffer);
	glBufferData(GL_UNIFORM_BUFFER, blockSize, 0, GL_DYNAMIC_DRAW);
	glBindBufferBase(GL_UNIFORM_BUFFER, m_lightBlock.m_blockBindingPoint, m_lightBlock.m_buffer);
	updateLightParameters();
}

bool UniformBlock::_isDataChanged(void * _pBuffer, const void * _pData, uint32_t _dataSize)
{
	uint32_t * pSrc = (uint32_t*)_pData;
	uint32_t * pDst = (uint32_t*)_pBuffer;
	uint32_t cnt = _dataSize / 4;
	for (uint32_t i = 0; i < cnt; ++i) {
		if (pSrc[i] != pDst[i]) {
			memcpy(_pBuffer, _pData, _dataSize);
			return true;
		}
	}
	return false;
}

void UniformBlock::bindWithShaderCombiner(ShaderCombiner * _pCombiner)
{
	const GLuint program = _pCombiner->m_program;
	if (_pCombiner->usesTexture()) {
		if (m_textureBlock.m_buffer == 0)
			_initTextureBuffer(program);
		else {
			const GLint blockIndex = glGetUniformBlockIndex(program, "TextureBlock");
			if (blockIndex != GL_INVALID_INDEX)
				glUniformBlockBinding(program, blockIndex, m_textureBlock.m_blockBindingPoint);
		}
	}

	if (m_colorsBlock.m_buffer == 0)
		_initColorsBuffer(program);
	else {
		const GLint blockIndex = glGetUniformBlockIndex(program, "ColorsBlock");
		if (blockIndex != GL_INVALID_INDEX)
			glUniformBlockBinding(program, blockIndex, m_colorsBlock.m_blockBindingPoint);
	}

	if (_pCombiner->usesShadeColor() && config.generalEmulation.enableHWLighting != 0) {
		if (m_lightBlock.m_buffer == 0)
			_initLightBuffer(program);
		else {
			const GLint blockIndex = glGetUniformBlockIndex(program, "LightBlock");
			if (blockIndex != GL_INVALID_INDEX)
				glUniformBlockBinding(program, blockIndex, m_lightBlock.m_blockBindingPoint);
		}
	}
}

void UniformBlock::setColorData(ColorUniforms _index, uint32_t _dataSize, const void * _data)
{
	if (m_colorsBlock.m_buffer == 0)
		return;
	if (!_isDataChanged(m_colorsBlockData.data() + m_colorsBlock.m_offsets[_index], _data, _dataSize))
		return;

	if (m_currentBuffer != m_colorsBlock.m_buffer) {
		m_currentBuffer = m_colorsBlock.m_buffer;
		glBindBuffer(GL_UNIFORM_BUFFER, m_colorsBlock.m_buffer);
	}

	if (m_renderer != OGLRender::glrAdreno)
		glBufferSubData(GL_UNIFORM_BUFFER, m_colorsBlock.m_offsets[_index], _dataSize, _data);
	else
		glBufferData(GL_UNIFORM_BUFFER, m_colorsBlockData.size(), m_colorsBlockData.data(), GL_STATIC_DRAW);
}

void UniformBlock::updateTextureParameters()
{
	if (m_textureBlock.m_buffer == 0)
		return;

	GLbyte * pData = m_textureBlockData.data();
	float texScale[4] = { gSP.texture.scales, gSP.texture.scalet, 0, 0 };
	memcpy(pData + m_textureBlock.m_offsets[tuTexScale], texScale, m_textureBlock.m_offsets[tuTexOffset] - m_textureBlock.m_offsets[tuTexScale]);

	float texOffset[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
	if (gSP.textureTile[0] != NULL) {
		if (gSP.textureTile[0]->textureMode != TEXTUREMODE_BGIMAGE && gSP.textureTile[0]->textureMode != TEXTUREMODE_FRAMEBUFFER_BG) {
			texOffset[0] = gSP.textureTile[0]->fuls;
			texOffset[1] = gSP.textureTile[0]->fult;
			FrameBuffer * pBuffer = gSP.textureTile[0]->frameBuffer;
			if (pBuffer != NULL) {
				if (gSP.textureTile[0]->masks > 0 && gSP.textureTile[0]->clamps == 0)
					texOffset[0] = float(gSP.textureTile[0]->uls % (1 << gSP.textureTile[0]->masks));
				if (gSP.textureTile[0]->maskt > 0 && gSP.textureTile[0]->clampt == 0)
					texOffset[1] = float(gSP.textureTile[0]->ult % (1 << gSP.textureTile[0]->maskt));
			}
		}
	}
	if (gSP.textureTile[1] != 0) {
		texOffset[4] = gSP.textureTile[1]->fuls;
		texOffset[5] = gSP.textureTile[1]->fult;
		FrameBuffer * pBuffer = gSP.textureTile[1]->frameBuffer;
		if (pBuffer != NULL) {
			if (gSP.textureTile[1]->masks > 0 && gSP.textureTile[1]->clamps == 0)
				texOffset[4] = float(gSP.textureTile[1]->uls % (1 << gSP.textureTile[1]->masks));
			if (gSP.textureTile[1]->maskt > 0 && gSP.textureTile[1]->clampt == 0)
				texOffset[5] = float(gSP.textureTile[1]->ult % (1 << gSP.textureTile[1]->maskt));
		}
	}
	memcpy(pData + m_textureBlock.m_offsets[tuTexOffset], texOffset, m_textureBlock.m_offsets[tuCacheScale] - m_textureBlock.m_offsets[tuTexOffset]);

	float texCacheScale[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
	float texCacheOffset[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
	float texCacheShiftScale[8] = { 0, 0, 0, 0, 0, 0, 0, 0 };
	GLint texCacheFrameBuffer[4] = { 0, 0, 0, 0 };
	TextureCache & cache = textureCache();
	if (cache.current[0]) {
		texCacheScale[0] = cache.current[0]->scaleS;
		texCacheScale[1] = cache.current[0]->scaleT;
		texCacheOffset[0] = cache.current[0]->offsetS;
		texCacheOffset[1] = cache.current[0]->offsetT;

		float shiftScaleS = 1.0f;
		float shiftScaleT = 1.0f;
		getTextureShiftScale(0, cache, shiftScaleS, shiftScaleT);
		texCacheShiftScale[0] = shiftScaleS;
		texCacheShiftScale[1] = shiftScaleT;
		texCacheFrameBuffer[0] = cache.current[0]->frameBufferTexture;
	}
	if (cache.current[1]) {
		texCacheScale[4] = cache.current[1]->scaleS;
		texCacheScale[5] = cache.current[1]->scaleT;
		texCacheOffset[4] = cache.current[1]->offsetS;
		texCacheOffset[5] = cache.current[1]->offsetT;

		float shiftScaleS = 1.0f;
		float shiftScaleT = 1.0f;
		getTextureShiftScale(1, cache, shiftScaleS, shiftScaleT);
		texCacheShiftScale[4] = shiftScaleS;
		texCacheShiftScale[5] = shiftScaleT;
		texCacheFrameBuffer[1] = cache.current[1]->frameBufferTexture;
	}
	memcpy(pData + m_textureBlock.m_offsets[tuCacheScale], texCacheScale, m_textureBlock.m_offsets[tuCacheOffset] - m_textureBlock.m_offsets[tuCacheScale]);
	memcpy(pData + m_textureBlock.m_offsets[tuCacheOffset], texCacheOffset, m_textureBlock.m_offsets[tuCacheShiftScale] - m_textureBlock.m_offsets[tuCacheOffset]);
	memcpy(pData + m_textureBlock.m_offsets[tuCacheShiftScale], texCacheShiftScale, m_textureBlock.m_offsets[tuCacheFrameBuffer] - m_textureBlock.m_offsets[tuCacheShiftScale]);
	memcpy(pData + m_textureBlock.m_offsets[tuCacheFrameBuffer], texCacheFrameBuffer, m_textureBlockData.size() - m_textureBlock.m_offsets[tuCacheFrameBuffer]);

	if (m_currentBuffer != m_textureBlock.m_buffer) {
		m_currentBuffer = m_textureBlock.m_buffer;
		glBindBuffer(GL_UNIFORM_BUFFER, m_textureBlock.m_buffer);
	}

	if (m_renderer != OGLRender::glrAdreno)
		glBufferSubData(GL_UNIFORM_BUFFER, m_textureBlock.m_offsets[tuTexScale], m_textureBlockData.size(), pData);
	else
		glBufferData(GL_UNIFORM_BUFFER, m_textureBlockData.size(), m_textureBlockData.data(), GL_STATIC_DRAW);
}

void UniformBlock::updateLightParameters()
{
	if (m_lightBlock.m_buffer == 0)
		return;

	GLbyte * pData = m_lightBlockData.data();
	const uint32_t arraySize = m_lightBlock.m_offsets[luLightColor] / 8;
	for (int32_t i = 0; i <= gSP.numLights; ++i) {
		memcpy(pData + m_lightBlock.m_offsets[luLightDirection] + arraySize*i, &gSP.lights[i].x, arraySize);
		memcpy(pData + m_lightBlock.m_offsets[luLightColor] + arraySize*i, &gSP.lights[i].r, arraySize);
	}
	if (m_currentBuffer != m_lightBlock.m_buffer) {
		m_currentBuffer = m_lightBlock.m_buffer;
		glBindBuffer(GL_UNIFORM_BUFFER, m_lightBlock.m_buffer);
	}

	if (m_renderer != OGLRender::glrAdreno)
		glBufferSubData(GL_UNIFORM_BUFFER, m_lightBlock.m_offsets[luLightDirection], m_lightBlockData.size(), pData);
	else
		glBufferData(GL_UNIFORM_BUFFER, m_lightBlockData.size(), m_lightBlockData.data(), GL_STATIC_DRAW);
}

UniformCollection * createUniformCollection()
{
	return new UniformBlock();
}
