#include "LunarWM_wayland.h"

#include "LunarWM_core.h"
#include "LunarWM_render.h"
#include "common.h"
#include "raylib.h"
#include "vec.h"

#include <wlr/render/color.h>
#include <wlr/render/pass.h>

#include <assert.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <xcb/xcb.h>
#include <xcb/xcb_icccm.h>

#include <xkbcommon/xkbcommon-keysyms.h>
#include <xkbcommon/xkbcommon.h>

#include <rlgl.h>

static void handle_new_output(struct wl_listener *listener, void *data);
static void handle_output_frame(struct wl_listener *listener, void *data);
static void handle_output_destroy(struct wl_listener *listener, void *data);

static inline SphericalCoord get_forward_spherical_with_nearest(
    Vector3 fwd, float r)
{
	if (fabs(fwd.y) < 0.2f) {
		fwd.y = 0;
	}
	auto vec = Vector3Scale(Vector3Normalize(fwd), r);
	return Vector3ToSpherical(vec);
}

struct ExternalTexturePipeline {
	bool attempted_init;
	bool ready;
	GLuint program;
	GLint sampler_loc;
	GLuint vao;
	GLuint vbo;
};

static struct ExternalTexturePipeline g_external_pipeline = { 0 };

static GLuint compile_shader(GLenum type, char const *source)
{
	GLuint shader = glCreateShader(type);
	if (shader == 0) {
		TraceLog(
		    LOG_ERROR, "Failed to create shader object for external texture");
		return 0;
	}
	glShaderSource(shader, 1, &source, NULL);
	glCompileShader(shader);
	GLint status = GL_FALSE;
	glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
	if (status != GL_TRUE) {
		GLint log_len = 0;
		glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &log_len);
		if (log_len > 1) {
			char *log = malloc((size_t)log_len);
			if (log) {
				glGetShaderInfoLog(shader, log_len, NULL, log);
				TraceLog(LOG_ERROR,
				    "External texture shader compile failed: %s", log);
				free(log);
			}
		}
		glDeleteShader(shader);
		return 0;
	}
	return shader;
}

static bool ensure_external_pipeline(void)
{
	if (g_external_pipeline.attempted_init)
		return g_external_pipeline.ready;

	g_external_pipeline.attempted_init = true;

	static char const *vertex_src_300
	    = "#version 300 es\n"
	      "precision highp float;\n"
	      "layout(location = 0) in vec2 a_pos;\n"
	      "layout(location = 1) in vec2 a_uv;\n"
	      "out vec2 v_uv;\n"
	      "void main() {\n"
	      "    v_uv = a_uv;\n"
	      "    gl_Position = vec4(a_pos, 0.0, 1.0);\n"
	      "}\n";

	static char const *fragment_src_300
	    = "#version 300 es\n"
	      "#extension GL_OES_EGL_image_external_essl3 : require\n"
	      "precision mediump float;\n"
	      "in vec2 v_uv;\n"
	      "layout(location = 0) out vec4 fragColor;\n"
	      "uniform samplerExternalOES u_texture;\n"
	      "void main() {\n"
	      "    fragColor = texture(u_texture, v_uv);\n"
	      "}\n";

	static char const *vertex_src_100
	    = "#version 100\n"
	      "attribute vec2 a_pos;\n"
	      "attribute vec2 a_uv;\n"
	      "varying vec2 v_uv;\n"
	      "void main() {\n"
	      "    v_uv = a_uv;\n"
	      "    gl_Position = vec4(a_pos, 0.0, 1.0);\n"
	      "}\n";

	static char const *fragment_src_100
	    = "#version 100\n"
	      "#extension GL_OES_EGL_image_external : require\n"
	      "precision mediump float;\n"
	      "varying vec2 v_uv;\n"
	      "uniform samplerExternalOES u_texture;\n"
	      "void main() {\n"
	      "    gl_FragColor = texture2D(u_texture, v_uv);\n"
	      "}\n";

	struct {
		char const *vs;
		char const *fs;
	} const variants[] = {
		{ vertex_src_300, fragment_src_300 },
		{ vertex_src_100, fragment_src_100 },
	};

	GLuint program = 0;
	GLint sampler_loc = -1;
	size_t chosen_idx = (size_t)-1;
	for (size_t i = 0; i < ARRAY_SZ(variants); ++i) {
		GLuint vert = compile_shader(GL_VERTEX_SHADER, variants[i].vs);
		if (vert == 0)
			continue;
		GLuint frag = compile_shader(GL_FRAGMENT_SHADER, variants[i].fs);
		if (frag == 0) {
			glDeleteShader(vert);
			continue;
		}

		GLuint prog = glCreateProgram();
		if (prog == 0) {
			TraceLog(LOG_ERROR,
			    "Failed to create shader program for external texture");
			glDeleteShader(vert);
			glDeleteShader(frag);
			continue;
		}
		glAttachShader(prog, vert);
		glAttachShader(prog, frag);
		glBindAttribLocation(prog, 0, "a_pos");
		glBindAttribLocation(prog, 1, "a_uv");
		glLinkProgram(prog);
		GLint link_status = GL_FALSE;
		glGetProgramiv(prog, GL_LINK_STATUS, &link_status);
		if (link_status != GL_TRUE) {
			GLint log_len = 0;
			glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &log_len);
			if (log_len > 1) {
				char *log = malloc((size_t)log_len);
				if (log) {
					glGetProgramInfoLog(prog, log_len, NULL, log);
					TraceLog(LOG_ERROR,
					    "External texture program link failed: %s", log);
					free(log);
				}
			}
			glDeleteShader(vert);
			glDeleteShader(frag);
			glDeleteProgram(prog);
			continue;
		}
		glDetachShader(prog, vert);
		glDetachShader(prog, frag);
		glDeleteShader(vert);
		glDeleteShader(frag);

		sampler_loc = glGetUniformLocation(prog, "u_texture");
		if (sampler_loc < 0) {
			TraceLog(LOG_ERROR,
			    "External texture program missing u_texture uniform");
			glDeleteProgram(prog);
			continue;
		}

		program = prog;
		chosen_idx = i;
		break;
	}

	if (program == 0)
		return false;

	if (chosen_idx == 1) {
		TraceLog(
		    LOG_DEBUG, "External texture shader fallback to ESSL 100 variant");
	}

	GLuint vao = 0;
	GLuint vbo = 0;
	glGenVertexArrays(1, &vao);
	glGenBuffers(1, &vbo);
	if (vao == 0 || vbo == 0) {
		TraceLog(LOG_ERROR, "Failed to allocate buffers for external texture");
		if (vao)
			glDeleteVertexArrays(1, &vao);
		if (vbo)
			glDeleteBuffers(1, &vbo);
		glDeleteProgram(program);
		return false;
	}

	GLint prev_vao = 0;
	glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &prev_vao);
	GLint prev_array_buffer = 0;
	glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &prev_array_buffer);
	glBindVertexArray(vao);
	glBindBuffer(GL_ARRAY_BUFFER, vbo);
	glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 16, NULL, GL_DYNAMIC_DRAW);
	glEnableVertexAttribArray(0);
	glVertexAttribPointer(
	    0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 4, (void *)0);
	glEnableVertexAttribArray(1);
	glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 4,
	    (void *)(sizeof(float) * 2));
	glBindBuffer(GL_ARRAY_BUFFER, (GLuint)prev_array_buffer);
	glBindVertexArray((GLuint)prev_vao);

	g_external_pipeline.program = program;
	g_external_pipeline.sampler_loc = sampler_loc;
	g_external_pipeline.vao = vao;
	g_external_pipeline.vbo = vbo;
	g_external_pipeline.ready = true;
	return true;
}

static bool draw_external_texture(
    struct wlr_gles2_texture_attribs const *attribs, int tex_width,
    int tex_height, Rectangle src, Rectangle dst, int target_width,
    int target_height)
{
	if (!attribs || attribs->target != GL_TEXTURE_EXTERNAL_OES)
		return false;
	if (tex_width <= 0 || tex_height <= 0 || target_width <= 0
	    || target_height <= 0)
		return false;
	if (!ensure_external_pipeline())
		return false;

	rlDrawRenderBatchActive();

	float x0 = dst.x;
	float y0 = dst.y;
	float x1 = dst.x + dst.width;
	float y1 = dst.y + dst.height;

	float ndc_x0 = (x0 / (float)target_width) * 2.0f - 1.0f;
	float ndc_x1 = (x1 / (float)target_width) * 2.0f - 1.0f;
	float ndc_y0 = 1.0f - (y0 / (float)target_height) * 2.0f;
	float ndc_y1 = 1.0f - (y1 / (float)target_height) * 2.0f;

	float inv_tex_w = 1.0f / (float)tex_width;
	float inv_tex_h = 1.0f / (float)tex_height;
	float u0 = src.x * inv_tex_w;
	float v0 = src.y * inv_tex_h;
	float u1 = (src.x + src.width) * inv_tex_w;
	float v1 = (src.y + src.height) * inv_tex_h;

	GLfloat vertices[] = {
		ndc_x0,
		ndc_y0,
		u0,
		v0,
		ndc_x1,
		ndc_y0,
		u1,
		v0,
		ndc_x0,
		ndc_y1,
		u0,
		v1,
		ndc_x1,
		ndc_y1,
		u1,
		v1,
	};

	GLint prev_program = 0;
	glGetIntegerv(GL_CURRENT_PROGRAM, &prev_program);

	GLint prev_active_texture = 0;
	glGetIntegerv(GL_ACTIVE_TEXTURE, &prev_active_texture);
	glActiveTexture(GL_TEXTURE0);
	GLint prev_tex0_external = 0;
	glGetIntegerv(GL_TEXTURE_BINDING_EXTERNAL_OES, &prev_tex0_external);
	GLint prev_tex0_2d = 0;
	glGetIntegerv(GL_TEXTURE_BINDING_2D, &prev_tex0_2d);

	GLint prev_array_buffer = 0;
	glGetIntegerv(GL_ARRAY_BUFFER_BINDING, &prev_array_buffer);
	GLint prev_element_array_buffer = 0;
	glGetIntegerv(GL_ELEMENT_ARRAY_BUFFER_BINDING, &prev_element_array_buffer);
	GLint prev_vertex_array = 0;
	glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &prev_vertex_array);

	glUseProgram(g_external_pipeline.program);
	glUniform1i(g_external_pipeline.sampler_loc, 0);
	glBindTexture(GL_TEXTURE_EXTERNAL_OES, attribs->tex);
	glBindVertexArray(g_external_pipeline.vao);
	glBindBuffer(GL_ARRAY_BUFFER, g_external_pipeline.vbo);
	glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices);
	glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

	glBindBuffer(GL_ARRAY_BUFFER, (GLuint)prev_array_buffer);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, (GLuint)prev_element_array_buffer);
	glBindVertexArray((GLuint)prev_vertex_array);
	glBindTexture(GL_TEXTURE_EXTERNAL_OES, (GLuint)prev_tex0_external);
	glBindTexture(GL_TEXTURE_2D, (GLuint)prev_tex0_2d);
	glUseProgram((GLuint)prev_program);
	glActiveTexture((GLenum)prev_active_texture);

	return true;
}

static void LunarWM_Toplevel_release_surface_rt(LunarWM_Toplevel *tl)
{
	if (!tl)
		return;
	if (IsRenderTextureValid(tl->surface_rt)) {
		UnloadRenderTexture(tl->surface_rt);
	}
	tl->surface_rt = (RenderTexture2D) { 0 };
	tl->surface_extents = (struct wlr_box) { 0 };
	tl->composed_has_alpha = false;
}

static void remove_windows_for_tl(LunarWM *wm, LunarWM_Toplevel *tl)
{
	if (!wm || !tl)
		return;
	for (size_t ws = 0; ws < ARRAY_SZ(wm->wm.workspaces); ++ws) {
		auto *vec = &wm->wm.workspaces[ws].v_windows;
		for (size_t i = 0; i < vector_size(*vec);) {
			if ((*vec)[i].tl == tl) {
				vector_remove(*vec, i);
			} else {
				i++;
			}
		}
	}
}

static void focus_fallback(LunarWM *wm);

static void toplevel_commit_notify(struct wl_listener *l, void *)
{
	auto *tl = wl_container_of(l, (LunarWM_Toplevel *)(NULL), commit);

	if (!tl || !tl->surface)
		return;

	if (!tl->is_xwayland) {
		if (tl->u.xdg && tl->u.xdg->base->initial_commit) {
			wlr_xdg_toplevel_set_size(tl->u.xdg, 0, 0);
			return;
		}
	}

	LunarWM_Toplevel_update(tl);
}

static void toplevel_destroy_notify(struct wl_listener *l, void *)
{
	auto *tl = wl_container_of(l, (LunarWM_Toplevel *)(NULL), destroy);

	for (size_t i = 0; i < vector_size(tl->server->wayland.v_toplevels); i++) {
		if (tl == tl->server->wayland.v_toplevels[i]) {
			vector_remove(tl->server->wayland.v_toplevels, i);
			break;
		}
	}

	remove_windows_for_tl(tl->server, tl);

	focus_fallback(tl->server);

	LunarWM_Toplevel_destroy(tl);
	free(tl);
}

static void toplevel_map_notify(struct wl_listener *l, void *data)
{
	(void)data;
	LunarWM_Toplevel *tl = wl_container_of(l, (LunarWM_Toplevel *)0, map);
	if (!tl || !tl->surface)
		return;

	if (tl->is_xwayland && tl->u.xwl) {
		if (tl->u.xwl->override_redirect
		    && !wlr_xwayland_surface_override_redirect_wants_focus(tl->u.xwl)) {
			return;
		}
	}

	LunarWM_Toplevel_focus(tl);

	LunarWM *wm = tl->server;
	for (size_t i = 0; i < vector_size(wm->wayland.v_toplevels); ++i) {
		if (wm->wayland.v_toplevels[i] == tl) {
			wm->wayland.current_focus = (int)i;
			break;
		}
	}
}

static void toplevel_unmap_notify(struct wl_listener *l, void *data)
{
	(void)data;
	LunarWM_Toplevel *tl = wl_container_of(l, tl, unmap);
	if (!tl || !tl->surface)
		return;

	if (tl->map.link.prev || tl->map.link.next)
		wl_list_remove(&tl->map.link);
	if (tl->unmap.link.prev || tl->unmap.link.next)
		wl_list_remove(&tl->unmap.link);
	if (tl->commit.link.prev || tl->commit.link.next)
		wl_list_remove(&tl->commit.link);

	if (tl->locked_buffer) {
		wlr_buffer_unlock(tl->locked_buffer);
		tl->locked_buffer = NULL;
	}
	tl->texture = NULL;
	tl->gles_texture = NULL;
	tl->rl_texture = (Texture) { 0 };
	LunarWM_Toplevel_release_surface_rt(tl);
	tl->surface = NULL;
	tl->composed_has_alpha = false;

	focus_fallback(tl->server);
}

bool LunarWM_Toplevel_init_xdg(
    LunarWM_Toplevel *tl, LunarWM *wm, struct wlr_xdg_toplevel *xdg)
{
	tl->id = LunarWM_get_new_id(wm);
	tl->server = wm;
	tl->is_xwayland = false;
	tl->u.xdg = xdg;
	tl->surface = xdg->base->surface;

	assert(tl->surface);

	tl->commit.notify = toplevel_commit_notify;
	wl_signal_add(&tl->surface->events.commit, &tl->commit);

	tl->destroy.notify = toplevel_destroy_notify;
	wl_signal_add(&tl->surface->events.destroy, &tl->destroy);

	tl->map.notify = toplevel_map_notify;
	wl_signal_add(&tl->u.xdg->base->surface->events.map, &tl->map);

	tl->unmap.notify = toplevel_unmap_notify;
	wl_signal_add(&tl->u.xdg->base->surface->events.unmap, &tl->unmap);

	return true;
}

bool LunarWM_Toplevel_init_xwayland(
    LunarWM_Toplevel *tl, LunarWM *wm, struct wlr_xwayland_surface *xwl)
{
	tl->id = LunarWM_get_new_id(wm);
	tl->server = wm;
	tl->is_xwayland = true;
	tl->u.xwl = xwl;
	tl->surface = xwl->surface;
	assert(tl->surface);

	tl->commit.notify = toplevel_commit_notify;
	wl_signal_add(&tl->surface->events.commit, &tl->commit);

	tl->destroy.notify = toplevel_destroy_notify;
	wl_signal_add(&tl->surface->events.destroy, &tl->destroy);

	tl->map.notify = toplevel_map_notify;
	wl_signal_add(&xwl->surface->events.map, &tl->map);

	tl->unmap.notify = toplevel_unmap_notify;
	wl_signal_add(&xwl->surface->events.unmap, &tl->unmap);

	return true;
}

bool LunarWM_Toplevel_destroy(LunarWM_Toplevel *this)
{
	if (!this)
		return false;
	if (this->map.link.prev || this->map.link.next)
		wl_list_remove(&this->map.link);
	if (this->unmap.link.prev || this->unmap.link.next)
		wl_list_remove(&this->unmap.link);
	if (this->commit.link.prev || this->commit.link.next)
		wl_list_remove(&this->commit.link);
	if (this->destroy.link.prev || this->destroy.link.next)
		wl_list_remove(&this->destroy.link);
	if (this->locked_buffer)
		wlr_buffer_unlock(this->locked_buffer);
	LunarWM_Toplevel_release_surface_rt(this);
	this->composed_has_alpha = false;
	return true;
}

struct SurfaceComposeCtx {
	struct wlr_box extents;
	bool *has_alpha;
};

char const *GLInternalFormatName(GLenum format)
{
	switch (format) {
	case GL_RGBA8:
		return "GL_RGBA8";
	case GL_RGB8:
		return "GL_RGB8";
	case GL_RGB565:
		return "GL_RGB565";
	case GL_RGBA4:
		return "GL_RGBA4";
	case GL_RGB5_A1:
		return "GL_RGB5_A1";
	case GL_DEPTH_COMPONENT16:
		return "GL_DEPTH_COMPONENT16";
	case GL_DEPTH_COMPONENT24:
		return "GL_DEPTH_COMPONENT24";
	case GL_DEPTH24_STENCIL8:
		return "GL_DEPTH24_STENCIL8";
	case GL_DEPTH32F_STENCIL8:
		return "GL_DEPTH32F_STENCIL8";
	case GL_R8:
		return "GL_R8";
	case GL_RG8:
		return "GL_RG8";
	default:
		return "Unknown format";
	}
}

static void surface_compose_draw(
    struct wlr_surface *surface, int sx, int sy, void *user_data)
{
	struct SurfaceComposeCtx *ctx = user_data;
	if (!ctx)
		return;

	struct wlr_texture *wlr_tex = wlr_surface_get_texture(surface);
	if (!wlr_tex || wlr_tex->width == 0 || wlr_tex->height == 0)
		return;

	struct wlr_gles2_texture *gles_tex = gles2_get_texture(wlr_tex);
	if (!gles_tex)
		return;
	if (ctx->has_alpha && gles_tex->has_alpha)
		*ctx->has_alpha = true;

	struct wlr_gles2_texture_attribs attribs;
	wlr_gles2_texture_get_attribs(wlr_tex, &attribs);
	Texture2D tex = {
		.id = (unsigned int)attribs.tex,
		.width = (int)wlr_tex->width,
		.height = (int)wlr_tex->height,
		.mipmaps = 1,
		.format = gles_tex->has_alpha ? PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
		                              : PIXELFORMAT_UNCOMPRESSED_R8G8B8,
	};

	float dest_w = (float)surface->current.width;
	float dest_h = (float)surface->current.height;
	if (dest_w <= 0.0f || dest_h <= 0.0f) {
		dest_w = (float)tex.width;
		dest_h = (float)tex.height;
	}

	Rectangle src = { 0.0f, 0.0f, (float)tex.width, (float)tex.height };
	Rectangle dst = {
		(float)(sx - ctx->extents.x),
		(float)(sy - ctx->extents.y),
		dest_w,
		dest_h,
	};

	if (attribs.target == GL_TEXTURE_EXTERNAL_OES) {
		static bool external_draw_warned = false;
		if (!draw_external_texture(&attribs, tex.width, tex.height, src, dst,
		        ctx->extents.width, ctx->extents.height)) {
			if (!external_draw_warned) {
				TraceLog(LOG_WARNING,
				    "Failed to draw external texture, skipping frame");
				external_draw_warned = true;
			}
		}
	} else {
		assert(attribs.target == GL_TEXTURE_2D);
		DrawTexturePro(tex, src, dst, (Vector2) { 0.0f, 0.0f }, 0.0f, WHITE);
	}
}

bool LunarWM_Toplevel_update(LunarWM_Toplevel *this)
{
	if (!this || !this->surface)
		return false;

	this->texture = wlr_surface_get_texture(this->surface);
	struct wlr_client_buffer *cl_buf = this->surface->buffer;
	if ((this->texture == nullptr) || (cl_buf == nullptr)) {
		return false;
	}

	if ((this->locked_buffer != nullptr)
	    && this->locked_buffer != &cl_buf->base) {
		wlr_buffer_unlock(this->locked_buffer);
		this->locked_buffer = nullptr;
	}
	if (this->locked_buffer == nullptr) {
		this->locked_buffer = wlr_buffer_lock(&cl_buf->base);
	}

	wlr_gles2_texture_get_attribs(this->texture, &this->attribs);
	this->gles_texture = gles2_get_texture(this->texture);
	if (this->gles_texture == nullptr) {
		return false;
	}

	this->rl_texture.id = (unsigned int)this->attribs.tex;
	this->rl_texture.width = (int)this->texture->width;
	this->rl_texture.height = (int)this->texture->height;
	this->rl_texture.mipmaps = 1;
	this->rl_texture.format = this->gles_texture->has_alpha
	    ? PIXELFORMAT_UNCOMPRESSED_R8G8B8A8
	    : PIXELFORMAT_UNCOMPRESSED_R8G8B8;

	SetTextureFilter(this->rl_texture, TEXTURE_FILTER_BILINEAR);
	SetTextureWrap(this->rl_texture, TEXTURE_WRAP_CLAMP);

	struct wlr_box extents = { 0 };
	wlr_surface_get_extents(this->surface, &extents);
	if (extents.width <= 0 || extents.height <= 0) {
		LunarWM_Toplevel_release_surface_rt(this);
		this->composed_has_alpha = this->gles_texture->has_alpha;
		return true;
	}

	bool needs_alpha = this->gles_texture->has_alpha;
	if (extents.x != 0 || extents.y != 0
	    || extents.width != (int)this->texture->width
	    || extents.height != (int)this->texture->height) {
		needs_alpha = true;
	}

	bool const size_changed = !IsRenderTextureValid(this->surface_rt)
	    || this->surface_rt.texture.width != extents.width
	    || this->surface_rt.texture.height != extents.height;
	if (size_changed) {
		LunarWM_Toplevel_release_surface_rt(this);
		this->surface_rt = LoadRenderTexture(extents.width, extents.height);
		if (!IsRenderTextureValid(this->surface_rt)) {
			this->composed_has_alpha = needs_alpha;
			return true;
		}
		SetTextureFilter(this->surface_rt.texture, TEXTURE_FILTER_BILINEAR);
		SetTextureWrap(this->surface_rt.texture, TEXTURE_WRAP_CLAMP);
	}

	BeginTextureMode(this->surface_rt);
	ClearBackground(BLANK);
	BeginBlendMode(BLEND_ALPHA);
	struct SurfaceComposeCtx ctx = {
		.extents = extents,
		.has_alpha = &needs_alpha,
	};
	wlr_surface_for_each_surface(this->surface, surface_compose_draw, &ctx);
	EndBlendMode();
	EndTextureMode();

	this->surface_extents = extents;
	Texture2D composed = this->surface_rt.texture;
	composed.width = extents.width;
	composed.height = extents.height;
	this->rl_texture = composed;
	this->composed_has_alpha = needs_alpha;

	return true;
}

static void clamp_to_hints(
    const struct wlr_xwayland_surface *x, uint16_t *w, uint16_t *h)
{
	xcb_size_hints_t *hints = x->size_hints;
	if (!hints)
		return;

	if ((hints->flags & XCB_ICCCM_SIZE_HINT_P_MIN_SIZE)) {
		if (*w < hints->min_width)
			*w = hints->min_width;
		if (*h < hints->min_height)
			*h = hints->min_height;
	}
	if ((hints->flags & XCB_ICCCM_SIZE_HINT_P_MAX_SIZE)) {
		if (hints->max_width > 0 && *w > hints->max_width)
			*w = hints->max_width;
		if (hints->max_height > 0 && *h > hints->max_height)
			*h = hints->max_height;
	}
	if ((hints->flags & XCB_ICCCM_SIZE_HINT_P_RESIZE_INC)) {
		if (hints->width_inc > 0)
			*w = (*w / hints->width_inc) * hints->width_inc;
		if (hints->height_inc > 0)
			*h = (*h / hints->height_inc) * hints->height_inc;
	}
	if ((hints->flags & XCB_ICCCM_SIZE_HINT_BASE_SIZE)) {
		if (*w < hints->base_width)
			*w = hints->base_width;
		if (*h < hints->base_height)
			*h = hints->base_height;
	}
}

struct XwlHooks {
	LunarWM *wm;
	struct wlr_xwayland_surface *xwl;
	struct wl_listener associate;
	struct wl_listener dissociate;
	struct wl_listener destroy;

	struct wl_listener req_configure;
	struct wl_listener req_maximize;
	struct wl_listener req_fullscreen;
	struct wl_listener req_activate;
	struct wl_listener set_geometry;
};

static void xwayland_ready_notify(struct wl_listener *l, void *data)
{
	(void)data;
	LunarWM *wm = wl_container_of(l, wm, wayland.xwayland_ready);
	wlr_xwayland_set_seat(wm->wayland.xwayland, wm->wayland.seat);
	setenv("DISPLAY", wm->wayland.xwayland->display_name, 1);
}

static void handle_associate(struct wl_listener *ll, void *d)
{
	struct wlr_xwayland_surface *x = d;
	LunarWM *wm2 = wl_container_of(ll, wm2, wayland.xwayland_associate_tmp);
	if (!x->surface)
		return;

	LunarWM_Toplevel *tl = calloc(1, sizeof *tl);
	if (!tl)
		return;

	if (LunarWM_Toplevel_init_xwayland(tl, wm2, x)) {
		vector_add(&wm2->wayland.v_toplevels, tl);
		LunarWM_Window window = {
			.tl = tl,
			.coord = get_forward_spherical_with_nearest(
			    wm2->renderer.camera.target, wm2->cman->cfg.space.radius),
		};
		vector_add(
		    &wm2->wm.workspaces[wm2->wm.active_workspace].v_windows, window);
	} else {
		free(tl);
	}
	wl_list_remove(&wm2->wayland.xwayland_associate_tmp.link);
}

static void handle_dissociate(struct wl_listener *ll, void *d)
{
	struct wlr_xwayland_surface *x = d;
	LunarWM *wm2 = wl_container_of(ll, wm2, wayland.xwayland_dissociate_tmp);
	for (size_t i = 0; i < vector_size(wm2->wayland.v_toplevels); ++i) {
		LunarWM_Toplevel *tl = wm2->wayland.v_toplevels[i];
		if (tl->is_xwayland && tl->u.xwl == x) {
			vector_remove(wm2->wayland.v_toplevels, i);
			remove_windows_for_tl(wm2, tl);
			LunarWM_Toplevel_destroy(tl);
			free(tl);
			break;
		}
	}
	wl_list_remove(&wm2->wayland.xwayland_dissociate_tmp.link);
}

static void xwl_hooks_destroy(struct XwlHooks *h)
{
	if (!h)
		return;
	if (h->associate.link.prev || h->associate.link.next)
		wl_list_remove(&h->associate.link);
	if (h->dissociate.link.prev || h->dissociate.link.next)
		wl_list_remove(&h->dissociate.link);
	if (h->destroy.link.prev || h->destroy.link.next)
		wl_list_remove(&h->destroy.link);
	free(h);
}

static void xwl_on_associate(struct wl_listener *ll, void *data)
{
	struct XwlHooks *h = wl_container_of(ll, h, associate);
	struct wlr_xwayland_surface *x = h->xwl;
	if (!x || !x->surface)
		return;

	LunarWM_Toplevel *tl = calloc(1, sizeof *tl);
	if (!tl)
		return;
	if (LunarWM_Toplevel_init_xwayland(tl, h->wm, x)) {
		vector_add(&h->wm->wayland.v_toplevels, tl);
	} else {
		free(tl);
	}
}

static void xwl_unmap_toplevel(LunarWM_Toplevel *tl)
{
	if (!tl)
		return;

	if (tl->map.link.prev || tl->map.link.next)
		wl_list_remove(&tl->map.link);
	if (tl->unmap.link.prev || tl->unmap.link.next)
		wl_list_remove(&tl->unmap.link);
	if (tl->commit.link.prev || tl->commit.link.next)
		wl_list_remove(&tl->commit.link);
	if (tl->destroy.link.prev || tl->destroy.link.next)
		wl_list_remove(&tl->destroy.link);

	if (tl->locked_buffer) {
		wlr_buffer_unlock(tl->locked_buffer);
		tl->locked_buffer = NULL;
	}

	tl->texture = NULL;
	tl->gles_texture = NULL;
	tl->rl_texture = (Texture) { 0 };
	LunarWM_Toplevel_release_surface_rt(tl);
	tl->surface = NULL;
}

static void xwl_on_dissociate(struct wl_listener *ll, void *data)
{
	struct XwlHooks *h = wl_container_of(ll, h, dissociate);
	LunarWM *wm = h->wm;
	struct wlr_xwayland_surface *x = h->xwl;

	for (size_t i = 0; i < vector_size(wm->wayland.v_toplevels); ++i) {
		LunarWM_Toplevel *tl = wm->wayland.v_toplevels[i];
		if (tl->is_xwayland && tl->u.xwl == x) {
			xwl_unmap_toplevel(tl);
			break;
		}
	}

	focus_fallback(wm);
}

static void xwl_on_request_configure(struct wl_listener *ll, void *data)
{
	struct XwlHooks *xh = wl_container_of(ll, xh, req_configure);
	struct wlr_xwayland_surface_configure_event *ev = data;
	if (!xh->xwl)
		return;

	int16_t x = xh->xwl->x, y = xh->xwl->y;
	uint16_t w = xh->xwl->width, h = xh->xwl->height;

	if (ev->mask & XCB_CONFIG_WINDOW_X)
		x = ev->x;
	if (ev->mask & XCB_CONFIG_WINDOW_Y)
		y = ev->y;
	if (ev->mask & XCB_CONFIG_WINDOW_WIDTH)
		w = ev->width;
	if (ev->mask & XCB_CONFIG_WINDOW_HEIGHT)
		h = ev->height;

	clamp_to_hints(xh->xwl, &w, &h);

	wlr_xwayland_surface_configure(xh->xwl, x, y, w, h);
}

static void xwl_on_request_maximize(struct wl_listener *ll, void *data)
{
	(void)data;
	struct XwlHooks *xh = wl_container_of(ll, xh, req_maximize);
	wlr_xwayland_surface_set_maximized(xh->xwl, true, true);
}

static void xwl_on_request_fullscreen(struct wl_listener *ll, void *data)
{
	(void)data;
	struct XwlHooks *xh = wl_container_of(ll, xh, req_fullscreen);
	wlr_xwayland_surface_set_fullscreen(xh->xwl, true);
}

static void xwl_on_request_activate(struct wl_listener *ll, void *data)
{
	(void)data;
	struct XwlHooks *h = wl_container_of(ll, h, req_activate);
	for (size_t i = 0; i < vector_size(h->wm->wayland.v_toplevels); ++i) {
		LunarWM_Toplevel *tl = h->wm->wayland.v_toplevels[i];
		if (tl->is_xwayland && tl->u.xwl == h->xwl) {
			LunarWM_Toplevel_focus(tl);
			break;
		}
	}
}

static void xwl_on_set_geometry(struct wl_listener *ll, void *data)
{
	(void)ll;
	(void)data;
}

static void xwl_on_destroy(struct wl_listener *ll, void *data)
{
	(void)data;
	struct XwlHooks *xh = wl_container_of(ll, xh, destroy);
	LunarWM *wm = xh->wm;
	struct wlr_xwayland_surface *x = xh->xwl;

	if (xh->req_configure.link.prev)
		wl_list_remove(&xh->req_configure.link);
	if (xh->req_maximize.link.prev)
		wl_list_remove(&xh->req_maximize.link);
	if (xh->req_fullscreen.link.prev)
		wl_list_remove(&xh->req_fullscreen.link);
	if (xh->req_activate.link.prev)
		wl_list_remove(&xh->req_activate.link);
	if (xh->set_geometry.link.prev)
		wl_list_remove(&xh->set_geometry.link);
	if (xh->associate.link.prev)
		wl_list_remove(&xh->associate.link);
	if (xh->dissociate.link.prev)
		wl_list_remove(&xh->dissociate.link);
	if (xh->destroy.link.prev)
		wl_list_remove(&xh->destroy.link);

	for (size_t i = 0; i < vector_size(wm->wayland.v_toplevels); ++i) {
		LunarWM_Toplevel *tl = wm->wayland.v_toplevels[i];
		if (tl->is_xwayland && tl->u.xwl == x) {
			vector_remove(wm->wayland.v_toplevels, i);
			remove_windows_for_tl(wm, tl);
			free(tl);
			break;
		}
	}

	focus_fallback(wm);

	free(xh);
}

static bool xwl_wants_focus(struct wlr_xwayland_surface *x)
{
	if (!x)
		return false;
	if (!x->surface)
		return false;
	if (x->override_redirect
	    && !wlr_xwayland_surface_override_redirect_wants_focus(x)) {
		return false;
	}
	if (x->withdrawn || x->minimized)
		return false;
	return true;
}

static void focus_fallback(LunarWM *wm)
{
	if (!wm || !wm->wayland.seat)
		return;

	for (ssize_t i = (ssize_t)vector_size(wm->wayland.v_toplevels) - 1; i >= 0;
	    --i) {
		LunarWM_Toplevel *cand = wm->wayland.v_toplevels[i];
		if (!cand || !cand->surface)
			continue;

		if (cand->is_xwayland) {
			if (!xwl_wants_focus(cand->u.xwl))
				continue;
		}
		LunarWM_Toplevel_focus(cand);
		wm->wayland.current_focus = (int)i;
		return;
	}

	struct wlr_seat *seat = wm->wayland.seat;
	struct wlr_surface *prev = seat->keyboard_state.focused_surface;
	if (prev) {
		struct wlr_xdg_toplevel *pt;
		struct wlr_xwayland_surface *px;
		if ((pt = wlr_xdg_toplevel_try_from_wlr_surface(prev)))
			wlr_xdg_toplevel_set_activated(pt, false);
		if ((px = wlr_xwayland_surface_try_from_wlr_surface(prev)))
			wlr_xwayland_surface_activate(px, false);
	}
	wlr_seat_keyboard_clear_focus(seat);
	wm->wayland.current_focus = -1;
}

static void xwayland_new_surface_notify(struct wl_listener *l, void *data)
{
	LunarWM *wm = wl_container_of(l, wm, wayland.xwayland_new_surface);
	struct wlr_xwayland_surface *xwl = data;

	if (xwl->surface) {
		LunarWM_Toplevel *tl = calloc(1, sizeof *tl);
		if (tl && LunarWM_Toplevel_init_xwayland(tl, wm, xwl))
			vector_add(&wm->wayland.v_toplevels, tl);
		else
			free(tl);
	}

	struct XwlHooks *h = calloc(1, sizeof *h);
	if (!h)
		return;
	h->wm = wm;
	h->xwl = xwl;

	h->associate.notify = xwl_on_associate;
	wl_signal_add(&xwl->events.associate, &h->associate);

	h->dissociate.notify = xwl_on_dissociate;
	wl_signal_add(&xwl->events.dissociate, &h->dissociate);

	h->req_configure.notify = xwl_on_request_configure;
	wl_signal_add(&xwl->events.request_configure, &h->req_configure);

	h->req_maximize.notify = xwl_on_request_maximize;
	wl_signal_add(&xwl->events.request_maximize, &h->req_maximize);

	h->req_fullscreen.notify = xwl_on_request_fullscreen;
	wl_signal_add(&xwl->events.request_fullscreen, &h->req_fullscreen);

	h->req_activate.notify = xwl_on_request_activate;
	wl_signal_add(&xwl->events.request_activate, &h->req_activate);

	h->set_geometry.notify = xwl_on_set_geometry;
	wl_signal_add(&xwl->events.set_geometry, &h->set_geometry);

	h->destroy.notify = xwl_on_destroy;
	wl_signal_add(&xwl->events.destroy, &h->destroy);

	xwl->data = h;
}

void LunarWM_Toplevel_focus(LunarWM_Toplevel *this)
{
	if (!this)
		return;

	LunarWM *wm = this->server;
	struct wlr_seat *seat = wm->wayland.seat;
	struct wlr_surface *prev_surface = seat->keyboard_state.focused_surface;
	struct wlr_surface *surface = this->surface;
	if (prev_surface == surface) {
		return;
	}
	if (prev_surface) {
		struct wlr_xdg_toplevel *prev_tl
		    = wlr_xdg_toplevel_try_from_wlr_surface(prev_surface);
		if (prev_tl)
			wlr_xdg_toplevel_set_activated(prev_tl, false);
		struct wlr_xwayland_surface *prev_x
		    = wlr_xwayland_surface_try_from_wlr_surface(prev_surface);
		if (prev_x)
			wlr_xwayland_surface_activate(prev_x, false);
	}
	struct wlr_keyboard *keyboard = wlr_seat_get_keyboard(seat);

	if (this->is_xwayland) {
		wlr_xwayland_surface_offer_focus(this->u.xwl);
		wlr_xwayland_surface_activate(this->u.xwl, true);
	} else {
		wlr_xdg_toplevel_set_activated(this->u.xdg, true);
	}

	if (keyboard != NULL) {
		wlr_seat_keyboard_notify_enter(seat, surface, keyboard->keycodes,
		    keyboard->num_keycodes, &keyboard->modifiers);
	}
}

static void Keyboard_modifiers_notify(struct wl_listener *listener, void *)
{
	auto *kbd
	    = wl_container_of(listener, (LunarWM_Keyboard *)(NULL), modifiers);
	if (!kbd->server || !kbd->server->wayland.seat) {
		return;
	}
	wlr_seat_set_keyboard(kbd->server->wayland.seat, kbd->wlr_keyboard);
	wlr_seat_keyboard_notify_modifiers(
	    kbd->server->wayland.seat, &kbd->wlr_keyboard->modifiers);
}

static void Keyboard_key_notify(struct wl_listener *listener, void *data)
{
	auto *kbd = wl_container_of(listener, (LunarWM_Keyboard *)(NULL), key);
	if (!kbd->server || !kbd->server->wayland.seat) {
		return;
	}
	auto *server = kbd->server;
	auto *event = (struct wlr_keyboard_key_event *)data;
	struct wlr_seat *seat = server->wayland.seat;

	uint32_t const keycode = event->keycode + 8;
	xkb_keysym_t const *syms = nullptr;
	int const nsyms
	    = xkb_state_key_get_syms(kbd->wlr_keyboard->xkb_state, keycode, &syms);
	xkb_keysym_t const keysym
	    = xkb_state_key_get_one_sym(kbd->wlr_keyboard->xkb_state, keycode);

	bool handled = false;
	uint32_t const modifiers = wlr_keyboard_get_modifiers(kbd->wlr_keyboard);

	if (event->state == WL_KEYBOARD_KEY_STATE_PRESSED) {
		if (server->wayland.session && keysym >= XKB_KEY_XF86Switch_VT_1
		    && keysym <= XKB_KEY_XF86Switch_VT_12) {
			unsigned const vt = keysym - XKB_KEY_XF86Switch_VT_1 + 1;
			wlr_session_change_vt(server->wayland.session, vt);
			return;
		}

		for (int i = 0; i < server->cman->cfg.keybindings.count; i++) {
			BindingRef ref = server->cman->cfg.keybindings.items[i];
			if (ref.mods_mask == 0 || ref.sym == XKB_KEY_NoSymbol)
				continue;

			bool sym_match = false;
			if (syms && nsyms > 0) {
				xkb_keysym_t want = xkb_keysym_to_lower(ref.sym);
				for (int s = 0; s < nsyms; ++s) {
					if (syms[s] == want) {
						sym_match = true;
						break;
					}
					if (xkb_keysym_to_lower(syms[s]) == want) {
						sym_match = true;
						break;
					}
				}
			}

			if (((modifiers & ref.mods_mask) == ref.mods_mask) && sym_match) {
				config_trigger_ref(
				    server->cman->L, &server->cman->cfg, ref.action_ref);
				handled = true;
				break;
			}
		}
	}

	if (!handled) {
		if (!seat)
			return;
		if (!kbd->wlr_keyboard)
			return;
		wlr_seat_set_keyboard(seat, kbd->wlr_keyboard);
		wlr_seat_keyboard_notify_key(
		    seat, event->time_msec, event->keycode, event->state);
	}
}

static void Keyboard_destroy_notify(struct wl_listener *listener, void *)
{
	auto *p = wl_container_of(listener, (LunarWM_Keyboard *)(NULL), destroy);
	wl_list_remove(&p->modifiers.link);
	wl_list_remove(&p->key.link);
	wl_list_remove(&p->destroy.link);
	wl_list_remove(&p->link);
	free(p);
}

static inline float wrap_pi(float a)
{
	a = fmodf(a + PI, 2.0f * PI);
	if (a < 0.0f)
		a += 2.0f * PI;
	return a - PI;
}

static void Pointer_motion_notify(struct wl_listener *listener, void *data)
{
	LunarWM_Pointer *p
	    = wl_container_of(listener, (LunarWM_Pointer *)NULL, motion);
	if (!p->server || !p->server->wayland.seat)
		return;

	struct wlr_pointer_motion_event *ev = data;

	float dx = (float)ev->delta_x;
	float dy = (float)ev->delta_y;
	if (p->server->cman->cfg.input.mouse.invert_x) {
		dx *= -1;
	}
	if (p->server->cman->cfg.input.mouse.invert_y) {
		dy *= -1;
	}

	wlr_log(WLR_DEBUG, "cursor delta: %f,%f", dx, dy);

	float const R = p->server->cman->cfg.space.radius;
	float const g = 0.0005f;

	float const POLE_GUARD = 6.0f * (float)M_PI / 180.0f;
	float const MIN_SIN = sinf(POLE_GUARD);

	SphericalCoord *coord = &p->server->wm.pointer;

	float theta = coord->theta + dx * g;
	float phi = coord->phi + dy * g;

	theta = wrap_pi(theta);

	float sin_phi = sinf(phi);
	if (fabsf(sin_phi) < MIN_SIN) {
		float sign = sin_phi < 0.0f ? -1.0f : 1.0f;
		phi = asinf(sign * MIN_SIN);
	}

	coord->theta = theta;
	coord->phi = phi;
	coord->r = R;
}

static void Pointer_destroy_notify(struct wl_listener *listener, void *)
{
	auto *kbd = wl_container_of(listener, (LunarWM_Pointer *)(NULL), destroy);
	wl_list_remove(&kbd->motion.link);
	wl_list_remove(&kbd->destroy.link);
	wl_list_remove(&kbd->link);
	free(kbd);
}

static void new_input_listener_notify(struct wl_listener *listener, void *data)
{
	LunarWM *wm = wl_container_of(listener, wm, wayland.new_input_listener);
	auto *dev = (struct wlr_input_device *)data;
	if (dev->type == WLR_INPUT_DEVICE_KEYBOARD) {
		struct wlr_keyboard *wlr_keyboard = wlr_keyboard_from_input_device(dev);

		LunarWM_Keyboard *keyboard = calloc(1, sizeof(*keyboard));
		keyboard->server = wm;
		keyboard->wlr_keyboard = wlr_keyboard;

		struct xkb_rule_names const rule_names = {
			.options = wm->cman->cfg.input.keyboard.xkb_options,
		};

		wlr_log(LOG_INFO, "xkb_options=%s",
		    wm->cman->cfg.input.keyboard.xkb_options);

		struct xkb_context *context = xkb_context_new(XKB_CONTEXT_NO_FLAGS);
		struct xkb_keymap *keymap = xkb_keymap_new_from_names(
		    context, &rule_names, XKB_KEYMAP_COMPILE_NO_FLAGS);

		wlr_keyboard_set_keymap(wlr_keyboard, keymap);
		xkb_keymap_unref(keymap);
		xkb_context_unref(context);
		wlr_keyboard_set_repeat_info(wlr_keyboard, 25, 600);

		keyboard->modifiers.notify = Keyboard_modifiers_notify;
		wl_signal_add(&wlr_keyboard->events.modifiers, &keyboard->modifiers);

		keyboard->key.notify = Keyboard_key_notify;
		wl_signal_add(&wlr_keyboard->events.key, &keyboard->key);

		keyboard->destroy.notify = Keyboard_destroy_notify;
		wl_signal_add(&dev->events.destroy, &keyboard->destroy);

		wlr_seat_set_keyboard(wm->wayland.seat, keyboard->wlr_keyboard);

		wl_list_insert(&wm->wayland.keyboards, &keyboard->link);
	} else if (dev->type == WLR_INPUT_DEVICE_POINTER) {
		struct wlr_pointer *wlr_pointer = wlr_pointer_from_input_device(dev);

		LunarWM_Pointer *pointer = calloc(1, sizeof(*pointer));
		pointer->server = wm;
		pointer->wlr_pointer = wlr_pointer;

		pointer->destroy.notify = Pointer_destroy_notify;
		wl_signal_add(&dev->events.destroy, &pointer->destroy);

		pointer->motion.notify = Pointer_motion_notify;
		wl_signal_add(&wlr_pointer->events.motion, &pointer->motion);

		wl_list_insert(&wm->wayland.pointers, &pointer->link);
	}

	uint32_t caps = WL_SEAT_CAPABILITY_POINTER;
	if (!wl_list_empty(&wm->wayland.keyboards)) {
		caps |= WL_SEAT_CAPABILITY_KEYBOARD;
	}
	assert(wm->wayland.seat);
	wlr_seat_set_capabilities(wm->wayland.seat, caps);
}

static void new_xdg_popup_listener_notify(struct wl_listener *, void *) { }

static void new_xdg_toplevel_listener_notify(
    struct wl_listener *listener, void *data)
{
	LunarWM *wm
	    = wl_container_of(listener, wm, wayland.new_xdg_toplevel_listener);
	auto *xdg_tl = (struct wlr_xdg_toplevel *)data;

	LunarWM_Toplevel *tl = (LunarWM_Toplevel *)calloc(1, sizeof(*tl));
	if (!tl) {
		wlr_log(WLR_ERROR, "oom");
		return;
	}

	if (LunarWM_Toplevel_init_xdg(tl, wm, xdg_tl)) {
		vector_add(&wm->wayland.v_toplevels, tl);
		LunarWM_Window window = {
			.tl = tl,
			.coord = get_forward_spherical_with_nearest(
			    wm->renderer.camera.target, wm->cman->cfg.space.radius),
		};
		vector_add(
		    &wm->wm.workspaces[wm->wm.active_workspace].v_windows, window);
	} else {
		wlr_log(WLR_ERROR, "Failed to initialize Toplevel.");
		free(tl);
	}
}

struct vo_client_res {
	struct wl_resource *res;
	struct wl_list link;
};

static void vo_send_initial(struct virtual_output *vo, struct wl_resource *res)
{
	wl_output_send_geometry(res, vo->x, vo->y, vo->phys_w_mm, vo->phys_h_mm,
	    vo->subpixel, vo->make, vo->model, vo->transform);

	uint32_t flags = WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED;
	wl_output_send_mode(res, flags, vo->width, vo->height, vo->refresh_mhz);

	if (wl_resource_get_version(res) >= 2)
		wl_output_send_scale(res, vo->scale);

	if (wl_resource_get_version(res) >= 4) {
		wl_output_send_name(res, vo->name);
		wl_output_send_description(res, vo->desc);
	}

	if (wl_resource_get_version(res) >= 2)
		wl_output_send_done(res);
}

static void vo_resource_destroy(struct wl_resource *res)
{
	struct vo_client_res *cr = wl_resource_get_user_data(res);
	if (!cr)
		return;
	wl_list_remove(&cr->link);
	free(cr);
}

static void output_release(struct wl_client *client, struct wl_resource *res)
{
	(void)client;
	wl_resource_destroy(res);
}

static const struct wl_output_interface output_impl = {
	.release = output_release,
};

static void vo_bind(
    struct wl_client *client, void *data, uint32_t version, uint32_t id)
{
	struct virtual_output *vo = data;
	struct wl_resource *res
	    = wl_resource_create(client, &wl_output_interface, version, id);
	if (!res)
		return;

	struct vo_client_res *cr = calloc(1, sizeof(*cr));
	if (!cr) {
		wl_resource_destroy(res);
		return;
	}

	cr->res = res;
	wl_resource_set_implementation(res, &output_impl, cr, vo_resource_destroy);
	wl_list_insert(&vo->clients, &cr->link);

	vo_send_initial(vo, res);
}

static void vo_broadcast_mode(struct virtual_output *vo)
{
	struct vo_client_res *cr;
	wl_list_for_each(cr, &vo->clients, link)
	{
		struct wl_resource *res = cr->res;
		uint32_t flags = WL_OUTPUT_MODE_CURRENT | WL_OUTPUT_MODE_PREFERRED;
		wl_output_send_mode(res, flags, vo->width, vo->height, vo->refresh_mhz);
		if (wl_resource_get_version(res) >= 2)
			wl_output_send_done(res);
	}
}

static void vo_broadcast_scale(struct virtual_output *vo)
{
	struct vo_client_res *cr;
	wl_list_for_each(cr, &vo->clients, link)
	{
		struct wl_resource *res = cr->res;
		if (wl_resource_get_version(res) >= 2) {
			wl_output_send_scale(res, vo->scale);
			wl_output_send_done(res);
		}
	}
}

static void vo_destroy(struct virtual_output *vo)
{
	struct vo_client_res *cr, *tmp;
	wl_list_for_each_safe(cr, tmp, &vo->clients, link)
	{
		wl_resource_destroy(cr->res);
	}
	if (vo->global) {
		wl_global_destroy(vo->global);
		vo->global = NULL;
	}
	free(vo);
}

static struct virtual_output *vo_create(struct wl_display *display,
    char const *name, char const *desc, int32_t w, int32_t h,
    int32_t refresh_mhz, int32_t scale, char const *make, char const *model)
{
	struct virtual_output *vo = calloc(1, sizeof *vo);
	if (!vo)
		return NULL;
	vo->display = display;
	wl_list_init(&vo->clients);

	vo->x = 0;
	vo->y = 0;
	vo->phys_w_mm = 0;
	vo->phys_h_mm = 0;
	vo->width = w;
	vo->height = h;
	vo->refresh_mhz = refresh_mhz;
	vo->scale = scale > 0 ? scale : 1;
	vo->subpixel = WL_OUTPUT_SUBPIXEL_UNKNOWN;
	vo->transform = WL_OUTPUT_TRANSFORM_NORMAL;
	vo->make = make ? make : "Lunar";
	vo->model = model ? model : "Virtual";
	vo->name = name;
	vo->desc = desc ? desc : name;

	vo->global
	    = wl_global_create(display, &wl_output_interface, 4, vo, vo_bind);
	if (!vo->global) {
		free(vo);
		return NULL;
	}

	return vo;
}

static void setup_xwayland(LunarWM *this)
{
	this->wayland.xwayland_ready.notify = xwayland_ready_notify;
	wl_signal_add(
	    &this->wayland.xwayland->events.ready, &this->wayland.xwayland_ready);

	this->wayland.xwayland_new_surface.notify = xwayland_new_surface_notify;
	wl_signal_add(&this->wayland.xwayland->events.new_surface,
	    &this->wayland.xwayland_new_surface);
}
static void destroy_output(LunarWM_Output *output)
{
	if (!output) {
		return;
	}

	if (output->frame.link.prev || output->frame.link.next) {
		wl_list_remove(&output->frame.link);
	}
	if (output->destroy.link.prev || output->destroy.link.next) {
		wl_list_remove(&output->destroy.link);
	}

	free(output);
}

static void handle_output_destroy(struct wl_listener *listener, void *data)
{
	(void)data;
	LunarWM_Output *output = wl_container_of(listener, output, destroy);
	LunarWM *wm = output->wm;

	if (wm && wm->wayland.v_outputs) {
		for (size_t i = 0; i < vector_size(wm->wayland.v_outputs); ++i) {
			if (wm->wayland.v_outputs[i] == output) {
				vector_remove(wm->wayland.v_outputs, i);
				break;
			}
		}
	}

	destroy_output(output);
}

static void handle_output_frame(struct wl_listener *listener, void *data)
{
	(void)data;
	LunarWM_Output *output = wl_container_of(listener, output, frame);
	LunarWM *wm = output->wm;
	struct wlr_output *wlr_output = output->wlr_output;

	if (wm->xr.available) {
		wlr_output_schedule_frame(wlr_output);
		return;
	}

	struct wlr_output_state state;
	wlr_output_state_init(&state);

	int width, height;
	wlr_output_effective_resolution(wlr_output, &width, &height);

	struct wlr_render_pass *pass
	    = wlr_output_begin_render_pass(wlr_output, &state, NULL);
	if (pass == NULL) {
		wlr_output_state_finish(&state);
		wlr_output_schedule_frame(wlr_output);
		wlr_log(WLR_ERROR, "Failed to begin render pass for output %s",
		    wlr_output->name);
		return;
	}

	GLint drawFboId = 0, readFboId = 0;
	glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &drawFboId);
	glGetIntegerv(GL_READ_FRAMEBUFFER_BINDING, &readFboId);

	rlMatrixMode(RL_MODELVIEW);
	rlLoadIdentity();

	// if (!IsTextureValid(wm->renderer.main_rt.texture)) {
	//	wm->renderer.main_rt = LoadRenderTexture(width, height);
	// }

	if (!wlr_render_pass_submit(pass)) {
		wlr_output_state_finish(&state);
		wlr_output_schedule_frame(wlr_output);
		wlr_log(WLR_ERROR, "Failed to submit render pass for output %s",
		    wlr_output->name);
		return;
	}

	if (!wlr_output_commit_state(wlr_output, &state)) {
		wlr_output_state_finish(&state);
		wlr_output_schedule_frame(wlr_output);
		return;
	}

	wm->renderer.tmp_rt.id = drawFboId;
	wm->renderer.tmp_rt.texture = (Texture) {
		.id = drawFboId,
		.width = width,
		.height = height,
		.mipmaps = 1,
		.format = PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
	};
	if (!IsRenderTextureValid(wm->renderer.tmp_rt)) {
		wm->renderer.tmp_rt.depth.id = rlLoadTextureDepth(width, height, true);
		wm->renderer.tmp_rt.depth.width = width;
		wm->renderer.tmp_rt.depth.height = height;
		wm->renderer.tmp_rt.depth.format = 19; // DEPTH_COMPONENT_24BIT?
		wm->renderer.tmp_rt.depth.mipmaps = 1;
	}
	rlFramebufferAttach(wm->renderer.tmp_rt.id, wm->renderer.tmp_rt.depth.id,
	    RL_ATTACHMENT_DEPTH, RL_ATTACHMENT_RENDERBUFFER, 0);

	if (!wm->xr.available && !wm->renderer.first_frame) {
		wm->renderer.camera.target = SphericalToVector3(wm->wm.pointer);
	}

	int const hud_size = wm->cman->cfg.displays.hud.size;
	if (!IsTextureValid(wm->renderer.hud_rt.texture)) {
		wm->renderer.hud_rt = LoadRenderTexture(hud_size, hud_size);
	}
	BeginTextureMode(wm->renderer.hud_rt);
	{
		LunarWM_render_hud(wm, GetFrameTime(), 1);
	}
	EndTextureMode();

	if (!IsTextureValid(wm->renderer.main_rt.texture)) {
		wm->renderer.main_rt = LoadRenderTexture(width, height);
	}
	BeginTextureMode(wm->renderer.main_rt);
	{
		BeginMode3D(wm->renderer.camera);
		{
			ClearBackground(BLACK);
			LunarWM_render_3d(wm, GetFrameTime());
		}
		EndMode3D();

		rlDrawRenderBatchActive();
	}
	EndTextureMode();

	BeginTextureMode(wm->renderer.tmp_rt);
	{
		DrawTexturePro(wm->renderer.main_rt.texture,
		    (Rectangle) {
		        0,
		        0,
		        wm->renderer.main_rt.texture.width,
		        wm->renderer.main_rt.texture.height,
		    },
		    (Rectangle) {
		        0,
		        0,
		        wm->renderer.main_rt.texture.width,
		        -wm->renderer.main_rt.texture.height,
		    },
		    (Vector2) { 0, 0 }, 0, WHITE);
	}
	EndTextureMode();

	if (wm->renderer.first_frame) {
		LunarWM_set_recenter_from_camera(wm);
		wm->renderer.first_frame = false;
		wm->wm.pointer = get_forward_spherical_with_nearest(
		    wm->renderer.camera.target, wm->cman->cfg.space.radius);
	}

	wlr_output_state_finish(&state);
}

static void handle_new_output(struct wl_listener *listener, void *data)
{
	struct wlr_output *wlr_output = data;
	LunarWM *wm = wl_container_of(listener, wm, wayland.new_output_listener);

	wlr_log(WLR_INFO, "Got new output.");

	if (!wlr_output_init_render(
	        wlr_output, wm->wayland.allocator, wm->wayland.renderer)) {
		wlr_log(
		    WLR_ERROR, "Failed to init render for output %s", wlr_output->name);
		return;
	}

	struct wlr_output_state state;
	wlr_output_state_init(&state);
	wlr_output_state_set_enabled(&state, true);

	struct wlr_output_mode *mode = wlr_output_preferred_mode(wlr_output);
	if (mode != NULL) {
		wlr_output_state_set_mode(&state, mode);
	}

	bool committed = wlr_output_commit_state(wlr_output, &state);
	wlr_output_state_finish(&state);
	if (!committed) {
		wlr_log(WLR_ERROR, "Failed to commit init state for output %s",
		    wlr_output->name);
		return;
	}

	LunarWM_Output *output = calloc(1, sizeof(*output));
	if (!output) {
		wlr_log(WLR_ERROR, "Out of memory creating output state");
		return;
	}

	output->wm = wm;
	output->wlr_output = wlr_output;

	output->frame.notify = handle_output_frame;
	wl_signal_add(&wlr_output->events.frame, &output->frame);
	output->destroy.notify = handle_output_destroy;
	wl_signal_add(&wlr_output->events.destroy, &output->destroy);

	vector_add(&wm->wayland.v_outputs, output);

	wlr_output_schedule_frame(wlr_output);
}

bool LunarWM_wayland_init(LunarWM *this)
{
	wlr_log_init(WLR_DEBUG, nullptr);

	this->wayland.v_toplevels = vector_create();
	this->wayland.v_outputs = vector_create();

	this->wayland.display = wl_display_create();
	if (this->wayland.display == nullptr) {
		return false;
	}

	this->wayland.event_loop = wl_display_get_event_loop(this->wayland.display);
	if (this->wayland.event_loop == nullptr) {
		wlr_log(WLR_ERROR, "Failed to get wayland event loop");
		return false;
	}

	this->wayland.backend
	    = wlr_backend_autocreate(this->wayland.event_loop, nullptr);
	if (this->wayland.backend == nullptr) {
		wlr_log(WLR_ERROR, "Failed to create wlroots backend");
		return false;
	}

	setenv("WLR_RENDERER", "gles2", 1);
	this->wayland.renderer = wlr_renderer_autocreate(this->wayland.backend);
	if (this->wayland.renderer == nullptr) {
		wlr_log(WLR_ERROR, "Failed to create wlroots renderer");
		return false;
	}

	this->wayland.session = wlr_session_create(this->wayland.event_loop);
	if (this->wayland.session == nullptr) {
		wlr_log(WLR_ERROR, "Failed to create session");
		return false;
	}

	this->wayland.egl = wlr_gles2_renderer_get_egl(this->wayland.renderer);
	if (this->wayland.egl == nullptr) {
		wlr_log(WLR_ERROR, "Failed to get egl information from renderer");
		return false;
	}

	this->wayland.egl_display = wlr_egl_get_display(this->wayland.egl);
	this->wayland.egl_context = wlr_egl_get_context(this->wayland.egl);
	this->wayland.egl_config = EGL_NO_CONFIG_KHR;

	if (!wlr_renderer_init_wl_display(
	        this->wayland.renderer, this->wayland.display)) {
		wlr_log(
		    WLR_ERROR, "Failed to initialize renderer with wayland display");
		return false;
	}

	this->wayland.allocator = wlr_allocator_autocreate(
	    this->wayland.backend, this->wayland.renderer);
	if (!this->wayland.allocator) {
		wlr_log(WLR_ERROR, "Failed to create allocator");
		return false;
	}

	this->wayland.compositor = wlr_compositor_create(
	    this->wayland.display, 5, this->wayland.renderer);
	if (!this->wayland.compositor) {
		wlr_log(WLR_ERROR, "Failed to create compositor");
		return false;
	}

	this->wayland.subcompositor
	    = wlr_subcompositor_create(this->wayland.display);
	if (!this->wayland.subcompositor) {
		wlr_log(WLR_ERROR, "Failed to create subcompositor");
		return false;
	}

	this->wayland.data_device_manager
	    = wlr_data_device_manager_create(this->wayland.display);
	if (!this->wayland.data_device_manager) {
		wlr_log(WLR_ERROR, "Failed to create data device manager");
		return false;
	}

	this->wayland.xdg_shell = wlr_xdg_shell_create(this->wayland.display, 3);
	if (!this->wayland.xdg_shell) {
		wlr_log(WLR_ERROR, "Failed to create xdg shell");
		return false;
	}

	this->wayland.new_xdg_toplevel_listener.notify
	    = new_xdg_toplevel_listener_notify;
	wl_signal_add(&this->wayland.xdg_shell->events.new_toplevel,
	    &this->wayland.new_xdg_toplevel_listener);

	this->wayland.new_xdg_popup_listener.notify = new_xdg_popup_listener_notify;
	wl_signal_add(&this->wayland.xdg_shell->events.new_popup,
	    &this->wayland.new_xdg_popup_listener);

	this->wayland.seat = wlr_seat_create(this->wayland.display, "seat0");
	if (!this->wayland.seat) {
		wlr_log(WLR_ERROR, "Failed to create seat");
		return false;
	}
	wl_list_init(&this->wayland.keyboards);
	wl_list_init(&this->wayland.pointers);

	this->wayland.new_output_listener.notify = handle_new_output;
	wl_signal_add(&this->wayland.backend->events.new_output,
	    &this->wayland.new_output_listener);

	this->wayland.new_input_listener.notify = new_input_listener_notify;
	wl_signal_add(&this->wayland.backend->events.new_input,
	    &this->wayland.new_input_listener);

	this->wayland.xwayland = wlr_xwayland_create(
	    this->wayland.display, this->wayland.compositor, false);
	if (!this->wayland.xwayland) {
		wlr_log(WLR_ERROR, "Failed to start XWayland");
		return false;
	}

	setup_xwayland(this);

	this->wayland.event_loop = wl_display_get_event_loop(this->wayland.display);

	return true;
}

void LunarWM_wayland_cleanup(LunarWM *this)
{
	if (this->wayland.custom_out_virtual) {
		vo_destroy(this->wayland.custom_out_virtual);
		this->wayland.custom_out_virtual = NULL;
	}
	if (this->wayland.custom_out_hud) {
		vo_destroy(this->wayland.custom_out_hud);
		this->wayland.custom_out_hud = NULL;
	}

	if (this->wayland.new_output_listener.link.prev
	    || this->wayland.new_output_listener.link.next) {
		wl_list_remove(&this->wayland.new_output_listener.link);
		this->wayland.new_output_listener.notify = NULL;
	}

	if (this->wayland.v_outputs) {
		for (size_t i = 0; i < vector_size(this->wayland.v_outputs); ++i) {
			destroy_output(this->wayland.v_outputs[i]);
		}
		vector_free(this->wayland.v_outputs);
		this->wayland.v_outputs = NULL;
	}

	if (this->wayland.xwayland) {
		if (this->wayland.xwayland_new_surface.link.prev
		    || this->wayland.xwayland_new_surface.link.next) {
			wl_list_remove(&this->wayland.xwayland_new_surface.link);
			this->wayland.xwayland_new_surface.notify = NULL;
		}
		if (this->wayland.xwayland_ready.link.prev
		    || this->wayland.xwayland_ready.link.next) {
			wl_list_remove(&this->wayland.xwayland_ready.link);
			this->wayland.xwayland_ready.notify = NULL;
		}
		wlr_xwayland_destroy(this->wayland.xwayland);
		this->wayland.xwayland = NULL;
	}

	if (this->wayland.new_xdg_toplevel_listener.link.prev
	    || this->wayland.new_xdg_toplevel_listener.link.next) {
		wl_list_remove(&this->wayland.new_xdg_toplevel_listener.link);
		this->wayland.new_xdg_toplevel_listener.notify = NULL;
	}
	if (this->wayland.new_xdg_popup_listener.link.prev
	    || this->wayland.new_xdg_popup_listener.link.next) {
		wl_list_remove(&this->wayland.new_xdg_popup_listener.link);
		this->wayland.new_xdg_popup_listener.notify = NULL;
	}
	if (this->wayland.new_input_listener.link.prev
	    || this->wayland.new_input_listener.link.next) {
		wl_list_remove(&this->wayland.new_input_listener.link);
		this->wayland.new_input_listener.notify = NULL;
	}

	if (this->wayland.v_toplevels) {
		for (size_t i = 0; i < vector_size(this->wayland.v_toplevels); ++i) {
			if (this->wayland.v_toplevels[i]) {
				LunarWM_Toplevel_destroy(this->wayland.v_toplevels[i]);
				free(this->wayland.v_toplevels[i]);
			}
		}
		vector_free(this->wayland.v_toplevels);
		this->wayland.v_toplevels = NULL;
	}

	if (this->wayland.backend) {
		wlr_backend_destroy(this->wayland.backend);
		this->wayland.backend = NULL;
	}

	if (this->wayland.seat) {
		wlr_seat_destroy(this->wayland.seat);
		this->wayland.seat = NULL;
	}

	if (this->wayland.allocator) {
		wlr_allocator_destroy(this->wayland.allocator);
		this->wayland.allocator = NULL;
	}
	if (this->wayland.renderer) {
		wlr_renderer_destroy(this->wayland.renderer);
		this->wayland.renderer = NULL;
	}
	if (this->wayland.display) {
		wl_display_destroy(this->wayland.display);
		this->wayland.display = NULL;
	}
}

void LunarWM_wayland_update_virtual_outputs(
    LunarWM *wm, int virtual_width, int virtual_height, int hud_size)
{
	if (wm->wayland.custom_out_virtual) {
		vo_destroy(wm->wayland.custom_out_virtual);
		wm->wayland.custom_out_virtual = NULL;
	}
	if (wm->wayland.custom_out_hud) {
		vo_destroy(wm->wayland.custom_out_hud);
		wm->wayland.custom_out_hud = NULL;
	}

	wm->wayland.custom_out_virtual
	    = vo_create(wm->wayland.display, "Virtual", "Virtual output",
	        virtual_width, virtual_height, 60000, 1, "LunarWM", "Virtual");
	wm->wayland.custom_out_hud = vo_create(wm->wayland.display, "HUD",
	    "HUD output", hud_size, hud_size, 60000, 1, "LunarWM", "HUD");
}
