/* Theorafile - Ogg Theora Video Decoder Library
 *
 * Copyright (c) 2017-2024 Ethan Lee.
 * Based on TheoraPlay, Copyright (c) 2011-2016 Ryan C. Gordon.
 *
 * This software is provided 'as-is', without any express or implied warranty.
 * In no event will the authors be held liable for any damages arising from
 * the use of this software.
 *
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 *
 * 1. The origin of this software must not be misrepresented; you must not
 * claim that you wrote the original software. If you use this software in a
 * product, an acknowledgment in the product documentation would be
 * appreciated but is not required.
 *
 * 2. Altered source versions must be plainly marked as such, and must not be
 * misrepresented as being the original software.
 *
 * 3. This notice may not be removed or altered from any source distribution.
 *
 * Ethan "flibitijibibo" Lee <flibitijibibo@flibitijibibo.com>
 *
 */

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_opengl.h>
#include "theorafile.h"

/* GL Function typedefs */
#define GL_PROC(ret, func, parms) \
	typedef ret (GLAPIENTRY *glfntype_##func) parms;
#include "glfuncs.h"
#undef GL_PROC
/* GL Function declarations */
#define GL_PROC(ret, func, parms) \
	glfntype_##func INTERNAL_##func;
#include "glfuncs.h"
#undef GL_PROC

static const GLchar *GLVert =
	"#version 110\n"
	"attribute vec2 pos;\n"
	"attribute vec2 tex;\n"
	"void main() {\n"
		"gl_Position = vec4(pos.xy, 0.0, 1.0);\n"
		"gl_TexCoord[0].xy = tex;\n"
	"}\n";

/* This shader was originally from SDL 1.3!
 * It has been modified to use color conversion for HD formats:
 * http://www.equasys.de/colorconversion.html
 * http://www.equasys.de/colorformat.html
 */
static const GLchar *GLFrag =
	"#version 110\n"
	"uniform sampler2D samp0;\n"
	"uniform sampler2D samp1;\n"
	"uniform sampler2D samp2;\n"
	"const vec3 offset = vec3(-0.0625, -0.5, -0.5);\n"
	"const vec3 Rcoeff = vec3(1.164,  0.000,  1.793);\n"
	"const vec3 Gcoeff = vec3(1.164, -0.213, -0.533);\n"
	"const vec3 Bcoeff = vec3(1.164,  2.112,  0.000);\n"
	"void main() {\n"
	"	vec2 tcoord;\n"
	"	vec3 yuv, rgb;\n"
	"	tcoord = gl_TexCoord[0].xy;\n"
	"	yuv.x = texture2D(samp0, tcoord).r;\n"
	"	yuv.y = texture2D(samp1, tcoord).r;\n"
	"	yuv.z = texture2D(samp2, tcoord).r;\n"
	"	yuv += offset;\n"
	"	rgb.r = dot(yuv, Rcoeff);\n"
	"	rgb.g = dot(yuv, Gcoeff);\n"
	"	rgb.b = dot(yuv, Bcoeff);\n"
	"	gl_FragColor = vec4(rgb, 1.0);\n"
	"}\n";

static void SDLCALL AudioCallback(
	void *userdata,
	SDL_AudioStream *stream,
	int additional_amount,
	int total_amount
) {
	float buffer[additional_amount];
	int read = tf_readaudio(
		(OggTheora_File*) userdata,
		(float*) buffer,
		SDL_arraysize(buffer)
	);
	SDL_PutAudioStreamData(
		stream,
		buffer,
		sizeof(buffer)
	);
}

int main(int argc, char **argv)
{
	/* SDL variables */
	SDL_Window *window;
	SDL_GLContext context;
	SDL_AudioStream *stream;
	SDL_AudioSpec spec;
	SDL_Event evt;
	Uint64 timeStart, timeCurrent;
	Uint8 run = 1;

	/* Theorafile variables */
	OggTheora_File fileIn;
	int width, height, uvWidth, uvHeight;
	int channels, samplerate;
	double fps;
	th_pixel_fmt fmt;
	int curframe = 0, thisframe, newframe;
	char *frame = NULL;

	/* OpenGL variables */
	GLuint yuvTextures[3];
	GLuint vertex = 0;
	GLuint fragment = 0;
	GLuint program = 0;
	GLint shaderlen = 0;

	/* Vertex client arrays */
	static struct { float pos[2]; float tex[2]; } verts[4] = {
		{ { -1.0f,  1.0f }, { 0.0f, 0.0f } },
		{ {  1.0f,  1.0f }, { 1.0f, 0.0f } },
		{ { -1.0f, -1.0f }, { 0.0f, 1.0f } },
		{ {  1.0f, -1.0f }, { 1.0f, 1.0f } }
	};

	/* We need a file name! */
	if (argc < 2 || argc > 2)
	{
		SDL_Log("Need a file name!\n");
		return 1;
	}

	/* Open the Theora file */
	if (tf_fopen(argv[1], &fileIn) < 0)
	{
		SDL_Log("Failed to open file.\n");
		return 1;
	}

	/* This is a video test, people! */
	if (!tf_hasvideo(&fileIn))
	{
		SDL_Log("No video!\n");
		return 1;
	}

	/* Get the video metadata, allocate first frame */
	tf_videoinfo(&fileIn, &width, &height, &fps, &fmt);
	if (fmt == TH_PF_420)
	{
		/* Subsampled in both dimensions */
		uvWidth = width / 2;
		uvHeight = height / 2;
	}
	else if (fmt == TH_PF_422)
	{
		/* Subsampled only horizontally */
		uvWidth = width / 2;
		uvHeight = height;
	}
	else
	{
		/* No subsampling at all... */
		uvWidth = width;
		uvHeight = height;
	}
	frame = (char*) SDL_malloc(width * height + uvWidth * uvHeight * 2);
	while (!tf_readvideo(&fileIn, frame, 1));

	/* Create window (and audio device, if applicable) */
	SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
	window = SDL_CreateWindow(
		"Theorafile Test",
		width,
		height,
		SDL_WINDOW_OPENGL
	);
	context = SDL_GL_CreateContext(window);
	SDL_GL_SetSwapInterval(1);

	/* GL function loading */
	#define GL_PROC(ret, func, parms) \
		INTERNAL_##func = (glfntype_##func) SDL_GL_GetProcAddress(#func);
	#include "glfuncs.h"
	#undef GL_PROC

	/* Remap GL function names to internal entry points */
	#include "glmacros.h"

	if (tf_hasaudio(&fileIn))
	{
		/* Get the audio metadata, allocate queue */
		tf_audioinfo(&fileIn, &channels, &samplerate);
		SDL_zero(spec);
		spec.freq = samplerate;
		spec.format = SDL_AUDIO_F32;
		spec.channels = channels;
		stream = SDL_OpenAudioDeviceStream(
			SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK,
			&spec,
			AudioCallback,
			&fileIn
		);
		SDL_ResumeAudioStreamDevice(stream);
	}

	/* Initial GL state */
	glDepthMask(GL_FALSE);
	glDisable(GL_DEPTH_TEST);
	glDisable(GL_ALPHA_TEST);
	glDisable(GL_BLEND);
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

	/* YUV buffers */
	glGenTextures(3, yuvTextures);
	#define GEN_TEXTURE(index, w, h, ptr) \
		glActiveTexture(GL_TEXTURE0 + index); \
		glBindTexture(GL_TEXTURE_2D, yuvTextures[index]); \
		glTexImage2D( \
			GL_TEXTURE_2D, \
			0, \
			GL_LUMINANCE8, \
			w, \
			h, \
			0, \
			GL_LUMINANCE, \
			GL_UNSIGNED_BYTE, \
			ptr \
		); \
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); \
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); \
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); \
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); \
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0); \
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
	GEN_TEXTURE(
		0,
		width,
		height,
		frame
	)
	GEN_TEXTURE(
		1,
		uvWidth,
		uvHeight,
		frame + (width * height)
	)
	GEN_TEXTURE(
		2,
		uvWidth,
		uvHeight,
		frame + (width * height) + (uvWidth * uvHeight)
	)
	#undef GEN_TEXTURE

	/* Vertex shader... */
	shaderlen = (GLint) SDL_strlen(GLVert);
	vertex = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(vertex, 1, &GLVert, &shaderlen);
	glCompileShader(vertex);

	/* Fragment shader... */
	shaderlen = (GLint) SDL_strlen(GLFrag);
	fragment = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(fragment, 1, &GLFrag, &shaderlen);
	glCompileShader(fragment);

	/* Program object... */
	program = glCreateProgram();
	glAttachShader(program, vertex);
	glAttachShader(program, fragment);
	glBindAttribLocation(program, 0, "pos");
	glBindAttribLocation(program, 1, "tex");
	glLinkProgram(program);
	glDeleteShader(vertex);
	glDeleteShader(fragment);

	/* ... Finally. */
	glUseProgram(program);
	glUniform1i(glGetUniformLocation(program, "samp0"), 0);
	glUniform1i(glGetUniformLocation(program, "samp1"), 1);
	glUniform1i(glGetUniformLocation(program, "samp2"), 2);

	/* Vertex buffers */
	glVertexAttribPointer(0, 2, GL_FLOAT, 0, sizeof (verts[0]), &verts[0].pos[0]);
	glVertexAttribPointer(1, 2, GL_FLOAT, 0, sizeof (verts[0]), &verts[0].tex[0]);
	glEnableVertexAttribArray(0);
	glEnableVertexAttribArray(1);

	timeStart = SDL_GetPerformanceCounter();
	while (run)
	{
		while (SDL_PollEvent(&evt))
		{
			if (evt.type == SDL_EVENT_QUIT)
			{
				run = 0;
			}
			else if (evt.type == SDL_EVENT_KEY_DOWN)
			{
				if (evt.key.key == SDLK_SPACE)
				{
					/* Slowdown simulator */
					SDL_Delay(1000);
				}
				else if (	evt.key.key >= SDLK_1 &&
						evt.key.key <= SDLK_9	)
				{
					SDL_LockAudioStream(stream);
					tf_setaudiotrack(
						&fileIn,
						evt.key.key - SDLK_1
					);
					SDL_UnlockAudioStream(stream);
				}
			}
		}

		/* Loop this video! */
		SDL_LockAudioStream(stream);
		if (tf_eos(&fileIn))
		{
			tf_reset(&fileIn);
		}
		SDL_UnlockAudioStream(stream);

		/* Based on when we started, what frame should we be on? */
		timeCurrent = SDL_GetPerformanceCounter();
		thisframe = (int) (
			(double) (timeCurrent - timeStart) /
			(double) SDL_GetPerformanceFrequency() *
			fps
		);
		if (thisframe > curframe)
		{
			/* Keep reading frames until we're caught up */
			SDL_LockAudioStream(stream);
			newframe = tf_readvideo(&fileIn, frame, thisframe - curframe);
			SDL_UnlockAudioStream(stream);
			curframe = thisframe;

			/* Only update the textures if we need to! */
			if (newframe)
			{
				glActiveTexture(GL_TEXTURE0);
				glTexSubImage2D(
					GL_TEXTURE_2D,
					0,
					0,
					0,
					width,
					height,
					GL_LUMINANCE,
					GL_UNSIGNED_BYTE,
					frame
				);
				glActiveTexture(GL_TEXTURE1);
				glTexSubImage2D(
					GL_TEXTURE_2D,
					0,
					0,
					0,
					uvWidth,
					uvHeight,
					GL_LUMINANCE,
					GL_UNSIGNED_BYTE,
					frame + (width * height)
				);
				glActiveTexture(GL_TEXTURE2);
				glTexSubImage2D(
					GL_TEXTURE_2D,
					0,
					0,
					0,
					uvWidth,
					uvHeight,
					GL_LUMINANCE,
					GL_UNSIGNED_BYTE,
					frame + (width * height) + (uvWidth * uvHeight)
				);
			}
		}

		/* Draw! */
		glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
		SDL_GL_SwapWindow(window);
	}

	/* Clean up. We out. */
	glDeleteProgram(program);
	glDeleteTextures(3, yuvTextures);
	SDL_free(frame);
	if (tf_hasaudio(&fileIn))
	{
		SDL_DestroyAudioStream(stream);
	}
	tf_close(&fileIn);
	SDL_GL_DestroyContext(context);
	SDL_DestroyWindow(window);
	SDL_Quit();
	SDL_Log("Test complete.\n");
	return 0;
}
