/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#include "WindowGLFW.hpp"
#include "Base/Logger.hpp"
#include <GLFW/glfw3.h>

namespace OpenVulkano::GLFW
{
	WindowGLFW::WindowGLFW(OpenVulkano::GLFW::InputProviderGLFW& inputProvider)
		: inputProvider(inputProvider)
	{}

	WindowGLFW::~WindowGLFW() noexcept
	{
		if (window)
		{
			inputProvider.Close();
			Close();
		}
	}

	GLFWmonitor* WindowGLFW::GetCurrentMonitor() const
	{
		int monitorCount = 0;
		GLFWmonitor** monitors = glfwGetMonitors(&monitorCount);
		for (int i = 0; i < monitorCount; i++)
		{
			int posX, posY, sizeX, sizeY;
			glfwGetMonitorWorkarea(monitors[i], &posX, &posY, &sizeX, &sizeY);
			if (windowConfig.position.x >= posX && windowConfig.position.x < posX + sizeX &&
				windowConfig.position.y >= posY && windowConfig.position.y < posY + sizeY)
			{
				return monitors[i];
			}
		}
		return nullptr;
	}

	GLFWmonitor* WindowGLFW::GetTargetMonitor() const
	{
		if (windowConfig.windowMode == FULLSCREEN)
		{
			// TODO add config to control used display
			return GetPrimaryMonitor();
		}
		return nullptr;
	}

	void WindowGLFW::Create()
	{
		glfwWindowHint(GLFW_DECORATED, (~windowConfig.windowMode) & 1);
		//TODO handle full screen resolutions
		window = glfwCreateWindow(windowConfig.size.x, windowConfig.size.y, windowConfig.title.c_str(), GetTargetMonitor(), nullptr);
		if (!window) return;
		glfwSetWindowUserPointer(window, this);
		RegisterCallbacks();
	}

	void WindowGLFW::RegisterCallbacks() const
	{
		glfwSetDropCallback(window, DropCallback);
		glfwSetFramebufferSizeCallback(window, ResizeCallback);
		glfwSetWindowFocusCallback(window, FocusCallback);
		glfwSetWindowRefreshCallback(window, RefreshCallback);
		glfwSetWindowIconifyCallback(window, MinimizeCallback);
		glfwSetWindowPosCallback(window, WindowMoveCallback);
		glfwSetWindowCloseCallback(window, CloseCallback);

		// Input Callbacks
		glfwSetKeyCallback(window, KeyboardCallback);
		glfwSetMouseButtonCallback(window, MouseButtonCallback);
		glfwSetCursorPosCallback(window, MouseMoveCallback);
		glfwSetScrollCallback(window, MouseScrollCallback);
		glfwSetCursorEnterCallback(window, MouseEnterExitCallback);

		// Set mouse visibility
	}

	void WindowGLFW::SetMouseVisibility(bool hideMouse)
	{
		glfwSetInputMode(window, GLFW_RAW_MOUSE_MOTION, hideMouse);
		glfwSetInputMode(window, GLFW_CURSOR, hideMouse ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL);

	}

	GLFWmonitor* WindowGLFW::GetPrimaryMonitor()
	{
		return glfwGetPrimaryMonitor();
	}

	std::vector<GLFWmonitor*> WindowGLFW::GetMonitors()
	{
		int count;
		GLFWmonitor** monitorsArray = glfwGetMonitors(&count);
		std::vector<GLFWmonitor*> monitors;
		monitors.reserve(count);
		for (int i = 0; i < count; i++)
		{
			monitors[i] = monitorsArray[i];
		}
		return monitors;
	}

	void WindowGLFW::Init(RenderAPI::RenderApi renderApi)
	{
		if (renderApi == RenderAPI::Vulkan) glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
		Create();
		if (!window)
		{
			throw WindowInitFailedException("Failed to initialize window");
		}
		if (renderApi != RenderAPI::Vulkan) MakeCurrentThread();
		Logger::WINDOW->info("GLFW Window created (id: {0})", GetWindowId());
	}

	void WindowGLFW::Close()
	{
		glfwDestroyWindow(window);
		window = nullptr;
		Logger::WINDOW->info("GLFW Window destroyed (id: {0})", GetWindowId());
	}

	void WindowGLFW::Present() const
	{
		glfwSwapBuffers(window);
	}

	void WindowGLFW::Show()
	{
		glfwShowWindow(window);
	}

	void WindowGLFW::Hide()
	{
		glfwHideWindow(window);
	}

	void WindowGLFW::SetTitle(const std::string& title)
	{
		windowConfig.title = title;
		if (window)
		{
			glfwSetWindowTitle(window, title.c_str());
		}
	}

	void WindowGLFW::SetSize(uint32_t width, uint32_t height)
	{
		currentSize = { width, height };
		if (window)
		{
			glfwSetWindowSize(window, width, height);
		}
	}

	void WindowGLFW::SetPosition(int posX, int posY)
	{
		windowConfig.position.x = posX;
		windowConfig.position.y = posY;
		if (window)
		{
			glfwSetWindowPos(window, posX, posY);
		}
	}

	Math::Vector2ui WindowGLFW::GetSize()
	{
		if (currentSize.x == 0 || currentSize.y == 0)
			glfwGetWindowSize(window, reinterpret_cast<int*>(&currentSize.x), reinterpret_cast<int*>(&currentSize.y));
		return currentSize;
	}

	Math::Vector2i WindowGLFW::GetPosition()
	{
		Math::Vector2i pos;
		glfwGetWindowPos(window, &pos.x, &pos.y);
		return pos;
	}

	void WindowGLFW::SetSizeLimits(int minWidth, int minHeight, int maxWidth, int maxHeight)
	{
		minWidth = (minWidth < 0) ? GLFW_DONT_CARE : minWidth;
		minHeight = (minHeight < 0) ? GLFW_DONT_CARE : minHeight;
		maxWidth = (maxWidth < 0) ? GLFW_DONT_CARE : maxWidth;
		maxHeight = (maxHeight < 0) ? GLFW_DONT_CARE : maxHeight;
		glfwSetWindowSizeLimits(window, minWidth, minHeight, maxWidth, maxHeight);
	}

	void WindowGLFW::MakeCurrentThread()
	{
		glfwMakeContextCurrent(window);
	}

	void WindowGLFW::SetWindowMode(WindowMode windowMode)
	{
		if (windowMode == this->windowConfig.windowMode) return; // Nothing to change here
		if (!window) return; // Window not yet created
		this->windowConfig.windowMode = windowMode;
		uint32_t sizeX = 0, sizeY = 0;
		int posX = 0, posY = 0;
		GLFWmonitor* monitor = nullptr;
		glfwWindowHint(GLFW_DECORATED, (~windowMode) & 1);
		if (windowMode == WINDOWED || windowMode == BORDERLESS)
		{
			sizeX = windowConfig.size.x;
			sizeY = windowConfig.size.y;
			posX = windowConfig.position.x;
			posY = windowConfig.position.y;
			monitor = nullptr;
			if (windowMode == WINDOWED)
			{
				Logger::WINDOW->info("Switching to Windowed mode");
			}
			else
			{
				Logger::WINDOW->info("Switching to Borderless Windowed mode");
			}
		}
		else
		{ // Fullscreen
			windowConfig.position = GetPosition(); // Backup current window position
			monitor = GetCurrentMonitor();
			if (!monitor) monitor = GetPrimaryMonitor();
			if (!monitor) return; // We don't have a monitor to set the fullscreen window to
			const GLFWvidmode* videoMode = glfwGetVideoMode(monitor ? monitor : GetPrimaryMonitor());
			sizeX = videoMode->width;
			sizeY = videoMode->height;
			if (windowMode == FULLSCREEN)
			{
				// TODO find video mode that best matches user settings
				Logger::WINDOW->info("Switching to Fullscreen mode");
			}
			else if (windowMode == BORDERLESS_FULLSCREEN)
			{
				int sX, sY;
				glfwGetMonitorWorkarea(monitor, &posX, &posY, &sX, &sY);
				monitor = nullptr;
				Logger::WINDOW->info("Switching to Borderless Fullscreen mode");
			}
		}
		glfwSetWindowMonitor(this->window, monitor, posX, posY, sizeX, sizeY, GLFW_DONT_CARE);
	}

	void WindowGLFW::SetWindowHandler(IWindowHandler* handler)
	{
		this->handler = handler;
	}

	vk::SurfaceKHR WindowGLFW::CreateSurface(const vk::Instance& instance, const vk::AllocationCallbacks* pAllocator)
	{
		VkSurfaceKHR rawSurface;
		const vk::Result result = static_cast<vk::Result>(glfwCreateWindowSurface(static_cast<VkInstance>(instance), window, reinterpret_cast<const VkAllocationCallbacks*>(pAllocator), &rawSurface));
		if (result != vk::Result::eSuccess) [[unlikely]]
		{
			Logger::WINDOW->error("Failed to create vulkan surface. Result: {}", vk::to_string(result));
		}
		return rawSurface;//createResultValue(result, rawSurface, "vk::CommandBuffer::begin");
	}

	std::vector<std::string> WindowGLFW::GetRequiredInstanceExtensions()
	{
		return GetVulkanRequiredInstanceExtensions();
	}

	void WindowGLFW::OnResize(const uint32_t newWidth, const uint32_t newHeight)
	{
		Logger::WINDOW->debug("Window (id: {0}) resized (width: {1}, height: {2})", GetWindowId(), newWidth, newHeight);
		currentSize = { newWidth, newHeight };
		if (GetWindowMode() == WINDOWED || GetWindowMode() == BORDERLESS)
		{
			windowConfig.size = currentSize;
		}
		handler->OnWindowResize(this, newWidth, newHeight);
	}

	void WindowGLFW::OnMinimize()
	{
		Logger::WINDOW->debug("Window (id: {0}) minimized", GetWindowId());
		handler->OnWindowMinimize(this);
	}

	void WindowGLFW::OnRestore()
	{
		Logger::WINDOW->debug("Window (id: {0}) restored", GetWindowId());
		handler->OnWindowRestore(this);
	}

	void WindowGLFW::OnFocusLost()
	{
		Logger::WINDOW->trace("Window (id: {0}) focus lost", GetWindowId());
		handler->OnWindowFocusLost(this);
	}

	void WindowGLFW::OnFocusGained()
	{
		Logger::WINDOW->trace("Window (id: {0}) focus gained", GetWindowId());
		handler->OnWindowFocusGained(this);
	}

	void WindowGLFW::OnMove(const int posX, const int posY)
	{
		Logger::WINDOW->trace("Window (id: {0}) moved (x: {1}, y: {2})", GetWindowId(), posX, posY);
		if (windowConfig.windowMode == WINDOWED || windowConfig.windowMode == BORDERLESS)
		{ // Don't save window position for fullscreen
			windowConfig.position.x = posX;
			windowConfig.position.y = posY;
		}
		handler->OnWindowMove(this, posX, posY);
	}

	void WindowGLFW::OnClose()
	{
		Logger::WINDOW->debug("Window (id: {0}) closed", GetWindowId());
		handler->OnWindowClose(this);
	}

	void WindowGLFW::OnKeyEvent(int key, int scanCode, int action, int mods)
	{
		if (key == GLFW_KEY_ENTER && action == GLFW_PRESS && mods == GLFW_MOD_ALT)
		{
			WindowMode newMode = FULLSCREEN;
			switch (windowConfig.windowMode)
			{
				case WINDOWED: newMode = FULLSCREEN; break;
				case BORDERLESS: newMode = BORDERLESS_FULLSCREEN; break;
				case FULLSCREEN: newMode = WINDOWED; break;
				case BORDERLESS_FULLSCREEN: newMode = BORDERLESS; break;
			}
			SetWindowMode(newMode);
		}
		switch (action)
		{
			case GLFW_PRESS:// OnKeyPressed(key, mods);
				Logger::INPUT->debug("Key Pressed: {0} Mod: {1} ScanCode: {2}", key, mods, scanCode);
				break;
			case GLFW_RELEASE:// OnKeyReleased(key, mods);
				Logger::INPUT->debug("Key Released: {0} Mod: {1} ScanCode: {2}", key, mods, scanCode);
				break;
			case GLFW_REPEAT:
				Logger::INPUT->debug("Key Repeat: {0} Mod: {1} ScanCode: {2}", key, mods, scanCode);
				break;

			default: break;
		}
	}

	WindowGLFW* WindowGLFW::GetWindow(GLFWwindow* window)
	{
		return static_cast<WindowGLFW*>(glfwGetWindowUserPointer(window));
	}

	void WindowGLFW::KeyboardCallback(GLFWwindow* window, int key, int scanCode, int action, int mods)
	{
		GetWindow(window)->OnKeyEvent(key, scanCode, action, mods);
		const auto windowInstance = GetWindow(window);
		windowInstance->inputProvider.SetKeyboardKey(windowInstance, key, scanCode, action, mods);
	}

	void WindowGLFW::MouseButtonCallback(GLFWwindow* window, int button, int action, int mods)
	{
		GetWindow(window)->inputProvider.SetMouseButton(button, action != GLFW_RELEASE);
	}

	void WindowGLFW::MouseMoveCallback(GLFWwindow* window, double posX, double posY)
	{
		GetWindow(window)->inputProvider.mouse.UpdatePosition(posX, posY);
	}

	void WindowGLFW::MouseScrollCallback(GLFWwindow* window, double xOffset, double yOffset)
	{
		GetWindow(window)->inputProvider.mouse.UpdateWheel(xOffset, yOffset);
	}

	void WindowGLFW::MouseEnterExitCallback(GLFWwindow* window, int entered)
	{
		const auto windowInstance = GetWindow(window);
		windowInstance->inputProvider.MouseEnterExitWindow(windowInstance);
		Logger::INPUT->trace("Mouse enter/exit: {}", entered);
	}

	void WindowGLFW::ResizeCallback(GLFWwindow* window, int width, int height)
	{
		GetWindow(window)->OnResize(width, height);
	}

	void WindowGLFW::FocusCallback(GLFWwindow* window, const int focused)
	{
		if (focused == GLFW_TRUE)
			GetWindow(window)->OnFocusGained();
		else
			GetWindow(window)->OnFocusLost();
	}

	void WindowGLFW::MinimizeCallback(GLFWwindow* window, const int minimized)
	{
		if (minimized == GLFW_TRUE)
			GetWindow(window)->OnMinimize();
		else
			GetWindow(window)->OnRestore();
	}

	void WindowGLFW::RefreshCallback(GLFWwindow* window)
	{
		//TODO is there really anything to do? or is it ok if the window is only redrawn on the next frame?
	}

	void WindowGLFW::WindowMoveCallback(GLFWwindow* window, const int posX, const int posY)
	{
		GetWindow(window)->OnMove(posX, posY);
	}

	void WindowGLFW::CloseCallback(GLFWwindow* window)
	{
		GetWindow(window)->OnClose();
	}

	void WindowGLFW::DropCallback(GLFWwindow* window, const int count, const char** paths)
	{
		//TODO something useful
	}

	std::vector<std::string> WindowGLFW::GetVulkanRequiredInstanceExtensions()
	{
		std::vector<std::string> result;
		uint32_t count = 0;
		const char** names = glfwGetRequiredInstanceExtensions(&count);
		if (names && count)
		{
			for (uint32_t i = 0; i < count; ++i)
			{
				result.emplace_back(names[i]);
			}
		}
		return result;
	}
}