#include "core.h"
#include "log.h"
#include "render.h"
#include "renderdevice.h"
#include "texturesmanager.h"
#include "shadersystem.h"
#include "modelsystem.h"
#include "debugrender.h"
#include "ifilesystem.h"
#include "camera.h"
#include "imguimanager.h"
#include "scenemanager.h"
#include "gpu_buffer.h"

static GLuint g_VAO = 0;

int g_NumModels = 0;

// TEMP
glm::vec3 g_viewOrigin;
glm::vec3 g_viewOrient;

#ifdef GL_DEBUG_OUTPUT
#define GL_DEBUG_OUTPUT                   0x92E0
#endif // GL_DEBUG_OUTPUT

#ifndef GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB
#define GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB 0x8242
#endif // !GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB

#ifndef GL_DEBUG_TYPE_ERROR_ARB
#define GL_DEBUG_TYPE_ERROR_ARB 0x824C
#endif // !GL_DEBUG_TYPE_ERROR_ARB

#ifndef GL_DEBUG_OUTPUT
#define GL_DEBUG_OUTPUT                   0x92E0
#endif // !GL_DEBUG_OUTPUT

#ifndef GL_ARB_debug_output
#define GL_ARB_debug_output 1
typedef void (APIENTRYP PFNGLDEBUGMESSAGECONTROLARBPROC)(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint* ids, GLboolean enabled);
PFNGLDEBUGMESSAGECONTROLARBPROC glDebugMessageControlARB;
typedef void (APIENTRYP PFNGLDEBUGMESSAGEINSERTARBPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar* buf);
PFNGLDEBUGMESSAGEINSERTARBPROC glDebugMessageInsertARB;
typedef void (APIENTRYP PFNGLDEBUGMESSAGECALLBACKARBPROC)(GLDEBUGPROCARB callback, const void* userParam);
PFNGLDEBUGMESSAGECALLBACKARBPROC glDebugMessageCallbackARB;
typedef GLuint(APIENTRYP PFNGLGETDEBUGMESSAGELOGARBPROC)(GLuint count, GLsizei bufSize, GLenum* sources, GLenum* types, GLuint* ids, GLenum* severities, GLsizei* lengths, GLchar* messageLog);
PFNGLGETDEBUGMESSAGELOGARBPROC glGetDebugMessageLogARB;
#endif

void APIENTRY GL_DebugOutput(GLenum source,
	GLenum type,
	unsigned int id,
	GLenum severity,
	GLsizei length,
	const char* message,
	const void* userParam)
{
	// Nvidia spam too much about buffer mapping
	if (type == 0x8251)
		return;

	Msg("OpenGL: %stype = 0x%x, severity = 0x%x, message = %s\n",
		(type == GL_DEBUG_TYPE_ERROR_ARB ? "** GL ERROR ** " : ""),
		type, severity, message);

	if (type == GL_DEBUG_TYPE_ERROR_ARB)
	{
		bool debug = true;
		//__debugbreak();
	}
}

Render* g_render = nullptr;

Render::Render() :
	m_pWindow(nullptr),
	m_pGLContext(nullptr),
	m_pStretchedPicVBuf(nullptr),
	m_usingVAO(false),
	m_showStats(false)
{
	m_viewMatrix = glm::identity<glm::mat4>();
	m_projectionMatrix = glm::identity<glm::mat4>();
}

Render::~Render()
{
}

void Render::Init(SDL_Window* pWindow)
{
	m_pWindow = pWindow;
	SDL_assert(m_pWindow);

	Msg("Initializing renderer ...");

	// Create OpenGL context
	m_pGLContext = SDL_GL_CreateContext(m_pWindow);
	SDL_GL_MakeCurrent(m_pWindow, m_pGLContext);

	gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress);

	if (GLVersion.major == 0 || GLVersion.minor == 0)
	{
		Core::Error("Failed to load OpenGL");
	}

	// Core profile probably, should use VAO
	if (GLVersion.major >= 3 && GLVersion.minor >= 1)
		m_usingVAO = true;

	Msg("%s", (const char*)glGetString(GL_VENDOR));
	Msg("%s", (const char*)glGetString(GL_RENDERER));
	Msg("OpenGL ver. %s", (const char*)glGetString(GL_VERSION));

	Msg("Initializing OpenGL extensions...");
	InitGLExtensions();
	
	// Reset OpenGL error stack
	glGetError();

	// Create render device
	g_renderDevice = new RenderDevice();

	// Create texture manager
	g_texturesManager = new TexturesManager();
	g_texturesManager->Init();

	// Create shader system
	g_shaderSystem = new ShaderSystem();
	g_shaderSystem->Init();

	// Create model system
	g_modelSystem = new ModelSystem();
	g_modelSystem->Init();

	// Create scene manager
	g_sceneManager = new SceneManager();

	// Create debug render
	g_debugRender = new DebugRender();
	g_debugRender->Initialize();

	// Create imgui manager
	g_ImGuiManager.Init();

	// Create stretched picture filenameBuffer
	m_pStretchedPicVBuf = g_renderDevice->CreateVertexBuffer(nullptr, MAX_STRETCH_VX, true);

	// Core profile require Vertex Array Object to render
	if (m_usingVAO)
	{
		// Create global vertex array
		glGenVertexArrays(1, &g_VAO);
		glBindVertexArray(g_VAO);
	}
}

void Render::InitGLExtensions()
{
	//// Load extension
	//glDebugMessageCallbackARB = (PFNGLDEBUGMESSAGECALLBACKARBPROC)SDL_GL_GetProcAddress("glDebugMessageCallbackARB");

	//// Enable debug output
	//if (glDebugMessageCallbackARB)
	//{
	//	Msg("...found GL_ARB_debug_output");

	//	//glEnable(GL_DEBUG_OUTPUT);
	//	glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB);
	//	glDebugMessageCallbackARB(GL_DebugOutput, NULL);
	//}
	
	//bool found_WGL_EXT_swap_control = false;
	bool found_GL_ARB_debug_output = false;

	// Load extensions
	char* extensions = (char*)glGetString(GL_EXTENSIONS); // OpenGL Error: GL_INVALID_ENUM at D:\projects\old\pke\src\render\shadersystem.cpp:169
	if (extensions)
	{
		char* extension = strtok(extensions, " ");
		while (extension)
		{
			//if (strcmp(extension, "WGL_EXT_swap_control") == 0)
			//	found_WGL_EXT_swap_control = true;

			if (strcmp(extension, "GL_ARB_debug_output") == 0)
				found_GL_ARB_debug_output = true;

			extension = strtok(NULL, " ");
		}
	}
	else // core profile
	{
		int NumberOfExtensions;
		glGetIntegerv(GL_NUM_EXTENSIONS, &NumberOfExtensions);

		for (int i = 0; i < NumberOfExtensions; i++) {
			char* extension = (char*)glGetStringi(GL_EXTENSIONS, i);
		
			//if (strcmp(extension, "WGL_EXT_swap_control") == 0)
			//	found_WGL_EXT_swap_control = true;

			if (strcmp(extension, "GL_ARB_debug_output") == 0)
				found_GL_ARB_debug_output = true;

		}
	}

	// Enable debug output
	if (found_GL_ARB_debug_output)
	{
		Msg("...found GL_ARB_debug_output");
	
		glDebugMessageCallbackARB = (PFNGLDEBUGMESSAGECALLBACKARBPROC)SDL_GL_GetProcAddress("glDebugMessageCallbackARB");

		glEnable(GL_DEBUG_OUTPUT);
		glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS_ARB);
		glDebugMessageCallbackARB(GL_DebugOutput, NULL);
	}
	else
	{
		Msg("...not found GL_ARB_debug_output");
	}

}

void Render::Shutdown()
{
	if (m_usingVAO)
	{
		glBindVertexArray(0);
		glDeleteVertexArrays(1, &g_VAO);
	}

	delete m_pStretchedPicVBuf;
	m_pStretchedPicVBuf = nullptr;

	g_ImGuiManager.Shutdown();

	g_debugRender->Shutdown();
	delete g_debugRender;
	g_debugRender = nullptr;

	delete g_sceneManager;
	g_sceneManager = nullptr;

	g_modelSystem->Shutdown();
	delete g_modelSystem;
	g_modelSystem = nullptr;

	g_shaderSystem->Shutdown();
	delete g_shaderSystem;
	g_shaderSystem = nullptr;

	g_texturesManager->Shutdown();
	delete g_texturesManager;
	g_texturesManager = nullptr;

	delete g_renderDevice;
	g_renderDevice = nullptr;

	// Destroy OpenGL context
	SDL_GL_MakeCurrent(nullptr, nullptr);
	SDL_GL_DestroyContext(m_pGLContext);
	m_pGLContext = nullptr;
}

void Render::RenderScene() {
	//if (m_sceneModel) {
	//	Camera* camera = g_cameraManager.GetActiveCamera();
	//	if (camera) {
	//		glm::mat4 viewProj = m_projectionMatrix * m_viewMatrix;
	//		viewProj = glm::transpose(viewProj);

	//		camera->GetFrustum().Build(viewProj);

	//		if (camera->GetFrustum().CullBoundingBox(m_sceneModel->GetBoundingBox()))
	//			return;
	//	}

	//	static glm::mat4 s_identity = glm::mat4(1.0f);
	//	m_sceneModel->Draw(s_identity);

	//	g_NumModels++;
	//
	//	g_debugRender->DrawBoundingBox(m_sceneModel->GetBoundingBox(), glm::vec3(1.0f));
	//}

	static glm::mat4 s_identity = glm::mat4(1.0f);
	if (g_sceneManager->isSceneLoaded())
		g_sceneManager->renderScene(s_identity);

	g_debugRender->RenderFrame();
}

void Render::RenderStats()
{
	if (!m_showStats)
		return;

	char buffer[256];

	snprintf(buffer, sizeof(buffer), "FPS: %.1f", ImGui::GetIO().Framerate);
	ImGui::GetForegroundDrawList()->AddText(ImVec2(0.0f, 0.0f), 0xffffffff, buffer);

	snprintf(buffer, sizeof(buffer), "Scene: %s", g_sceneManager->getSceneName());
	ImGui::GetForegroundDrawList()->AddText(ImVec2(0.0f, 15.0f), 0xffffffff, buffer);

	snprintf(buffer, sizeof(buffer), "numModels: %d", g_NumModels);
	ImGui::GetForegroundDrawList()->AddText(ImVec2(0.0f, 30.0f), 0xffffffff, buffer);

	Camera* camera = g_cameraManager.GetActiveCamera();
	if (camera)
	{
		snprintf(buffer, sizeof(buffer), "cam pos: %.2f %.2f %.2f", camera->GetPosition().x, camera->GetPosition().y, camera->GetPosition().z);
		ImGui::GetForegroundDrawList()->AddText(ImVec2(0.0f, 45.0f), 0xffffffff, buffer);
	}
}

void Render::Present(bool vsync)
{
	SDL_GL_SetSwapInterval(vsync);

	SDL_GL_SwapWindow(m_pWindow);

	// reset stats
	ResetStates();

	g_NumModels = 0;
}

void Render::ResetStates()
{
	g_renderDevice->SetDepthTest(true);
	g_renderDevice->SetDepthWrite(true);
	g_renderDevice->SetStencilTest(false);
	g_renderDevice->SetScissorTest(false);
	g_renderDevice->SetCullFace(false);
	g_renderDevice->SetBlending(false);
}

void Render::SetViewMatrix(const glm::mat4& matView)
{
	m_viewMatrix = matView;
}

void Render::SetProjectionMatrix(const glm::mat4& matProjection)
{
	m_projectionMatrix = matProjection;
}

void Render::LoadSceneXML(const char* filename)
{
	g_sceneManager->loadScene(filename);
}

void Render::ToggleShowStats()
{
	m_showStats = !m_showStats;
}

//IRender* GetRender()
//{
//	return g_render;
//}