/*
 * Copyright 2016 - 2025 Gary Talent (gary@drinkingtea.net). All rights reserved.
 */

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <lodepng.h>
#if TURBINE_USE_IMGUI
#include <imgui_impl_opengl3.h>
#include <imgui_impl_glfw.h>
#endif

#include <keel/keel.hpp>

#include <turbine/turbine.hpp>

#include "config.hpp"
#include "context.hpp"

namespace turbine {

namespace gl {

void addDrawer(Context &ctx, Drawer *cd) noexcept {
	ctx.drawers.emplace_back(cd);
}

void removeDrawer(Context &ctx, Drawer const *cd) noexcept {
	for (auto i = 0u; i < ctx.drawers.size(); ++i) {
		if (ctx.drawers[i] == cd) {
			std::ignore = ctx.drawers.erase(i);
			break;
		}
	}
}

}

#if TURBINE_USE_IMGUI
static void themeImgui() noexcept {
	// Dark Ruda style by Raikiri from ImThemes
	auto &style = ImGui::GetStyle();
	style.Alpha = 1.0;
	style.DisabledAlpha = 0.6000000238418579;
	style.WindowPadding = ImVec2(8.0, 8.0);
	style.WindowRounding = 0.0;
	style.WindowBorderSize = 1.0;
	style.WindowMinSize = ImVec2(32.0, 32.0);
	style.WindowTitleAlign = ImVec2(0.0, 0.5);
	style.WindowMenuButtonPosition = ImGuiDir_Left;
	style.ChildRounding = 0.0;
	style.ChildBorderSize = 1.0;
	style.PopupRounding = 0.0;
	style.PopupBorderSize = 1.0;
	style.FramePadding = ImVec2(4.0, 3.0);
	// custom value
	style.FrameRounding = 3.0;
	style.FrameBorderSize = 0.0;
	style.ItemSpacing = ImVec2(8.0, 4.0);
	style.ItemInnerSpacing = ImVec2(4.0, 4.0);
	style.CellPadding = ImVec2(4.0, 2.0);
	style.IndentSpacing = 21.0;
	style.ColumnsMinSpacing = 6.0;
	style.ScrollbarSize = 14.0;
	style.ScrollbarRounding = 9.0;
	style.GrabMinSize = 10.0;
	style.GrabRounding = 4.0;
	style.TabRounding = 4.0;
	style.TabBorderSize = 0.0;
	style.TabMinWidthForCloseButton = 0.0;
	style.ColorButtonPosition = ImGuiDir_Right;
	style.ButtonTextAlign = ImVec2(0.5, 0.5);
	style.SelectableTextAlign = ImVec2(0.0, 0.0);
	// colors
	constexpr auto imVec4 = [](double r, double g, double b, double a) {
		return ImVec4(
				static_cast<float>(r),
				static_cast<float>(g),
				static_cast<float>(b),
				static_cast<float>(a));
	};
	auto colors = ox::Span(style.Colors);
	colors[ImGuiCol_Text] = imVec4(0.9490196108818054, 0.95686274766922, 0.9764705896377563, 1.0);
	colors[ImGuiCol_TextDisabled] = imVec4(0.3568627536296844, 0.4196078479290009, 0.4666666686534882, 1.0);
	colors[ImGuiCol_WindowBg] = imVec4(0.1098039224743843, 0.1490196138620377, 0.168627455830574, 1.0);
	colors[ImGuiCol_ChildBg] = imVec4(0.1490196138620377, 0.1764705926179886, 0.2196078449487686, 1.0);
	colors[ImGuiCol_PopupBg] = imVec4(0.0784313753247261, 0.0784313753247261, 0.0784313753247261, 0.9399999976158142);
	colors[ImGuiCol_Border] = imVec4(0.0784313753247261, 0.09803921729326248, 0.1176470592617989, 1.0);
	colors[ImGuiCol_BorderShadow] = imVec4(0.0, 0.0, 0.0, 0.0);
	colors[ImGuiCol_FrameBg] = imVec4(0.2000000029802322, 0.2470588237047195, 0.2862745225429535, 1.0);
	colors[ImGuiCol_FrameBgHovered] = imVec4(0.1176470592617989, 0.2000000029802322, 0.2784313857555389, 1.0);
	colors[ImGuiCol_FrameBgActive] = imVec4(0.08627451211214066, 0.1176470592617989, 0.1372549086809158, 1.0);
	colors[ImGuiCol_TitleBg] = imVec4(0.08627451211214066, 0.1176470592617989, 0.1372549086809158, 0.6499999761581421);
	colors[ImGuiCol_TitleBgActive] = imVec4(0.0784313753247261, 0.09803921729326248, 0.1176470592617989, 1.0);
	colors[ImGuiCol_TitleBgCollapsed] = imVec4(0.0, 0.0, 0.0, 0.5099999904632568);
	colors[ImGuiCol_MenuBarBg] = imVec4(0.1490196138620377, 0.1764705926179886, 0.2196078449487686, 1.0);
	colors[ImGuiCol_ScrollbarBg] = imVec4(0.01960784383118153, 0.01960784383118153, 0.01960784383118153, 0.3899999856948853);
	colors[ImGuiCol_ScrollbarGrab] = imVec4(0.2000000029802322, 0.2470588237047195, 0.2862745225429535, 1.0);
	colors[ImGuiCol_ScrollbarGrabHovered] = imVec4(0.1764705926179886, 0.2196078449487686, 0.2470588237047195, 1.0);
	colors[ImGuiCol_ScrollbarGrabActive] = imVec4(0.08627451211214066, 0.2078431397676468, 0.3098039329051971, 1.0);
	colors[ImGuiCol_CheckMark] = imVec4(0.2784313857555389, 0.5568627715110779, 1.0, 1.0);
	colors[ImGuiCol_SliderGrab] = imVec4(0.2784313857555389, 0.5568627715110779, 1.0, 1.0);
	colors[ImGuiCol_SliderGrabActive] = imVec4(0.3686274588108063, 0.6078431606292725, 1.0, 1.0);
	colors[ImGuiCol_Button] = imVec4(0.2000000029802322, 0.2470588237047195, 0.2862745225429535, 1.0);
	colors[ImGuiCol_ButtonHovered] = imVec4(0.2784313857555389, 0.5568627715110779, 1.0, 1.0);
	colors[ImGuiCol_ButtonActive] = imVec4(0.05882352963089943, 0.529411792755127, 0.9764705896377563, 1.0);
	// custom value
	colors[ImGuiCol_Header] = imVec4(0.4000000029802322, 0.4470588237047195, 0.4862745225429535, 0.550000011920929);
	colors[ImGuiCol_HeaderHovered] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.800000011920929);
	colors[ImGuiCol_HeaderActive] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 1.0);
	colors[ImGuiCol_Separator] = imVec4(0.2000000029802322, 0.2470588237047195, 0.2862745225429535, 1.0);
	colors[ImGuiCol_SeparatorHovered] = imVec4(0.09803921729326248, 0.4000000059604645, 0.7490196228027344, 0.7799999713897705);
	colors[ImGuiCol_SeparatorActive] = imVec4(0.09803921729326248, 0.4000000059604645, 0.7490196228027344, 1.0);
	colors[ImGuiCol_ResizeGrip] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.25);
	colors[ImGuiCol_ResizeGripHovered] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.6700000166893005);
	colors[ImGuiCol_ResizeGripActive] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.949999988079071);
	colors[ImGuiCol_Tab] = imVec4(0.1098039224743843, 0.1490196138620377, 0.168627455830574, 1.0);
	colors[ImGuiCol_TabHovered] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.800000011920929);
	colors[ImGuiCol_TabActive] = imVec4(0.2000000029802322, 0.2470588237047195, 0.2862745225429535, 1.0);
	colors[ImGuiCol_TabUnfocused] = imVec4(0.1098039224743843, 0.1490196138620377, 0.168627455830574, 1.0);
	colors[ImGuiCol_TabUnfocusedActive] = imVec4(0.1098039224743843, 0.1490196138620377, 0.168627455830574, 1.0);
	colors[ImGuiCol_PlotLines] = imVec4(0.6078431606292725, 0.6078431606292725, 0.6078431606292725, 1.0);
	colors[ImGuiCol_PlotLinesHovered] = imVec4(1.0, 0.4274509847164154, 0.3490196168422699, 1.0);
	colors[ImGuiCol_PlotHistogram] = imVec4(0.8980392217636108, 0.6980392336845398, 0.0, 1.0);
	colors[ImGuiCol_PlotHistogramHovered] = imVec4(1.0, 0.6000000238418579, 0.0, 1.0);
	colors[ImGuiCol_TableHeaderBg] = imVec4(0.1882352977991104, 0.1882352977991104, 0.2000000029802322, 1.0);
	colors[ImGuiCol_TableBorderStrong] = imVec4(0.3098039329051971, 0.3098039329051971, 0.3490196168422699, 1.0);
	colors[ImGuiCol_TableBorderLight] = imVec4(0.2274509817361832, 0.2274509817361832, 0.2470588237047195, 1.0);
	colors[ImGuiCol_TableRowBg] = imVec4(0.0, 0.0, 0.0, 0.0);
	colors[ImGuiCol_TableRowBgAlt] = imVec4(1.0, 1.0, 1.0, 0.05999999865889549);
	colors[ImGuiCol_TextSelectedBg] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 0.3499999940395355);
	colors[ImGuiCol_DragDropTarget] = imVec4(1.0, 1.0, 0.0, 0.8999999761581421);
	colors[ImGuiCol_NavHighlight] = imVec4(0.2588235437870026, 0.5882353186607361, 0.9764705896377563, 1.0);
	colors[ImGuiCol_NavWindowingHighlight] = imVec4(1.0, 1.0, 1.0, 0.699999988079071);
	colors[ImGuiCol_NavWindowingDimBg] = imVec4(0.800000011920929, 0.800000011920929, 0.800000011920929, 0.2000000029802322);
	colors[ImGuiCol_ModalWindowDimBg] = imVec4(0.800000011920929, 0.800000011920929, 0.800000011920929, 0.3499999940395355);
}
#endif

struct IconData {
	std::vector<uint8_t> pixels;
	int w{}, h{};
};

[[nodiscard]]
[[maybe_unused]]
static ox::Result<IconData> toGlfwImgPixels(ox::SpanView<uint8_t> const &iconPng) noexcept {
	ox::Result<IconData> out;
	unsigned w{}, h{};
	if (lodepng::decode(out.value.pixels, w, h, iconPng.data(), iconPng.size())) {
		return ox::Error{1, "unable to decode window icon PNG data"};
	}
	out.value.w = static_cast<int>(w);
	out.value.h = static_cast<int>(h);
	return out;
}

static void setMandatoryRefreshPeriod(Context &ctx, TimeMs const newVal) {
	ctx.mandatoryRefreshPeriodEnd = ox::max(ctx.mandatoryRefreshPeriodEnd, newVal);
}

ox::Error setWindowIcon(Context &ctx, ox::SpanView<ox::SpanView<uint8_t>> const &iconPngs) noexcept {
	if constexpr(ox::defines::OS != ox::OS::Darwin) {
		ox::Vector<IconData, 8> src;
		ox::Vector<GLFWimage, 8> imgs;
		for (auto const &iconPng : iconPngs) {
			OX_RETURN_ERROR(toGlfwImgPixels(iconPng).moveTo(src.emplace_back()));
			auto &icon = *src.back().unwrap();
			imgs.emplace_back(GLFWimage{
				.width = icon.w,
				.height = icon.h,
				.pixels = icon.pixels.data(),
			});
		}
		glfwSetWindowIcon(ctx.window, static_cast<int>(imgs.size()), imgs.data());
	}
	return {};
}

void setWindowTitle(Context &ctx, ox::StringViewCR title) noexcept {
	auto cstr = ox_malloca(title.bytes() + 1, char);
	OX_ALLOW_UNSAFE_BUFFERS_BEGIN
	ox::strncpy(cstr.get(), title.data(), title.bytes());
	OX_ALLOW_UNSAFE_BUFFERS_END
	glfwSetWindowTitle(ctx.window, cstr.get());
}

void focusWindow(Context &ctx) noexcept {
	glfwFocusWindow(ctx.window);
}

int getScreenWidth(Context const &ctx) noexcept {
	int w = 0, h = 0;
	glfwGetFramebufferSize(ctx.window, &w, &h);
	return w;
}

int getScreenHeight(Context const &ctx) noexcept {
	int w = 0, h = 0;
	glfwGetFramebufferSize(ctx.window, &w, &h);
	return h;
}

ox::Size getScreenSize(Context const &ctx) noexcept {
	int w = 0, h = 0;
	glfwGetFramebufferSize(ctx.window, &w, &h);
	return {w, h};
}

ox::Bounds getWindowBounds(Context const &ctx) noexcept {
	ox::Bounds bnds;
	glfwGetWindowPos(ctx.window, &bnds.x, &bnds.y);
	glfwGetWindowSize(ctx.window, &bnds.width, &bnds.height);
	return bnds;
}

ox::Error setWindowBounds(Context &ctx, ox::Bounds const &bnds) noexcept {
	glfwSetWindowPos(ctx.window, bnds.x, bnds.y);
	glfwSetWindowSize(ctx.window, bnds.width, bnds.height);
	return {};
}

void requireRefreshWithin(Context &ctx, int const ms) noexcept {
	ctx.refreshWithinMs = ox::min(ms, ctx.refreshWithinMs);
}

void requireRefresh(Context &ctx) noexcept {
	setMandatoryRefreshPeriod(ctx, ticksMs(ctx) + config::MandatoryRefreshPeriod);
}

void requireRefreshFor(Context &ctx, int ms) noexcept {
	ms = ox::max(0, ms);
	setMandatoryRefreshPeriod(ctx, ticksMs(ctx) + static_cast<TimeMs>(ms));
}

static void draw(Context &ctx) noexcept {
	// draw start
	for (auto const d : ctx.drawers) {
		d->preDraw(ctx);
	}
#if TURBINE_USE_IMGUI
	ImGui_ImplOpenGL3_NewFrame();
	ImGui_ImplGlfw_NewFrame();
	ImGui::NewFrame();
#endif
	for (auto const d : ctx.drawers) {
		d->draw(ctx);
	}
#if TURBINE_USE_IMGUI
	ImGui::Render();
	ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
#endif
	// draw end
	glfwSwapBuffers(ctx.window);
}

static void handleGlfwResize(GLFWwindow *window, int, int) noexcept {
	auto &ctx = *static_cast<Context*>(glfwGetWindowUserPointer(window));
	draw(ctx);
}

static void handleGlfwError(int const err, char const *desc) noexcept {
	oxErrf("GLFW error ({}): {}\n", err, desc);
}

static auto setKeyDownStatus(Context &ctx, Key const key, bool const down) noexcept {
	ctx.keysDown &= ~(1llu << static_cast<int>(key));
	ctx.keysDown |= static_cast<uint64_t>(down) << static_cast<int>(key);
}

static void handleKeyPress(Context &ctx, int const key, bool const down) noexcept {
	static constexpr auto keyMap = [] {
		ox::Array<Key, GLFW_KEY_LAST> map = {};
		for (auto i = 0u; i < 26; ++i) {
			map[GLFW_KEY_A + i] = static_cast<Key>(static_cast<unsigned>(Key::Alpha_A) + i);
		}
		for (auto i = 0u; i < 10; ++i) {
			map[GLFW_KEY_0 + i] = static_cast<Key>(static_cast<unsigned>(Key::Num_0) + i);
		}
		map[GLFW_KEY_LEFT_ALT]      = Key::Mod_Alt;
		map[GLFW_KEY_RIGHT_ALT]     = Key::Mod_Alt;
		map[GLFW_KEY_LEFT_CONTROL]  = Key::Mod_Ctrl;
		map[GLFW_KEY_RIGHT_CONTROL] = Key::Mod_Ctrl;
		map[GLFW_KEY_LEFT_SUPER]    = Key::Mod_Super;
		map[GLFW_KEY_RIGHT_SUPER]   = Key::Mod_Super;
		map[GLFW_KEY_LEFT_SHIFT]    = Key::Mod_Shift;
		map[GLFW_KEY_RIGHT_SHIFT]   = Key::Mod_Shift;
		map[GLFW_KEY_ESCAPE]        = Key::Escape;
		return map;
	}();
	auto const keyIdx = static_cast<std::size_t>(key);
	if (keyIdx < keyMap.size()) {
		auto const k = keyMap[keyIdx];
		setKeyDownStatus(ctx, k, down);
		auto const eventHandler = keyEventHandler(ctx);
		if (eventHandler) {
			eventHandler(ctx, k, down);
		}
	}
}

static void handleGlfwCursorPosEvent(GLFWwindow*, double, double) noexcept {
}

static void handleGlfwMouseButtonEvent(
	GLFWwindow *window,
	int const btn,
	int const action,
	int) noexcept {
	auto &ctx = *static_cast<Context*>(glfwGetWindowUserPointer(window));
	setMandatoryRefreshPeriod(ctx, ticksMs(ctx) + config::MandatoryRefreshPeriod);
	if (ctx.mouseButtonEventHandler) {
		ctx.mouseButtonEventHandler(ctx, btn, action == 1);
	}
}

static void handleGlfwKeyEvent(GLFWwindow *window, int const key, int, int const action, int) noexcept {
	auto &ctx = *static_cast<Context*>(glfwGetWindowUserPointer(window));
	setMandatoryRefreshPeriod(ctx, ticksMs(ctx) + config::MandatoryRefreshPeriod);
	if (action == GLFW_PRESS) {
		handleKeyPress(ctx, key, true);
	} else if (action == GLFW_RELEASE) {
		handleKeyPress(ctx, key, false);
	}
}

static void handleGlfwWindowCloseEvent(GLFWwindow *window) noexcept {
	auto &ctx = *static_cast<Context*>(glfwGetWindowUserPointer(window));
	setMandatoryRefreshPeriod(ctx, ticksMs(ctx) + config::MandatoryRefreshPeriod);
	ctx.running = ctx.shutdownHandler ? !ctx.shutdownHandler(ctx) : false;
	glfwSetWindowShouldClose(window, !ctx.running);
	glfwPostEmptyEvent();
}

ox::Result<ox::UPtr<Context>> init(
		ox::UPtr<ox::FileSystem> &&fs,
		ox::StringViewCR appName,
		ox::StringViewCR appId) noexcept {
	auto ctx = ox::make_unique<Context>();
	OX_RETURN_ERROR(keel::init(ctx->keelCtx, std::move(fs), appName));
	using namespace std::chrono;
	ctx->startTime = static_cast<TimeMs>(
			duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count());
	setMandatoryRefreshPeriod(*ctx, ticksMs(*ctx) + config::MandatoryRefreshPeriod);
	// init GLFW context
	glfwInit();
	glfwWindowHint(GLFW_SCALE_TO_MONITOR, GLFW_TRUE);
	glfwSetErrorCallback(handleGlfwError);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
	glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
	glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
	{
		auto appIdCstr = ox_malloca(appId.bytes() + 1, char);
		OX_ALLOW_UNSAFE_BUFFERS_BEGIN
		ox::strncpy(appIdCstr.get(), appId.data(), appId.bytes());
		OX_ALLOW_UNSAFE_BUFFERS_END
		glfwWindowHintString(GLFW_WAYLAND_APP_ID, appIdCstr.get());
	}
	constexpr auto Scale = 5;
	ctx->window = glfwCreateWindow(
		240 * Scale, 160 * Scale, ctx->keelCtx.appName.c_str(), nullptr, nullptr);
	//ctx.window = glfwCreateWindow(876, 743, ctx.keelCtx.appName.c_str(), nullptr, nullptr);
	if (ctx->window == nullptr) {
		return ox::Error(1, "Could not open GLFW window");
	}
	glfwSetCursorPosCallback(ctx->window, handleGlfwCursorPosEvent);
	glfwSetMouseButtonCallback(ctx->window, handleGlfwMouseButtonEvent);
	glfwSetKeyCallback(ctx->window, handleGlfwKeyEvent);
	glfwSetWindowCloseCallback(ctx->window, handleGlfwWindowCloseEvent);
	glfwSetWindowSizeCallback(ctx->window, handleGlfwResize);
	glfwSetWindowUserPointer(ctx->window, ctx.get());
	glfwMakeContextCurrent(ctx->window);
	if (!gladLoadGLES2Loader(reinterpret_cast<GLADloadproc>(glfwGetProcAddress))) {
		return ox::Error(2, "Could not init Glad");
	}
	// init ImGui
#if TURBINE_USE_IMGUI
	IMGUI_CHECKVERSION();
	ImGui::CreateContext();
	auto &io = ImGui::GetIO();
	io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
	//io.MouseDrawCursor = true;
	ImGui_ImplGlfw_InitForOpenGL(ctx->window, true);
	ImGui_ImplOpenGL3_Init();
	io.IniFilename = nullptr;
	themeImgui();
	float xscale{}, yscale{};
	glfwGetWindowContentScale(ctx->window, &xscale, &yscale);
	ctx->scale = isWayland() || ox::defines::OS == ox::OS::Darwin ?
		1.f : ox::max(xscale, yscale);
	io.DisplayFramebufferScale = ImVec2{ctx->scale, ctx->scale};
	auto &style = ImGui::GetStyle();
	style.ScaleAllSizes(ctx->scale);
#endif
	return ctx;
}

static void tickFps(Context &ctx, uint64_t const nowMs) noexcept {
	++ctx.draws;
	if (ctx.draws >= 500) {
		auto const duration = static_cast<double>(nowMs - ctx.prevFpsCheckTime) / 1000.0;
		auto const fps = static_cast<int>(static_cast<double>(ctx.draws) / duration);
		if constexpr(config::GlFpsPrint) {
			oxOutf("FPS: {}\n", fps);
		}
		oxTracef("turbine.fps", "FPS: {}", fps);
		ctx.prevFpsCheckTime = nowMs;
		ctx.draws = 0;
	}
}

static void shutdown(Context &ctx) noexcept {
	if (ctx.window) {
#if TURBINE_USE_IMGUI
		ImGui_ImplOpenGL3_Shutdown();
		ImGui_ImplGlfw_Shutdown();
#endif
		glfwDestroyWindow(ctx.window);
		ctx.window = nullptr;
	}
}

ox::Error run(Context &ctx) noexcept {
	uint64_t sleepTime = 0;
	ctx.running = true;
	while (ctx.running) {
		ctx.refreshWithinMs = 10 * 1000; // refresh within 10 seconds
		glfwPollEvents();
		auto const ticks = ticksMs(ctx);
		if (ctx.wakeupTime <= ticks) {
			auto const st = ctx.updateHandler(ctx);
			if (st >= 0) {
				ctx.wakeupTime = ticks + static_cast<unsigned>(st);
				sleepTime = static_cast<uint64_t>(st);
			} else {
				ctx.wakeupTime = ~uint64_t{0};
				sleepTime = ctx.wakeupTime - ticks;
			}
		} else {
			sleepTime = ctx.wakeupTime - ticks;
		}
		tickFps(ctx, ticks);
		draw(ctx);
		auto const realSleepTime = ox::min(static_cast<uint64_t>(ctx.refreshWithinMs), sleepTime);
		if (realSleepTime && ctx.mandatoryRefreshPeriodEnd <= ticks) {
			glfwWaitEventsTimeout(static_cast<double>(realSleepTime) / 1000);
		}
	}
	shutdown(ctx);
	return {};
}

TimeMs ticksMs(Context const &ctx) noexcept {
	using namespace std::chrono;
	auto const now = duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
	return static_cast<TimeMs>(now) - ctx.startTime;
}

bool buttonDown(Context const &ctx, Key const key) noexcept {
	return (ctx.keysDown >> static_cast<int>(key)) & 1;
}

void requestShutdown(Context &ctx, bool const force) noexcept {
	glfwSetWindowShouldClose(ctx.window, true);
	if (force) {
		ctx.running = false;
	} else {
		ctx.running = ctx.shutdownHandler ? !ctx.shutdownHandler(ctx) : false;
	}
}

void setShutdownHandler(Context &ctx, ShutdownHandler const handler) noexcept {
	ctx.shutdownHandler = handler;
}

void setUpdateHandler(Context &ctx, UpdateHandler const h) noexcept {
	ctx.updateHandler = h;
}

void setKeyEventHandler(Context &ctx, KeyEventHandler const h) noexcept {
	ctx.keyEventHandler = h;
}

void setMouseButtonEventHandler(Context &ctx, MouseButtonEventHandler const h) noexcept {
	ctx.mouseButtonEventHandler = h;
}

KeyEventHandler keyEventHandler(Context const &ctx) noexcept {
	return ctx.keyEventHandler;
}

float scale(Context const &ctx) noexcept {
	float x{}, y{};
	glfwGetWindowContentScale(ctx.window, &x, &y);
	return y;
}

bool isWayland() noexcept {
	return glfwGetPlatform() == GLFW_PLATFORM_WAYLAND;
}

}
