#include "./sdl_service.hpp"

#include <tracy/Tracy.hpp>

#ifdef MM_OPENGL_3_GLES
	//#include <SDL_opengles2_gl2.h>
	#include <GLES3/gl3.h>
#else
	#include <glad/glad.h>
#endif

#ifndef MM_OPENGL_3_GLES
	#include <tracy/TracyOpenGL.hpp>
#else
	#define TracyGpuContext
#endif

#include <string>
#include <algorithm>

#include <mm/logger.hpp>
#define LOG_CRIT(...)		__LOG_CRIT(	"SDLService", __VA_ARGS__)
#define LOG_ERROR(...)		__LOG_ERROR("SDLService", __VA_ARGS__)
#define LOG_WARN(...)		__LOG_WARN(	"SDLService", __VA_ARGS__)
#define LOG_INFO(...)		__LOG_INFO(	"SDLService", __VA_ARGS__)
#define LOG_DEBUG(...)		__LOG_DEBUG("SDLService", __VA_ARGS__)
#define LOG_TRACE(...)		__LOG_TRACE("SDLService", __VA_ARGS__)

namespace MM::Services {

SDLService::SDLService(uint32_t _sdl_init_flags) {
	MM::Logger::initSectionLogger("SDLService");

#ifdef __EMSCRIPTEN__
	_sdl_init_flags &= ~SDL_INIT_HAPTIC;
#endif

#if 1 // hack for mingw + wine
	_sdl_init_flags &= ~SDL_INIT_SENSOR;
#endif

	sdl_init_flags = _sdl_init_flags;
}

SDLService::~SDLService(void) {
}

bool SDLService::enable(Engine&, std::vector<UpdateStrategies::TaskInfo>& task_array) {
	LOG_TRACE("enabling SDLService...");

	SDL_LogSetOutputFunction(
		+[](void*, int category, SDL_LogPriority priority, const char* message) {
			spdlog::get("SDLService")->log(spdlog::level::level_enum(priority-1), "cat {}: {}", category, message);
		},
		nullptr
	);

	SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);


	if (SDL_Init(sdl_init_flags)) {
		LOG_CRIT("SDL_Init() failed: {}", SDL_GetError());
		return false;
	}

	// add task
	task_array.push_back(
		UpdateStrategies::TaskInfo{"SDLService::events"}
		.phase(UpdateStrategies::update_phase_t::PRE)
		.fn([this](Engine& e) { this->processEvents(e); })
	);

	return true;
}

void SDLService::disable(Engine&) {
	LOG_TRACE("disabling SDLService...");

	// destroy stuff
	if (gl_context) {
		SDL_GL_DeleteContext(gl_context);
	}

	if (win) {
		LOG_WARN("destroying open window");
		destroyWindow();
	}

	SDL_Quit();
}

bool SDLService::createWindow(const char* title, int screen_width, int screen_height, uint32_t sdl_window_flags) {
	if (!win) {
		win = SDL_CreateWindow(title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, screen_width, screen_height, sdl_window_flags);
		if (setDefaultWindowIcon) {
			setDefaultIcon();
		}
	} else {
		LOG_WARN("tried to create window while already existing. ignoring");
	}
	return win; // implicit conversion to bool
}

void SDLService::destroyWindow(void) {
	if (win) {
		SDL_DestroyWindow(win);
		win = nullptr;
	} else {
		LOG_WARN("tried to destroy window while no window exists. ignoring");
	}
}

std::pair<int, int> SDLService::getWindowSize(SDL_Window* win_ptr) {
	if (!win_ptr)
		win_ptr = win; // defaulting to currently managed window
	if (!win_ptr) {
		LOG_ERROR("cannot get size of nullptr window");
		return {0, 0};
	}
	int w, h;
	//SDL_GetWindowSize(win_ptr, &w, &h); // does not support hdpi
	SDL_GL_GetDrawableSize(win_ptr, &w, &h);
	return {w, h};
}

bool SDLService::createGLContext(SDL_Window* win_ptr) {
	if (!win_ptr)
		win_ptr = win; // defaulting to currently managed window

	if (!win_ptr) {
		LOG_ERROR("cannot create gl context with nullptr window");
		return false;
	}

	gl_context = SDL_GL_CreateContext(win_ptr);
	if (!gl_context) {
		LOG_ERROR("SDL_GL_CreateContext(): {}", SDL_GetError());
		return false;
	}

#ifndef MM_OPENGL_3_GLES
	// glad
	if (!gladLoadGLLoader((GLADloadproc) SDL_GL_GetProcAddress)) {
		LOG_ERROR("Failed to initialize OpenGL context");
		return false;
	}
#endif

	if (SDL_GL_MakeCurrent(win, gl_context)) {
		LOG_ERROR("SDL_GL_MakeCurrent(): {}", SDL_GetError());
		return false;
	}

	TracyGpuContext;

	LOG_INFO("gl: {}", reinterpret_cast<const char*>(glGetString(GL_VERSION)));

	return true;
}

bool SDLService::createGLWindow(const char* title, int screen_width, int screen_height, uint32_t sdl_window_flags) {
#ifdef MM_OPENGL_3_GLES
	// opengl es 3.0
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
#else
	//Use OpenGL 3.3 core
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
	SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
#endif
	if (!createWindow(title, screen_width, screen_height, SDL_WINDOW_OPENGL | sdl_window_flags)) {
		return false;
	}
	if (!createGLContext()) {
		return false;
	}
	return true;
}

void SDLService::processEvents(Engine& engine) {
	ZoneScoped;
	SDL_Event e;
	while (SDL_PollEvent(&e)) {
		if (e.type == SDL_QUIT) {
			engine.stop();
		} else {
			for (auto& h : _event_handler_list) {
				if ((*h)(e))
					break;
			}
		}
	}
}

SDLService::EventHandlerHandle SDLService::addEventHandler(std::function<bool(const SDL_Event&)> function) {
	if (!function) {
		// error
		return nullptr;
	}

	auto& r = _event_handler_list.emplace_back(std::make_unique<EventHandlerType>(function));

	return r.get();
}

void SDLService::removeEventHandler(EventHandlerHandle function_handle) {
	if (!function_handle) {
		// error
		return;
	}

	_event_handler_list.erase(
		std::remove_if(_event_handler_list.begin(), _event_handler_list.end(),
			[&](std::unique_ptr<EventHandlerType>& eh) { return function_handle == eh.get(); }
		),
		_event_handler_list.end()
	);
}


void SDLService::setDefaultIcon(void) {
	if (!win) {
		LOG_WARN("tried to set icon of NO window");
		return;
	}
	#include "../logo-f6c-square.png.h"

	setIcon(
		(void*)logo_f6c_square.pixel_data,
		logo_f6c_square.width,
		logo_f6c_square.height,
		logo_f6c_square.bytes_per_pixel
	);
}

void SDLService::setIcon(SDL_Surface* surf) {
	if (!win) {
		LOG_WARN("tried to set icon of NO window");
		return;
	}

	SDL_SetWindowIcon(win, surf);
}

void SDLService::setIcon(void* data, size_t width, size_t height, size_t bytes_per_pixel) {
	if (!win) {
		LOG_WARN("tried to set icon of NO window");
		return;
	}

	// thanks to Danial at https://blog.gibson.sh/2015/04/13/how-to-integrate-your-sdl2-window-icon-or-any-image-into-your-executable/
	uint32_t rmask, gmask, bmask, amask;
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
	int shift = (bytes_per_pixel == 3) ? 8 : 0;
	rmask = 0xff000000 >> shift;
	gmask = 0x00ff0000 >> shift;
	bmask = 0x0000ff00 >> shift;
	amask = 0x000000ff >> shift;
#else // little endian, like x86
	rmask = 0x000000ff;
	gmask = 0x0000ff00;
	bmask = 0x00ff0000;
	amask = (bytes_per_pixel == 3) ? 0 : 0xff000000;
#endif

	SDL_Surface* icon = SDL_CreateRGBSurfaceFrom(
		data,
		width, height,
		bytes_per_pixel * 8,
		bytes_per_pixel * width,
		rmask, gmask, bmask, amask);

	setIcon(icon);

	SDL_FreeSurface(icon);
}

} // namespace MM::Services

