#include <PreCompiled.h>
#include <Core/SDLManager.h>
#include <Core/Memory.h>
#include <Embedded/IconMlx.h>
#include <Utils/Bits.h>

namespace mlx
{
	namespace Internal
	{
		struct WindowInfos
		{
			SDL_Window* window;
			SDL_Surface* icon;
		};
	}

	SDLManager* SDLManager::s_instance = nullptr;

	SDLManager::SDLManager()
	{
		MLX_PROFILE_FUNCTION();
		s_instance = this;

		m_drop_sdl_responsability = SDL_WasInit(SDL_INIT_VIDEO) || std::getenv("MLX_HEADLESS_MODE") != nullptr;
		if(m_drop_sdl_responsability) // is case the mlx is running in a sandbox like MacroUnitTester where SDL is already init
			return;
		//SDL_SetMemoryFunctions(MemManager::Get().Malloc, MemManager::Get().Calloc, MemManager::Get().Realloc, MemManager::Get().Free);

		#ifdef FORCE_WAYLAND
			SDL_SetHint(SDL_HINT_VIDEODRIVER, "wayland,x11");
		#endif

		//SDL_SetHintWithPriority(SDL_HINT_SHUTDOWN_DBUS_ON_QUIT, "1", SDL_HINT_OVERRIDE);

		if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_TIMER) != 0)
			FatalError("SDL: unable to init all subsystems; %", SDL_GetError());
		DebugLog("SDL Manager initialized");
	}

	Handle SDLManager::CreateWindow(const mlx_window_create_info* info, std::int32_t& id, bool hidden)
	{
		Internal::WindowInfos* infos = new Internal::WindowInfos;
		Verify(infos != nullptr, "SDL: window allocation failed");

		std::uint32_t flags = 0;
		if(hidden)
			flags |= SDL_WINDOW_HIDDEN;
		else
			flags |= SDL_WINDOW_SHOWN;
		if(info->is_resizable)
			flags |= SDL_WINDOW_RESIZABLE;
		if(info->is_fullscreen)
			flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;

		infos->window = SDL_CreateWindow(info->title, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, info->width, info->height, flags);
		if(!infos->window)
			FatalError("SDL: unable to open a new window; %", SDL_GetError());
		infos->icon = SDL_CreateRGBSurfaceFrom(static_cast<void*>(logo_mlx), logo_mlx_width, logo_mlx_height, 32, 4 * logo_mlx_width, Rmask(), Gmask(), Bmask(), Amask());
		SDL_SetWindowIcon(infos->window, infos->icon);

		m_windows_registry.insert(infos);

		id = SDL_GetWindowID(infos->window);

		return infos;
	}

	void SDLManager::DestroyWindow(Handle window) noexcept
	{
		Verify(m_windows_registry.find(window) != m_windows_registry.end(), "SDL: cannot destroy window; unknown window pointer");

		Internal::WindowInfos* infos = static_cast<Internal::WindowInfos*>(window);
		if(infos->window != nullptr)
			SDL_DestroyWindow(infos->window);
		if(infos->icon != nullptr)
			SDL_FreeSurface(infos->icon);

		m_windows_registry.erase(infos);
		delete infos;
	}

	SDL_Window* SDLManager::GetRawWindow(Handle window) noexcept
	{
		Internal::WindowInfos* infos = static_cast<Internal::WindowInfos*>(window);
		return infos->window;
	}

	VkSurfaceKHR SDLManager::CreateVulkanSurface(Handle window, VkInstance instance) const noexcept
	{
		SDL_SysWMinfo info;
		SDL_VERSION(&info.version);
		if(SDL_GetWindowWMInfo(static_cast<Internal::WindowInfos*>(window)->window, &info) != SDL_TRUE)
			FatalError("SDL Manager: cannot retrieve window informations");

		switch(info.subsystem)
		{
			#ifdef SDL_VIDEO_DRIVER_WINDOWS
				case SDL_SYSWM_WINDOWS: return kvfCreateSurfaceKHR(instance, KVF_SURFACE_WINDOWS, static_cast<void*>(info.info.win.hinstance), static_cast<void*>(info.info.win.window));
			#endif
			#ifdef SDL_VIDEO_DRIVER_X11
				case SDL_SYSWM_X11: return kvfCreateSurfaceKHR(instance, KVF_SURFACE_XLIB, static_cast<void*>(info.info.x11.display), static_cast<void*>(&info.info.x11.window));
			#endif
			#ifdef SDL_VIDEO_DRIVER_COCOA
				case SDL_SYSWM_COCOA: FatalError("SDL Manager: Vulkan surfaces over Metal are not supported yet. Please fill an issue here https://github.com/seekrs/MacroLibX/issues/new");
			#endif
			#ifdef SDL_VIDEO_DRIVER_WAYLAND
				case SDL_SYSWM_WAYLAND: return kvfCreateSurfaceKHR(instance, KVF_SURFACE_WAYLAND, static_cast<void*>(info.info.wl.display), static_cast<void*>(info.info.wl.surface));
			#endif
			#ifdef SDL_VIDEO_DRIVER_ANDROID
				case SDL_SYSWM_ANDROID: return kvfCreateSurfaceKHR(instance, KVF_SURFACE_ANDROID, nullptr, static_cast<void*>(info.info.android.window));
			#endif

			default : FatalError("SDL Manager: unsupported windowing system"); break;
		}
		return VK_NULL_HANDLE;
	}

	Vec2ui SDLManager::GetVulkanDrawableSize(Handle window) const noexcept
	{
		Vec2i extent;
		SDL_GetWindowSize(static_cast<Internal::WindowInfos*>(window)->window, &extent.x, &extent.y);
		return Vec2ui{ extent };
	}

	void SDLManager::MoveMouseOnWindow(Handle window, int x, int y) const noexcept
	{
		SDL_WarpMouseInWindow(static_cast<Internal::WindowInfos*>(window)->window, x, y);
		SDL_PumpEvents();
	}

	void SDLManager::GetScreenSizeWindowIsOn(Handle window, int* x, int* y) const noexcept
	{
		SDL_DisplayMode DM;
		SDL_GetDesktopDisplayMode(SDL_GetWindowDisplayIndex(static_cast<Internal::WindowInfos*>(window)->window), &DM);
		*x = DM.w;
		*y = DM.h;
	}

	void SDLManager::SetWindowPosition(Handle window, int x, int y) const noexcept
	{
		SDL_SetWindowPosition(static_cast<Internal::WindowInfos*>(window)->window, x, y);
	}

	void SDLManager::SetWindowSize(Handle window, int x, int y) const noexcept
	{
		SDL_SetWindowSize(static_cast<Internal::WindowInfos*>(window)->window, x, y);
	}

	void SDLManager::SetWindowTitle(Handle window, std::string_view title) const noexcept
	{
		SDL_SetWindowTitle(static_cast<Internal::WindowInfos*>(window)->window, title.data());
	}

	void SDLManager::SetWindowFullscreen(Handle window, bool enable) const noexcept
	{
		SDL_SetWindowFullscreen(static_cast<Internal::WindowInfos*>(window)->window, (enable ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0));
	}

	void SDLManager::SetWindowMaxSize(Handle window, int x, int y) const noexcept
	{
		SDL_SetWindowMaximumSize(static_cast<Internal::WindowInfos*>(window)->window, x, y);
	}

	void SDLManager::SetWindowMinSize(Handle window, int x, int y) const noexcept
	{
		SDL_SetWindowMinimumSize(static_cast<Internal::WindowInfos*>(window)->window, x, y);
	}

	void SDLManager::MaximizeWindow(Handle window) const noexcept
	{
		SDL_MaximizeWindow(static_cast<Internal::WindowInfos*>(window)->window);
	}

	void SDLManager::MinimizeWindow(Handle window) const noexcept
	{
		SDL_MinimizeWindow(static_cast<Internal::WindowInfos*>(window)->window);
	}

	void SDLManager::RestoreWindow(Handle window) const noexcept
	{
		SDL_RestoreWindow(static_cast<Internal::WindowInfos*>(window)->window);
	}

	void SDLManager::GetWindowPosition(Handle window, int* x, int* y) const noexcept
	{
		SDL_GetWindowPosition(static_cast<Internal::WindowInfos*>(window)->window, x, y);
	}

	void SDLManager::GetWindowSize(Handle window, int* x, int* y) const noexcept
	{
		SDL_GetWindowSize(static_cast<Internal::WindowInfos*>(window)->window, x, y);
	}

	void SDLManager::HideCursor() noexcept
	{
		SDL_ShowCursor(SDL_DISABLE);
	}

	void SDLManager::ShowCursor() noexcept
	{
		SDL_ShowCursor(SDL_ENABLE);
	}

	std::int32_t SDLManager::GetX() const noexcept
	{
		int dummy;
		int x;
		SDL_GetMouseState(&x, &dummy);
		return x;
	}

	std::int32_t SDLManager::GetY() const noexcept
	{
		int dummy;
		int y;
		SDL_GetMouseState(&dummy, &y);
		return y;
	}

	std::int32_t SDLManager::GetXRel() const noexcept
	{
		int dummy;
		int x;
		SDL_GetRelativeMouseState(&x, &dummy);
		return x;
	}

	std::int32_t SDLManager::GetYRel() const noexcept
	{
		int dummy;
		int y;
		SDL_GetRelativeMouseState(&dummy, &y);
		return y;
	}

	void SDLManager::SetInputBinding(std::function<void(SDL_Event*)> functor)
	{
		m_binding_hook = std::move(functor);
	}

	void SDLManager::InputsFetcher(std::function<void(mlx_event_type, int, int)> functor)
	{
		SDL_Event event;
		while(SDL_PollEvent(&event))
		{
			std::uint32_t id = event.window.windowID;
			switch(event.type)
			{
				case SDL_KEYUP: functor(MLX_KEYUP, id, event.key.keysym.scancode); break;
				case SDL_KEYDOWN: functor(MLX_KEYDOWN, id, event.key.keysym.scancode); break;
				case SDL_MOUSEBUTTONUP: functor(MLX_MOUSEUP, id, event.button.button); break;
				case SDL_MOUSEBUTTONDOWN: functor(MLX_MOUSEDOWN, id, event.button.button); break;
				case SDL_MOUSEWHEEL:
				{
					if(event.wheel.y > 0) // scroll up
						functor(MLX_MOUSEWHEEL, id, 1);
					else if(event.wheel.y < 0) // scroll down
						functor(MLX_MOUSEWHEEL, id, 2);
					if(event.wheel.x > 0) // scroll right
						functor(MLX_MOUSEWHEEL, id, 3);
					else if(event.wheel.x < 0) // scroll left
						functor(MLX_MOUSEWHEEL, id, 4);
					break;
				}
				case SDL_WINDOWEVENT:
				{
					switch(event.window.event)
					{
						case SDL_WINDOWEVENT_CLOSE: functor(MLX_WINDOW_EVENT, id, 0); break;
						case SDL_WINDOWEVENT_MOVED: functor(MLX_WINDOW_EVENT, id, 1); break;
						case SDL_WINDOWEVENT_MINIMIZED: functor(MLX_WINDOW_EVENT, id, 2); break;
						case SDL_WINDOWEVENT_MAXIMIZED: functor(MLX_WINDOW_EVENT, id, 3); break;
						case SDL_WINDOWEVENT_ENTER: functor(MLX_WINDOW_EVENT, id, 4); break;
						case SDL_WINDOWEVENT_FOCUS_GAINED: functor(MLX_WINDOW_EVENT, id, 5); break;
						case SDL_WINDOWEVENT_LEAVE: functor(MLX_WINDOW_EVENT, id, 6); break;
						case SDL_WINDOWEVENT_FOCUS_LOST: functor(MLX_WINDOW_EVENT, id, 7); break;
						case SDL_WINDOWEVENT_SIZE_CHANGED: functor(MLX_WINDOW_EVENT, id, 8); break;
						case SDL_WINDOWEVENT_RESTORED: functor(MLX_WINDOW_EVENT, id, 11); break;

						default : break;
					}
					break;
				}

				default: break;
			}

			if(m_binding_hook)
				m_binding_hook(&event);
		}
	}

	SDLManager::~SDLManager()
	{
		if(m_drop_sdl_responsability)
			return;

		SDL_QuitSubSystem(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_EVENTS);
		SDL_Quit();
		s_instance = nullptr;
		DebugLog("SDL Manager uninitialized");
	}
}
