#include "stdafx.h"

#include <fstream>
#include <strstream>
#include "FFmpegPlayer/FFmpegPlayer.h"

// iOrange - 0 means using CPU for yuv-rgb conversion, while 1 - use the GPU
#define USE_HW_YUV2RGB	1


HWND		g_hWnd;
HINSTANCE	g_hInst;

const char * _className = "Orange_Class_Wnd";
const char * _windowName = "OpenGL 3.0";

template <int what, int to>
int change_char(int c) { if (what == c) return to; return c; }

LRESULT CALLBACK WndProc(HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam);
bool CreateMainWindow(int w, int h);
void InitFrameDraw(void);
void Draw(void);
std::string ReadTextFromFile(const std::string& fileName);


GLint			_modelViewProjection, _bonesArray;
GLint			_vertPosition, _vertUV;
GLuint			_texture = 0, _vao = 0, _vbo = 0;

uint32			_prevTime, _prevFrame = ~0;

FFmpegPlayer	g_Player;
char*			g_FrameData = 0;


// iOrange - application entry point
int main(int argc, char* argv[])
{
	// iOrange - first - try to open video file
	if (!g_Player.OpenVideoFile("g:/Downloads/Videos/The Big Bang Theory, Season 5 [Kuraj-Bambey] WEB-DL 720p/The.Big.Bang.Theory.S05E18.720p.WEB-DL.eng.rus.[Kuraj-Bambey].mkv"))
	{
		Log_Error("Couldn't open video file!");
		return 1;
	}

	// iOrange - get our module handle
	g_hInst = GetModuleHandle(NULL);

	// iOrange - creating application main window
	Log_Normal( "Creating main window..." );
	if (!CreateMainWindow(g_Player.GetFrameWidth(), g_Player.GetFrameHeight()))
	{
		Log_Error("Couldn't create main window");
		return 1;
	}

	// iOrange - initializing OpenGL 3
	HGLRC _glRC;
	Log_Normal("Trying to initialize OpenGL 3.0");
	if (!OpenGL_3::Initialize(g_hWnd, _glRC))
	{
		Log_Error("Couldn't init OpenGL 3.0... Shutdown...");
		return 1;
	}
	OpenGL_3::AssertGLError();

	// iOrange - print some info about OpenGL
	std::string oglversion = OpenGL_3::GetVersion();
	OpenGL_3::AssertGLError();
	std::string ogl_vendor = OpenGL_3::GetVendor();
	OpenGL_3::AssertGLError();
	std::string ogl_renderer = OpenGL_3::GetRenderer();
	OpenGL_3::AssertGLError();
	std::string extensions = OpenGL_3::GetExtensions();
	OpenGL_3::AssertGLError();
	std::string wgl_extens = OpenGL_3::GetWGLExtensions();
	OpenGL_3::AssertGLError();
	std::transform(extensions.begin(), extensions.end(), extensions.begin(), change_char<' ', '/n'>);
	std::transform(wgl_extens.begin(), wgl_extens.end(), wgl_extens.begin(), change_char<' ', '/n'>);
	std::cout << "OpenGL Version  :  " << oglversion << std::endl;
	std::cout << "OpenGL Vendor   :  " << ogl_vendor << std::endl;
	std::cout << "OpenGL Renderer :  " << ogl_renderer << std::endl;
	std::cout << "OpenGL Extentions:" << std::endl << extensions << std::endl;
	std::cout << "OpenGL WGL-Extentions:" << std::endl << wgl_extens << std::endl;

	// iOrange - initialize some extensions
	Log_Normal("Initializing Multi-textures extensions...");
	if (!glext::InitMultitextureExtensions())
	{
		Log_Error("Couldn't initialize Multi-textures extensions");
		OpenGL_3::Shutdown();
	}
	Log_Normal("Initializing VBO extensions...");
	if (!glext::InitVBO())
	{
		Log_Error("Couldn't initialize VBO");
		OpenGL_3::Shutdown();
	}
	Log_Normal("Initializing VBO extensions OK");
	glext::InitVertexAttribs();
	Log_Normal("Initializing GLSL extensions...");
	if (!glext::InitGLSLNeededExts())
	{
		Log_Error("Couldn't initialize GLSL");
		OpenGL_3::Shutdown();
	}
	Log_Normal("Initializing GLSL extensions OK");
	std::cout << "OpenGL GLSL Version :  " << OpenGL_3::GetGLSLVersion() << std::endl;

	// iOrange - load our shaders
	const std::string vs = ReadTextFromFile("data/vertex.glsl");
#if USE_HW_YUV2RGB
	const std::string fs = ReadTextFromFile("data/fragment_yuv.glsl");
#else // USE_HW_YUV2RGB
	const std::string fs = ReadTextFromFile("data/fragment.glsl");
#endif // USE_HW_YUV2RGB

	Log_Normal("Try to compile such shader:/nVertex Program:/n/t" + vs + "/nFragment Program:/n/t" + fs);
	GLhandleARB program = OpenGL_3::CreateGLSLProgram(vs, fs);
	if (0 == program)
	{
		Log_Error("Compilation failed... Exiting...");
		exit(1);
	}
	Log_Normal("Compilation ok!");

	glext::glUseProgramObjectARB(program);

	// iOrange - query uniforms and attributes locations
	_modelViewProjection = glext::glGetUniformLocationARB(program, "modelViewProjection");
	_vertPosition = glext::glGetAttribLocationARB(program, "vPos");
	_vertUV = glext::glGetAttribLocationARB(program, "vTexCoord");
	// iOrange - set the texture unit
	glext::glUniform1iARB(glext::glGetUniformLocationARB(program, "videoFrame"), 0);

	// iOrange - initial OpenGL states setup
	glViewport(0, 0, g_Player.GetFrameWidth(), g_Player.GetFrameHeight());
	glScissor(0, 0, g_Player.GetFrameWidth(), g_Player.GetFrameHeight());
	glClearColor(0, 0, 0, 1);
	glDisable(GL_DEPTH_TEST);

	// iOrange - setup orthogonal 2D projection
	mat4 proj = MakeOrtho2D(0.0f, static_cast<float>(g_Player.GetFrameWidth()),
			static_cast<float>(g_Player.GetFrameHeight()), 0.0f, -1.0f, 1.0f);

	glext::glUniformMatrix4fvARB(_modelViewProjection, 1, 0, proj);
	OpenGL_3::AssertGLError();

	// iOrange - init vertex and texture data for video frame drawing
	InitFrameDraw();
	OpenGL_3::AssertGLError();

	_prevTime = GetTickCount();
	
	MSG msg;
	while (true)
	{
		if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE))
		{
			if (!GetMessage(&msg, NULL, 0, 0) || WM_QUIT == msg.message)
				break;
			else
			{
				TranslateMessage(&msg);
				DispatchMessage (&msg);
			}
		}

		Draw();
	}

	// iOrange - delete frame memory
	if (g_FrameData)
		delete [] g_FrameData;

	OpenGL_3::Shutdown();
	return 0;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT wMsg, WPARAM wParam, LPARAM lParam)
{
	switch (wMsg)
	{
		case WM_KEYDOWN:
		{
			if (VK_ESCAPE == wParam)
				PostQuitMessage(0);
		} return 0;

		case WM_DESTROY:
		case WM_CLOSE:
		{
			PostQuitMessage(0);
		} return 0;

		default:
			return DefWindowProc(hWnd, wMsg, wParam, lParam);
	}
}

bool CreateMainWindow(int w, int h)
{
	WNDCLASSEX wc;
	RtlZeroMemory(&wc, sizeof(wc));
	wc.cbSize        = sizeof(wc);
	wc.style         = CS_DBLCLKS | CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	wc.lpfnWndProc   = (WNDPROC)WndProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = GetModuleHandle(NULL);
	wc.hIcon         = LoadIcon(NULL, IDI_WINLOGO);
	wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
	wc.hbrBackground = NULL;
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = _className;

	if (!RegisterClassEx(&wc))
	{
		Log_Error("Couldn't register wnd class");
		return false;
	}
	uint32 styleEx = 0;
	uint32 style   = (WS_VISIBLE | WS_OVERLAPPEDWINDOW | WS_CLIPSIBLINGS | WS_CLIPCHILDREN) ^ WS_MAXIMIZEBOX;
	RECT rc;
	SetRect(&rc, 0, 0, w, h);
	AdjustWindowRectEx(&rc, style, FALSE, styleEx);
	if (!(g_hWnd = CreateWindowEx(styleEx, _className, _windowName, style, 0, 0,
								  rc.right - rc.left, rc.bottom - rc.top, 0, 0, g_hInst, 0)))
	{
		Log_Error("Couldn't create main window");
		UnregisterClass(_className, g_hInst);
		return false;
	}

	return true;
}

// iOrange - round up to nearest power-of-two value
int RoundToPOT(int n)
{
	n = n - 1;
	n = n | (n >> 1);
	n = n | (n >> 2);
	n = n | (n >> 4);
	n = n | (n >> 8);
	n = n | (n >> 16);
	n = n + 1;
	return n;
}

// iOrange - initialize vertex and texture data needed for video frame draw
void InitFrameDraw(void)
{
	// iOrange - get video frame size
	const int frameW = g_Player.GetFrameWidth();
	const int frameH = g_Player.GetFrameHeight();

	// iOrange - calc POTD texture size
	const int textureW = RoundToPOT(frameW);
	const int textureH = RoundToPOT(frameH);

	// iOrange - calculate vertexes positions and texture coordinates
	const float x0 = 0.0f, x1 = static_cast<float>(g_Player.GetFrameWidth());
	const float y0 = 0.0f, y1 = static_cast<float>(g_Player.GetFrameHeight());
	const float u0 = 0.0f, u1 = static_cast<float>(frameW) / static_cast<float>(textureW);
	const float v0 = 0.0f, v1 = static_cast<float>(frameH) / static_cast<float>(textureH);

	float vertexes[] = {
		x0, y1, u0, v1,
		x0, y0, u0, v0,
		x1, y1, u1, v1,
		x1, y0, u1, v0
	};

	// iOrange - create and fill vertex buffer
	glext::glGenBuffersARB(1, &_vbo);
	glext::glBindBufferARB(GL_ARRAY_BUFFER, _vbo);
	glext::glBufferDataARB(GL_ARRAY_BUFFER, sizeof(vertexes), vertexes, GL_STATIC_DRAW);

	// iOrange - create and setup vertex array
	glext::glGenVertexArrays(1, &_vao);
	glext::glBindVertexArray(_vao);

	glext::glBindBufferARB(GL_ARRAY_BUFFER, _vbo);
	glext::glEnableVertexAttribArrayARB(_vertPosition);
	glext::glEnableVertexAttribArrayARB(_vertUV);

	glext::glVertexAttribPointerARB(_vertPosition, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), vbo_offset(0));
	glext::glVertexAttribPointerARB(_vertUV, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), vbo_offset(2 * sizeof(float)));

	// iOrange - create and setup texture for holding video frame
	glGenTextures(1, &_texture);
	glBindTexture(GL_TEXTURE_2D, _texture);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, textureW, textureH, 0, GL_RGB, GL_UNSIGNED_BYTE, 0);

	// iOrange - buffer to hold unpacked video frame
	g_FrameData = new char[frameW * frameH * 3];
}

// iOrange - main function - update & draw
void Draw(void)
{
	// iOrange - calculate delta time
	uint32 deltaTime = GetTickCount() - _prevTime;
	_prevTime = GetTickCount();

	// iOrange - advance video playing by delta time
	uint32 frame = g_Player.Advance(deltaTime);

	// iOrange - if frame changes
	if (!g_Player.IsStopped() && _prevFrame != frame)
	{
		// iOrange - extract video frame into buffer
#if USE_HW_YUV2RGB
		g_Player.GetFrameYUV444(g_FrameData, g_Player.GetFrameWidth());
#else // USE_HW_YUV2RGB
		g_Player.GetFrameRGB(g_FrameData, g_Player.GetFrameWidth());
#endif // USE_HW_YUV2RGB

		// iOrange - and put this frame data into texture
		glBindTexture(GL_TEXTURE_2D, _texture);
		glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, g_Player.GetFrameWidth(), g_Player.GetFrameHeight(), GL_RGB, GL_UNSIGNED_BYTE, g_FrameData);
	}

	// iOrange - setup all needed stuff for drawing the frame
	glext::glActiveTextureARB(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, _texture);

	glext::glBindVertexArray(_vao);
	
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

	glBindTexture(GL_TEXTURE_2D, 0);

	// iOrange - swap frame buffers and check for OpenGL errors
	SwapBuffers(wglGetCurrentDC());
	OpenGL_3::AssertGLError();
}

// iOrange - helper function for reading text files, return file content as string
std::string ReadTextFromFile(const std::string& fileName)
{
	std::string fileContent;

#if _MSC_VER >= 1300
	FILE* f;
	if (0 == fopen_s(&f, fileName.c_str(), "rb"))
	{
#else
	FILE* f = fopen(fileName.c_str(), "rb");
	if (f != 0)
	{
#endif

		fseek(f, 0, SEEK_END);
		const long size = ftell(f);
		fseek(f, 0, SEEK_SET);

		char* text = new char[size];
		fread(text, 1, size, f);

		fileContent.assign(text, size);
		delete [] text;
	}

	return fileContent;
}
