#include "SimpleMotionRenderer.h"

#include "GLExtensions.h"

#include <algorithm>
#include <array>
#include <cmath>

namespace
{
constexpr GLuint kInputTextureUnit = 0;

const char* kTextureVertexShader = R"GLSL(
#version 430 core
out vec2 vTexCoord;
void main()
{
	vec2 positions[3] = vec2[3](
		vec2(-1.0, -1.0),
		vec2( 3.0, -1.0),
		vec2(-1.0,  3.0));
	vec2 texCoords[3] = vec2[3](
		vec2(0.0, 0.0),
		vec2(2.0, 0.0),
		vec2(0.0, 2.0));
	gl_Position = vec4(positions[gl_VertexID], 0.0, 1.0);
	vTexCoord = texCoords[gl_VertexID];
}
)GLSL";

const char* kTextureFragmentShader = R"GLSL(
#version 430 core
layout(binding = 0) uniform sampler2D uInputTexture;
in vec2 vTexCoord;
out vec4 fragColor;
void main()
{
	fragColor = texture(uInputTexture, clamp(vTexCoord, vec2(0.0), vec2(1.0)));
}
)GLSL";

const char* kPatternFragmentShader = R"GLSL(
#version 430 core
uniform vec2 uResolution;
uniform float uFrameIndex;
in vec2 vTexCoord;
out vec4 fragColor;

vec3 hexColor(float r, float g, float b)
{
	return vec3(r, g, b) / 255.0;
}

vec3 smpteTop(float x)
{
	if (x < 240.0) return hexColor(102.0, 102.0, 102.0);
	if (x < 445.0) return hexColor(191.0, 191.0, 191.0);
	if (x < 651.0) return hexColor(191.0, 191.0, 0.0);
	if (x < 857.0) return hexColor(0.0, 191.0, 191.0);
	if (x < 1063.0) return hexColor(0.0, 191.0, 0.0);
	if (x < 1269.0) return hexColor(191.0, 0.0, 191.0);
	if (x < 1475.0) return hexColor(191.0, 0.0, 0.0);
	if (x < 1680.0) return hexColor(0.0, 0.0, 191.0);
	return hexColor(102.0, 102.0, 102.0);
}

vec3 smpteMiddleA(float x)
{
	if (x < 240.0) return hexColor(0.0, 255.0, 255.0);
	if (x < 445.0) return hexColor(0.0, 63.0, 105.0);
	if (x < 1680.0) return hexColor(191.0, 191.0, 191.0);
	return hexColor(0.0, 0.0, 255.0);
}

vec3 smpteMiddleB(float x)
{
	if (x < 240.0) return hexColor(255.0, 255.0, 0.0);
	if (x < 445.0) return hexColor(65.0, 0.0, 119.0);
	if (x < 1475.0)
	{
		float ramp = clamp((x - 445.0) / (1475.0 - 445.0), 0.0, 1.0);
		return vec3(ramp);
	}
	if (x < 1680.0) return vec3(1.0);
	return hexColor(255.0, 0.0, 0.0);
}

vec3 smpteBottom(float x)
{
	if (x < 240.0) return hexColor(38.0, 38.0, 38.0);
	if (x < 549.0) return vec3(0.0);
	if (x < 960.0) return vec3(1.0);
	if (x < 1268.0) return vec3(0.0);
	if (x < 1337.0) return hexColor(5.0, 5.0, 5.0);
	if (x < 1405.0) return vec3(0.0);
	if (x < 1474.0) return hexColor(10.0, 10.0, 10.0);
	if (x < 1680.0) return vec3(0.0);
	return hexColor(38.0, 38.0, 38.0);
}

vec3 smpteColor(vec2 uv)
{
	vec2 pixel = vec2(clamp(uv.x, 0.0, 1.0), 1.0 - clamp(uv.y, 0.0, 1.0)) * vec2(1920.0, 1080.0);
	if (pixel.y < 630.0) return smpteTop(pixel.x);
	if (pixel.y < 720.0) return smpteMiddleA(pixel.x);
	if (pixel.y < 810.0) return smpteMiddleB(pixel.x);
	return smpteBottom(pixel.x);
}

void main()
{
	vec2 uv = clamp(vTexCoord, vec2(0.0), vec2(1.0));
	vec3 color = smpteColor(uv);

	float t = uFrameIndex / 60.0;
	vec2 cubeSize = vec2(0.16, 0.20);
	vec2 cubeMin = vec2(
		(0.5 + 0.5 * sin(t * 1.7)) * (1.0 - cubeSize.x),
		(0.5 + 0.5 * sin(t * 1.1 + 0.8)) * (1.0 - cubeSize.y));
	vec2 cubeMax = cubeMin + cubeSize;
	bool insideCube = uv.x >= cubeMin.x && uv.x <= cubeMax.x && uv.y >= cubeMin.y && uv.y <= cubeMax.y;
	if (insideCube)
	{
		vec2 local = (uv - cubeMin) / cubeSize;
		vec3 cubeColor = vec3(1.0, 0.74 + 0.18 * sin(t * 2.1), 0.08);
		float edge = step(local.x, 0.04) + step(local.y, 0.04) + step(0.96, local.x) + step(0.96, local.y);
		color = edge > 0.0 ? vec3(1.0) : cubeColor;
	}

	fragColor = vec4(color, 1.0);
}
)GLSL";
}

bool SimpleMotionRenderer::InitializeGl(unsigned width, unsigned height)
{
	mWidth = width;
	mHeight = height;
	return mWidth > 0 && mHeight > 0;
}

void SimpleMotionRenderer::RenderFrame(uint64_t frameIndex)
{
	if (!EnsurePatternProgram())
	{
		glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
		glDisable(GL_SCISSOR_TEST);
		glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);
		return;
	}

	glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
	glDisable(GL_SCISSOR_TEST);
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_BLEND);
	glUseProgram(mPatternProgram);
	const GLint resolutionLocation = glGetUniformLocation(mPatternProgram, "uResolution");
	if (resolutionLocation >= 0)
		glUniform2f(resolutionLocation, static_cast<float>(mWidth), static_cast<float>(mHeight));
	const GLint frameIndexLocation = glGetUniformLocation(mPatternProgram, "uFrameIndex");
	if (frameIndexLocation >= 0)
		glUniform1f(frameIndexLocation, static_cast<float>(frameIndex));
	glBindVertexArray(mPatternVertexArray);
	glDrawArrays(GL_TRIANGLES, 0, 3);
	glBindVertexArray(0);
	glUseProgram(0);
}

void SimpleMotionRenderer::RenderTexture(GLuint texture)
{
	if (texture == 0 || !EnsureTextureProgram())
	{
		RenderFrame(0);
		return;
	}

	glViewport(0, 0, static_cast<GLsizei>(mWidth), static_cast<GLsizei>(mHeight));
	glDisable(GL_SCISSOR_TEST);
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_BLEND);
	glActiveTexture(GL_TEXTURE0 + kInputTextureUnit);
	glBindTexture(GL_TEXTURE_2D, texture);
	glUseProgram(mTextureProgram);
	glBindVertexArray(mTextureVertexArray);
	glDrawArrays(GL_TRIANGLES, 0, 3);
	glBindVertexArray(0);
	glUseProgram(0);
	glBindTexture(GL_TEXTURE_2D, 0);
	glActiveTexture(GL_TEXTURE0);
}

void SimpleMotionRenderer::ShutdownGl()
{
	DestroyPatternProgram();
	DestroyTextureProgram();
	mWidth = 0;
	mHeight = 0;
}

bool SimpleMotionRenderer::EnsurePatternProgram()
{
	if (mPatternProgram != 0)
		return true;

	if (!CompileShader(GL_VERTEX_SHADER, kTextureVertexShader, mPatternVertexShader))
		return false;
	if (!CompileShader(GL_FRAGMENT_SHADER, kPatternFragmentShader, mPatternFragmentShader))
	{
		DestroyPatternProgram();
		return false;
	}

	mPatternProgram = glCreateProgram();
	glAttachShader(mPatternProgram, mPatternVertexShader);
	glAttachShader(mPatternProgram, mPatternFragmentShader);
	glLinkProgram(mPatternProgram);

	GLint linkResult = GL_FALSE;
	glGetProgramiv(mPatternProgram, GL_LINK_STATUS, &linkResult);
	if (linkResult == GL_FALSE)
	{
		DestroyPatternProgram();
		return false;
	}

	glGenVertexArrays(1, &mPatternVertexArray);
	return mPatternVertexArray != 0;
}

bool SimpleMotionRenderer::EnsureTextureProgram()
{
	if (mTextureProgram != 0)
		return true;

	if (!CompileShader(GL_VERTEX_SHADER, kTextureVertexShader, mTextureVertexShader))
		return false;
	if (!CompileShader(GL_FRAGMENT_SHADER, kTextureFragmentShader, mTextureFragmentShader))
	{
		DestroyTextureProgram();
		return false;
	}

	mTextureProgram = glCreateProgram();
	glAttachShader(mTextureProgram, mTextureVertexShader);
	glAttachShader(mTextureProgram, mTextureFragmentShader);
	glLinkProgram(mTextureProgram);

	GLint linkResult = GL_FALSE;
	glGetProgramiv(mTextureProgram, GL_LINK_STATUS, &linkResult);
	if (linkResult == GL_FALSE)
	{
		DestroyTextureProgram();
		return false;
	}

	glUseProgram(mTextureProgram);
	const GLint inputLocation = glGetUniformLocation(mTextureProgram, "uInputTexture");
	if (inputLocation >= 0)
		glUniform1i(inputLocation, static_cast<GLint>(kInputTextureUnit));
	glUseProgram(0);

	glGenVertexArrays(1, &mTextureVertexArray);
	return mTextureVertexArray != 0;
}

void SimpleMotionRenderer::DestroyTextureProgram()
{
	if (mTextureProgram != 0)
		glDeleteProgram(mTextureProgram);
	if (mTextureVertexShader != 0)
		glDeleteShader(mTextureVertexShader);
	if (mTextureFragmentShader != 0)
		glDeleteShader(mTextureFragmentShader);
	if (mTextureVertexArray != 0)
		glDeleteVertexArrays(1, &mTextureVertexArray);
	mTextureProgram = 0;
	mTextureVertexShader = 0;
	mTextureFragmentShader = 0;
	mTextureVertexArray = 0;
}

void SimpleMotionRenderer::DestroyPatternProgram()
{
	if (mPatternProgram != 0)
		glDeleteProgram(mPatternProgram);
	if (mPatternVertexShader != 0)
		glDeleteShader(mPatternVertexShader);
	if (mPatternFragmentShader != 0)
		glDeleteShader(mPatternFragmentShader);
	if (mPatternVertexArray != 0)
		glDeleteVertexArrays(1, &mPatternVertexArray);
	mPatternProgram = 0;
	mPatternVertexShader = 0;
	mPatternFragmentShader = 0;
	mPatternVertexArray = 0;
}

bool SimpleMotionRenderer::CompileShader(GLenum shaderType, const char* source, GLuint& shader)
{
	shader = glCreateShader(shaderType);
	glShaderSource(shader, 1, &source, nullptr);
	glCompileShader(shader);

	GLint compileResult = GL_FALSE;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &compileResult);
	if (compileResult != GL_FALSE)
		return true;

	glDeleteShader(shader);
	shader = 0;
	return false;
}
