/*
 * Created by Brett on 28/11/23.
 * Licensed under GNU General Public License V3.0
 * See LICENSE file for license detail
 */
#include <blt/gfx/window.h>
#include <blt/std/assert.h>
#include <blt/logging/logging.h>
#include <blt/std/time.h>
#include <blt/gfx/input.h>
#include <blt/gfx/font/font.h>
#include <queue>
#include <imgui.h>
#include "backends/imgui_impl_opengl3.h"
#include "backends/imgui_impl_glfw.h"
#include <blt/gfx/imgui/ImGuiUtils.h>

#ifdef __EMSCRIPTEN__

#include <emscripten/html5.h>
#include <blt/std/types.h>

#endif

void error_callback(int error, const char* description)
{
	BLT_ERROR("GLFW Error (%d): %s", error, description);
	std::abort();
}

void gl_error_callback(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei, const GLchar* message, const void*)
{
	// do not log performance concerns
	if (type == GL_DEBUG_TYPE_OTHER)
		return;
	if (type == GL_DEBUG_TYPE_ERROR)
	{
		BLT_ERROR("[OpenGL Error] message = '{}', type = 0x{:x}, severity = 0x{:x}, source = 0x{:x}, id = {}", message, type, severity, source, id);
	} else
	{
		BLT_WARN("[OpenGL Error] message = '{}', type = 0x{:x}, severity = 0x{:x}, source = 0x{:x}, id = {}", message, type, severity, source, id);
	}
}

namespace blt::gfx
{
	struct
	{
		/* GLFW Window Object */
		GLFWwindow* window = nullptr;
		/* BLT internal input state manager, handles keyboard/mouse allocations + states */
		input_manager inputManager;
		/* stores any drag and dropped paths for processing */
		std::queue<std::string> pendingPaths;
		/* current width and height of the window */
		std::int32_t width = 0;
		std::int32_t height = 0;
		/* Frame time information (last frame) */
		std::int64_t lastTime = blt::system::nanoTime();
		std::int64_t deltaTime = 0;
		double nanoDelta = 0;
		double millisDelta = 0;
		window_data* data = nullptr;
	} window_state;

	void create_callbacks()
	{
		/* Setup keyboard callback */
		glfwSetKeyCallback(window_state.window, [](GLFWwindow*, int key, int, int action, int) {
			if (key < 0 || key == GLFW_KEY_UNKNOWN)
				return;
			KEY_STATE state;
			switch (action)
			{
				case GLFW_PRESS:
					state = KEY_STATE::PRESS;
					window_state.inputManager.key_pressed = true;
					break;
				case GLFW_REPEAT:
					state = KEY_STATE::REPEAT;
					break;
				default:
					state = KEY_STATE::RELEASE;
					window_state.inputManager.key_released = true;
					break;
			}
			window_state.inputManager.key(key) = state;
		});

		/* Setup mouse button callback */
		glfwSetMouseButtonCallback(window_state.window, [](GLFWwindow*, int button, int action, int) {
			if (button < 0)
				return;
			MOUSE_STATE state;
			switch (action)
			{
				case GLFW_PRESS:
					state = MOUSE_STATE::PRESS;
					window_state.inputManager.mouse_pressed = true;
					break;
				default:
					state = MOUSE_STATE::RELEASE;
					window_state.inputManager.mouse_released = true;
					break;
			}
			window_state.inputManager.mouse(button) = state;
		});

		/* Setup mouse cursor callback */
		glfwSetCursorPosCallback(window_state.window, [](GLFWwindow*, double x, double y) {
			window_state.inputManager.updateMousePos(x, y);
			window_state.inputManager.mouse_moved = true;
		});

		/* Setup mouse scroll callback */
		glfwSetScrollCallback(window_state.window, [](GLFWwindow*, double, double s) {
			window_state.inputManager.updateScroll(s);
		});

		/* Setup drop input callback */
		glfwSetDropCallback(window_state.window, [](GLFWwindow*, int count, const char** paths) {
			for (int i = 0; i < count; i++)
				window_state.pendingPaths.emplace(paths[i]);
		});
	}

	void setup_ImGUI()
	{
		const char* glsl_version = "#version 100";

		// Setup Dear ImGui context
		IMGUI_CHECKVERSION();
		ImGui::CreateContext();
		ImGuiIO& io = ImGui::GetIO();
		io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
		io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;  // Enable Gamepad Controls

		// Setup Dear ImGui style
		ImGui::StyleColorsDark();
		//        ImGui::Spectrum::StyleColorsSpectrum();
		ImGui::Spectrum::LoadFont();
		ImGui::SetupImGuiStyle(true, 1.0);

		// Setup FA
		ImFontConfig config;
		config.MergeMode = true;
		config.GlyphMinAdvanceX = 13.0f; // Use if you want to make the icon monospaced
		static constexpr ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
		io.Fonts->AddFontFromMemoryCompressedBase85TTF(fontAwesomeRegular_compressed_data_base85, 13.0f, &config, icon_ranges);
		io.Fonts->AddFontFromMemoryCompressedTTF(fontAwesomeSolid_compressed_data, static_cast<int>(fontAwesomeSolid_compressed_size), 13.0, &config,
												icon_ranges);
		io.Fonts->AddFontFromMemoryCompressedTTF(fontAwesomeBrands_compressed_data, static_cast<int>(fontAwesomeBrands_compressed_size), 13.0,
												&config, icon_ranges);

		//ImGui::StyleColorsLight();

		// Setup Platform/Renderer backends
		ImGui_ImplGlfw_InitForOpenGL(window_state.window, true);
		#ifdef __EMSCRIPTEN__
		//        ImGui_ImplGlfw_InstallEmscriptenCanvasResizeCallback("#canvas");
		#endif
		ImGui_ImplOpenGL3_Init(glsl_version);

		#ifdef __EMSCRIPTEN__
		io.IniFilename = nullptr;
		#endif
	}

	void loop(void* arg)
	{
		auto& data = *((window_data*) arg);
		/* -- Get the current framebuffer size, update the global width/height state, along with OpenGL viewport -- */
		glfwGetFramebufferSize(window_state.window, &window_state.width, &window_state.height);
		glViewport(0, 0, window_state.width, window_state.height);
		data.width = window_state.width;
		data.height = window_state.height;
		// TODO: user option for this
		glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

		/* -- Begin the next ImGUI frame -- */
		ImGui_ImplOpenGL3_NewFrame();
		ImGui_ImplGlfw_NewFrame();
		ImGui::NewFrame();

		/* -- Call user update function -- */
		data.call_update();

		/* -- Render the ImGUI frame -- */
		ImGui::Render();
		ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

		/* -- Update GLFW state -- */
		window_state.inputManager.clear();
		glfwSwapBuffers(window_state.window);
		glfwPollEvents();

		/* -- Update Frame Timing Information -- */
		const auto current_time = system::nanoTime();
		window_state.deltaTime = current_time - window_state.lastTime;
		window_state.lastTime = current_time;
		window_state.nanoDelta = static_cast<double>(window_state.deltaTime) / 1e9f;
		window_state.millisDelta = static_cast<double>(window_state.deltaTime) / 1e6f;
	}

	#ifdef __EMSCRIPTEN__

	EM_BOOL emscripten_resize_callback(int, const EmscriptenUiEvent* event, void* data)
	{
		int width = event->documentBodyClientWidth;
		int height = event->documentBodyClientHeight;

		glfwSetWindowSize(window_state.window, width, height);

		return false;
	}

	EM_JS(int, get_screen_width, (), { return document.body.clientWidth; });

	EM_JS(int, get_screen_height, (), { return document.body.clientHeight; });

	#endif

	void init(window_data data)
	{
		window_state.data = &data; // NOLINT
		#ifdef __EMSCRIPTEN__
		logging::get_global_config().set_log_format(
			std::string("[") + logging::tags::FULL_TIME + "] [" + logging::tags::LOG_LEVEL + "] (" + logging::tags::FILE + ":" + logging::tags::LINE +
			") " + logging::tags::STR + "\n");
		emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, false, emscripten_resize_callback);

		data.width = get_screen_width();
		data.height = get_screen_height();
		#endif
		/* -- Set up Error Callback -- */
		glfwSetErrorCallback(error_callback);
		BLT_ASSERT(glfwInit() && "Unable to init GLFW. Aborting.");

		/* -- Set up Window Context -- */
		glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, data.context.GL_MAJOR);
		glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, data.context.GL_MINOR);
		glfwWindowHint(GLFW_DOUBLEBUFFER, data.context.DOUBLE_BUFFER);
		glfwWindowHint(GLFW_OPENGL_PROFILE, data.context.GL_PROFILE);
		glfwWindowHint(GLFW_SAMPLES, data.context.SAMPLES);
		glfwWindowHint(GLFW_MAXIMIZED, data.maximized);

		/* -- Create the Window -- */
		window_state.window = glfwCreateWindow(data.width, data.height, data.title.c_str(), data.monitor, data.share);
		data.window = window_state.window;
		BLT_ASSERT(window_state.window && "Unable to create GLFW window.");

		/* -- Set Window Specifics + OpenGL -- */
		glfwMakeContextCurrent(window_state.window);
		#ifndef __EMSCRIPTEN__
        glfwSwapInterval(data.sync_interval);
        gladLoadGL(glfwGetProcAddress);
		#endif

		#ifndef __EMSCRIPTEN__
        glEnable(GL_DEBUG_OUTPUT);
        glDebugMessageCallback(gl_error_callback, nullptr);
		#endif

		/* -- Set up our local callbacks, ImGUI will then call these -- */
		create_callbacks();

		/* -- Set up ImGUI -- */
		setup_ImGUI();

		#ifdef GL_MULTISAMPLE
        if (data.context.SAMPLES > 0)
            glEnable(GL_MULTISAMPLE);
		#endif

		window_state.width = data.width;
		window_state.height = data.height;

		/* -- Call User Provided post-window-init function  -- */
		data.call_init();

		#ifdef __EMSCRIPTEN__
		/*
	* "setting 0 or a negative value as the fps will use the browser’s requestAnimationFrame mechanism to call the main loop function.
	* This is HIGHLY recommended if you are doing rendering, as the browser’s requestAnimationFrame will
	* make sure you render at a proper smooth rate that lines up properly with the browser and monitor."
	* https://emscripten.org/docs/api_reference/emscripten.h.html
	*/
		emscripten_set_main_loop_arg(loop, (void*) &data, 0, true);
		#else
        /* -- General Loop -- */
        while (!glfwWindowShouldClose(window_state.window))
            loop((void*) &data);
		#endif

		/* -- Call User provided post-loop destroy function. Added for consistency -- */
		data.call_destroy();
	}

	void cleanup()
	{
		ImGui_ImplOpenGL3_Shutdown();
		ImGui_ImplGlfw_Shutdown();
		ImGui::DestroyContext();

		glfwDestroyWindow(window_state.window);
		glfwTerminate();
	}

	double getMouseX()
	{
		return window_state.inputManager.mouseX;
	}

	double getMouseY()
	{
		return window_state.inputManager.mouseY;
	}

	double getMouseDX()
	{
		return window_state.inputManager.deltaX;
	}

	double getMouseDY()
	{
		return window_state.inputManager.deltaY;
	}

	void lockCursor()
	{
		glfwSetInputMode(window_state.window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
	}

	void unlockCursor()
	{
		glfwSetInputMode(window_state.window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
	}

	bool isCursorLocked()
	{
		return glfwGetInputMode(window_state.window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED;
	}

	bool isCursorInWindow()
	{
		#ifdef __EMSCRIPTEN__
		// TODO:
		return true;
		#else
        return glfwGetWindowAttrib(window_state.window, GLFW_HOVERED);
		#endif
	}

	void setRawInput(bool state)
	{
		#ifdef __EMSCRIPTEN__
		// TODO?
		#else
        if (glfwRawMouseMotionSupported())
            glfwSetInputMode(window_state.window, GLFW_RAW_MOUSE_MOTION, state ? GLFW_TRUE : GLFW_FALSE);
		#endif
	}

	bool isRawInput()
	{
		#ifdef __EMSCRIPTEN__
		return false;
		#else
        return glfwGetInputMode(window_state.window, GLFW_RAW_MOUSE_MOTION);
		#endif
	}

	void setClipboard(const std::string& str)
	{
		glfwSetClipboardString(window_state.window, str.c_str());
	}

	std::string getClipboard()
	{
		return glfwGetClipboardString(window_state.window);
	}

	bool isMousePressed(int button)
	{
		return window_state.inputManager.isMousePressed(button);
	}

	bool isKeyPressed(int key)
	{
		return window_state.inputManager.isKeyPressed(key);
	}

	double getFrameDeltaSeconds()
	{
		return window_state.nanoDelta;
	}

	double getFrameDeltaMilliseconds()
	{
		return window_state.millisDelta;
	}

	i64 getFrameDelta()
	{
		return window_state.deltaTime;
	}

	bool mouseMovedLastFrame()
	{
		return window_state.inputManager.mouse_moved;
	}

	bool mousePressedLastFrame()
	{
		return window_state.inputManager.mouse_pressed;
	}

	bool keyPressedLastFrame()
	{
		return window_state.inputManager.key_pressed;
	}

	i32 getWindowHeight()
	{
		return window_state.height;
	}

	i32 getWindowWidth()
	{
		return window_state.width;
	}

	bool keyReleasedLastFrame()
	{
		return window_state.inputManager.key_released;
	}

	bool mouseReleaseLastFrame()
	{
		return window_state.inputManager.mouse_released;
	}

	void setWindowSize(const i32 width, const i32 height)
	{
		window_state.data->setWindowSize(width, height);
	}

	window_data& window_data::setWindowSize(int32_t new_width, int32_t new_height)
	{
		width = new_width;
		height = new_height;
		glfwSetWindowSize(window_state.window, width, height);
		return *this;
	}

	window_data& window_data::setMonitor(GLFWmonitor* m)
	{
		window_data::monitor = m;
		return *this;
	}

	window_data& window_data::setShare(GLFWwindow* s)
	{
		window_data::share = s;
		return *this;
	}

	window_data& window_data::setMaximized(bool b)
	{
		if (b)
			window_data::maximized = GLFW_TRUE;
		else
			window_data::maximized = GLFW_FALSE;
		return *this;
	}
}
