// SPDX-License-Identifier: MIT
/*
 * Compton - a compositor for X11
 *
 * Based on `xcompmgr` - Copyright (c) 2003, Keith Packard
 *
 * Copyright (c) 2011-2013, Christopher Jeffrey
 * Copyright (c) 2019 Yuxuan Shui <yshuiv7@gmail.com>
 * See LICENSE-mit for more information.
 *
 */

#include <X11/Xlib-xcb.h>
#include <assert.h>
#include <limits.h>
#include <pixman.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <xcb/composite.h>
#include <xcb/xcb.h>

#include "backend/backend.h"
#include "backend/backend_common.h"
#include "backend/gl/gl_common.h"
#include "backend/gl/glx.h"
#include "common.h"
#include "compiler.h"
#include "config.h"
#include "log.h"
#include "picom.h"
#include "region.h"
#include "utils.h"
#include "win.h"
#include "x.h"

struct _glx_pixmap {
	GLXPixmap glpixmap;
	xcb_pixmap_t pixmap;
	bool owned;
};

struct _glx_data {
	struct gl_data gl;
	Display *display;
	int screen;
	xcb_window_t target_win;
	GLXContext ctx;
};

#define glXGetFBConfigAttribChecked(a, b, attr, c)                                       \
	do {                                                                             \
		if (glXGetFBConfigAttrib(a, b, attr, c)) {                               \
			log_info("Cannot get FBConfig attribute " #attr);                \
			continue;                                                        \
		}                                                                        \
	} while (0)

struct glx_fbconfig_info *glx_find_fbconfig(Display *dpy, int screen, struct xvisual_info m) {
	log_debug("Looking for FBConfig for RGBA%d%d%d%d, depth %d", m.red_size,
	          m.blue_size, m.green_size, m.alpha_size, m.visual_depth);

	int ncfg;
	// clang-format off
	GLXFBConfig *cfg =
	    glXChooseFBConfig(dpy, screen, (int[]){
	                          GLX_RENDER_TYPE, GLX_RGBA_BIT,
	                          GLX_DRAWABLE_TYPE, GLX_PIXMAP_BIT,
	                          GLX_X_VISUAL_TYPE, GLX_TRUE_COLOR,
	                          GLX_X_RENDERABLE, true,
	                          GLX_FRAMEBUFFER_SRGB_CAPABLE_EXT, (GLint)GLX_DONT_CARE,
	                          GLX_BUFFER_SIZE, m.red_size + m.green_size +
	                                           m.blue_size + m.alpha_size,
	                          GLX_RED_SIZE, m.red_size,
	                          GLX_BLUE_SIZE, m.blue_size,
	                          GLX_GREEN_SIZE, m.green_size,
	                          GLX_ALPHA_SIZE, m.alpha_size,
	                          GLX_STENCIL_SIZE, 0,
	                          GLX_DEPTH_SIZE, 0,
	                          0
	                      }, &ncfg);
	// clang-format on

	int texture_tgts, y_inverted, texture_fmt;
	bool found = false;
	int min_cost = INT_MAX;
	GLXFBConfig ret;
	for (int i = 0; i < ncfg; i++) {
		int depthbuf, stencil, doublebuf, bufsize;
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BUFFER_SIZE, &bufsize);
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_DEPTH_SIZE, &depthbuf);
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_STENCIL_SIZE, &stencil);
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_DOUBLEBUFFER, &doublebuf);
		if (depthbuf + stencil + bufsize * (doublebuf + 1) >= min_cost) {
			continue;
		}
		int red, green, blue;
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_RED_SIZE, &red);
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BLUE_SIZE, &blue);
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_GREEN_SIZE, &green);
		if (red != m.red_size || green != m.green_size || blue != m.blue_size) {
			// Color size doesn't match, this cannot work
			continue;
		}

		int rgb, rgba;
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BIND_TO_TEXTURE_RGB_EXT, &rgb);
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BIND_TO_TEXTURE_RGBA_EXT, &rgba);
		if (!rgb && !rgba) {
			log_info("FBConfig is neither RGBA nor RGB, we cannot "
			         "handle this setup.");
			continue;
		}

		int visual;
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_VISUAL_ID, &visual);
		if (m.visual_depth != -1 &&
		    x_get_visual_depth(XGetXCBConnection(dpy), (xcb_visualid_t)visual) !=
		        m.visual_depth) {
			// FBConfig and the correspondent X Visual might not have the same
			// depth. (e.g. 32 bit FBConfig with a 24 bit Visual). This is
			// quite common, seen in both open source and proprietary drivers.
			//
			// If the FBConfig has a matching depth but its visual doesn't, we
			// still cannot use it.
			continue;
		}

		// All check passed, we are using this one.
		found = true;
		ret = cfg[i];
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_BIND_TO_TEXTURE_TARGETS_EXT,
		                            &texture_tgts);
		glXGetFBConfigAttribChecked(dpy, cfg[i], GLX_Y_INVERTED_EXT, &y_inverted);

		// Prefer the texture format with matching alpha, with the other one as
		// fallback
		if (m.alpha_size) {
			texture_fmt = rgba ? GLX_TEXTURE_FORMAT_RGBA_EXT
			                   : GLX_TEXTURE_FORMAT_RGB_EXT;
		} else {
			texture_fmt =
			    rgb ? GLX_TEXTURE_FORMAT_RGB_EXT : GLX_TEXTURE_FORMAT_RGBA_EXT;
		}
		min_cost = depthbuf + stencil + bufsize * (doublebuf + 1);
	}
	free(cfg);
	if (!found) {
		return NULL;
	}

	auto info = cmalloc(struct glx_fbconfig_info);
	info->cfg = ret;
	info->texture_tgts = texture_tgts;
	info->texture_fmt = texture_fmt;
	info->y_inverted = y_inverted;
	return info;
}

/**
 * Free a glx_texture_t.
 */
static void glx_release_image(backend_t *base, struct gl_texture *tex) {
	struct _glx_data *gd = (void *)base;

	struct _glx_pixmap *p = tex->user_data;
	// Release binding
	if (p->glpixmap && tex->texture) {
		glBindTexture(GL_TEXTURE_2D, tex->texture);
		glXReleaseTexImageEXT(gd->display, p->glpixmap, GLX_FRONT_LEFT_EXT);
		glBindTexture(GL_TEXTURE_2D, 0);
	}

	// Free GLX Pixmap
	if (p->glpixmap) {
		glXDestroyPixmap(gd->display, p->glpixmap);
		p->glpixmap = 0;
	}

	if (p->owned) {
		xcb_free_pixmap(base->c, p->pixmap);
		p->pixmap = XCB_NONE;
	}

	free(p);
	tex->user_data = NULL;
}

/**
 * Destroy GLX related resources.
 */
void glx_deinit(backend_t *base) {
	struct _glx_data *gd = (void *)base;

	gl_deinit(&gd->gl);

	// Destroy GLX context
	if (gd->ctx) {
		glXMakeCurrent(gd->display, None, NULL);
		glXDestroyContext(gd->display, gd->ctx);
		gd->ctx = 0;
	}

	free(gd);
}

static void *glx_decouple_user_data(backend_t *base attr_unused, void *ud attr_unused) {
	auto ret = cmalloc(struct _glx_pixmap);
	ret->owned = false;
	ret->glpixmap = 0;
	ret->pixmap = 0;
	return ret;
}

static bool glx_set_swap_interval(int interval, Display *dpy, GLXDrawable drawable) {
	bool vsync_enabled = false;
	if (glxext.has_GLX_MESA_swap_control) {
		vsync_enabled = (glXSwapIntervalMESA((uint)interval) == 0);
	}
	if (!vsync_enabled && glxext.has_GLX_SGI_swap_control) {
		vsync_enabled = (glXSwapIntervalSGI(interval) == 0);
	}
	if (!vsync_enabled && glxext.has_GLX_EXT_swap_control) {
		// glXSwapIntervalEXT doesn't return if it's successful
		glXSwapIntervalEXT(dpy, drawable, interval);
		vsync_enabled = true;
	}
	return vsync_enabled;
}

/**
 * Initialize OpenGL.
 */
static backend_t *glx_init(session_t *ps) {
	bool success = false;
	glxext_init(ps->dpy, ps->scr);
	auto gd = ccalloc(1, struct _glx_data);
	init_backend_base(&gd->gl.base, ps);

	gd->display = ps->dpy;
	gd->screen = ps->scr;
	gd->target_win = session_get_target_window(ps);

	XVisualInfo *pvis = NULL;

	// Check for GLX extension
	if (!ps->glx_exists) {
		log_error("No GLX extension.");
		goto end;
	}

	// Get XVisualInfo
	int nitems = 0;
	XVisualInfo vreq = {.visualid = ps->vis};
	pvis = XGetVisualInfo(ps->dpy, VisualIDMask, &vreq, &nitems);
	if (!pvis) {
		log_error("Failed to acquire XVisualInfo for current visual.");
		goto end;
	}

	// Ensure the visual is double-buffered
	int value = 0;
	if (glXGetConfig(ps->dpy, pvis, GLX_USE_GL, &value) || !value) {
		log_error("Root visual is not a GL visual.");
		goto end;
	}

	if (glXGetConfig(ps->dpy, pvis, GLX_STENCIL_SIZE, &value) || !value) {
		log_error("Root visual lacks stencil buffer.");
		goto end;
	}

	if (glXGetConfig(ps->dpy, pvis, GLX_DOUBLEBUFFER, &value) || !value) {
		log_error("Root visual is not a double buffered GL visual.");
		goto end;
	}

	if (glXGetConfig(ps->dpy, pvis, GLX_RGBA, &value) || !value) {
		log_error("Root visual is a color index visual, not supported");
		goto end;
	}

	if (!glxext.has_GLX_EXT_texture_from_pixmap) {
		log_error("GLX_EXT_texture_from_pixmap is not supported by your driver");
		goto end;
	}

	if (!glxext.has_GLX_ARB_create_context) {
		log_error("GLX_ARB_create_context is not supported by your driver");
		goto end;
	}

	// Find a fbconfig with visualid matching the one from the target win, so we can
	// be sure that the fbconfig is compatible with our target window.
	int ncfgs;
	GLXFBConfig *cfg = glXGetFBConfigs(gd->display, gd->screen, &ncfgs);
	bool found = false;
	for (int i = 0; i < ncfgs; i++) {
		int visualid;
		glXGetFBConfigAttribChecked(gd->display, cfg[i], GLX_VISUAL_ID, &visualid);
		if ((VisualID)visualid != pvis->visualid) {
			continue;
		}

		gd->ctx = glXCreateContextAttribsARB(ps->dpy, cfg[i], 0, true,
		                                     (int[]){
		                                         GLX_CONTEXT_MAJOR_VERSION_ARB,
		                                         3,
		                                         GLX_CONTEXT_MINOR_VERSION_ARB,
		                                         3,
		                                         GLX_CONTEXT_PROFILE_MASK_ARB,
		                                         GLX_CONTEXT_CORE_PROFILE_BIT_ARB,
		                                         0,
		                                     });
		free(cfg);

		if (!gd->ctx) {
			log_error("Failed to get GLX context.");
			goto end;
		}
		found = true;
		break;
	}

	if (!found) {
		log_error("Couldn't find a suitable fbconfig for the target window");
		goto end;
	}

	// Attach GLX context
	GLXDrawable tgt = gd->target_win;
	if (!glXMakeCurrent(ps->dpy, tgt, gd->ctx)) {
		log_error("Failed to attach GLX context.");
		goto end;
	}

	if (!gl_init(&gd->gl, ps)) {
		log_error("Failed to setup OpenGL");
		goto end;
	}

	gd->gl.decouple_texture_user_data = glx_decouple_user_data;
	gd->gl.release_user_data = glx_release_image;

	if (ps->o.vsync) {
		if (!glx_set_swap_interval(1, ps->dpy, tgt)) {
			log_error("Failed to enable vsync.");
		}
	} else {
		glx_set_swap_interval(0, ps->dpy, tgt);
	}

	success = true;

end:
	if (pvis) {
		XFree(pvis);
	}

	if (!success) {
		glx_deinit(&gd->gl.base);
		return NULL;
	}

	return &gd->gl.base;
}

static void *
glx_bind_pixmap(backend_t *base, xcb_pixmap_t pixmap, struct xvisual_info fmt, bool owned) {
	struct _glx_data *gd = (void *)base;
	struct _glx_pixmap *glxpixmap = NULL;
	// Retrieve pixmap parameters, if they aren't provided
	if (fmt.visual_depth > OPENGL_MAX_DEPTH) {
		log_error("Requested depth %d higher than max possible depth %d.",
		          fmt.visual_depth, OPENGL_MAX_DEPTH);
		return false;
	}

	if (fmt.visual_depth < 0) {
		log_error("Pixmap %#010x with invalid depth %d", pixmap, fmt.visual_depth);
		return false;
	}

	auto r = xcb_get_geometry_reply(base->c, xcb_get_geometry(base->c, pixmap), NULL);
	if (!r) {
		log_error("Invalid pixmap %#010x", pixmap);
		return NULL;
	}

	log_trace("Binding pixmap %#010x", pixmap);
	auto wd = ccalloc(1, struct gl_image);
	wd->max_brightness = 1;
	wd->inner = ccalloc(1, struct gl_texture);
	wd->inner->width = wd->ewidth = r->width;
	wd->inner->height = wd->eheight = r->height;
	free(r);

	auto fbcfg = glx_find_fbconfig(gd->display, gd->screen, fmt);
	if (!fbcfg) {
		log_error("Couldn't find FBConfig with requested visual %x", fmt.visual);
		goto err;
	}

	// Choose a suitable texture target for our pixmap.
	// Refer to GLX_EXT_texture_om_pixmap spec to see what are the mean
	// of the bits in texture_tgts
	if (!(fbcfg->texture_tgts & GLX_TEXTURE_2D_BIT_EXT)) {
		log_error("Cannot bind pixmap to GL_TEXTURE_2D, giving up");
		goto err;
	}

	log_debug("depth %d, rgba %d", fmt.visual_depth,
	          (fbcfg->texture_fmt == GLX_TEXTURE_FORMAT_RGBA_EXT));

	GLint attrs[] = {
	    GLX_TEXTURE_FORMAT_EXT,
	    fbcfg->texture_fmt,
	    GLX_TEXTURE_TARGET_EXT,
	    GLX_TEXTURE_2D_EXT,
	    0,
	};

	wd->inner->y_inverted = fbcfg->y_inverted;

	glxpixmap = cmalloc(struct _glx_pixmap);
	glxpixmap->pixmap = pixmap;
	glxpixmap->glpixmap = glXCreatePixmap(gd->display, fbcfg->cfg, pixmap, attrs);
	glxpixmap->owned = owned;
	free(fbcfg);

	if (!glxpixmap->glpixmap) {
		log_error("Failed to create glpixmap for pixmap %#010x", pixmap);
		goto err;
	}

	log_trace("GLXPixmap %#010lx", glxpixmap->glpixmap);

	// Create texture
	wd->inner->user_data = glxpixmap;
	wd->inner->texture = gl_new_texture(GL_TEXTURE_2D);
	wd->opacity = 1;
	wd->color_inverted = false;
	wd->dim = 0;
	wd->has_alpha = fmt.alpha_size != 0;
	wd->inner->refcount = 1;
	glBindTexture(GL_TEXTURE_2D, wd->inner->texture);
	glXBindTexImageEXT(gd->display, glxpixmap->glpixmap, GLX_FRONT_LEFT_EXT, NULL);
	glBindTexture(GL_TEXTURE_2D, 0);

	gl_check_err();
	return wd;
err:
	if (glxpixmap && glxpixmap->glpixmap) {
		glXDestroyPixmap(gd->display, glxpixmap->glpixmap);
	}
	free(glxpixmap);

	if (owned) {
		xcb_free_pixmap(base->c, pixmap);
	}
	free(wd);
	return NULL;
}

static void glx_present(backend_t *base, const region_t *region attr_unused) {
	struct _glx_data *gd = (void *)base;
	gl_present(base, region);
	glXSwapBuffers(gd->display, gd->target_win);
	// XXX there should be no need to block, the core should wait for render to finish
	if (!gd->gl.is_nvidia) {
		glFinish();
	}
}

static int glx_buffer_age(backend_t *base) {
	if (!glxext.has_GLX_EXT_buffer_age) {
		return -1;
	}

	struct _glx_data *gd = (void *)base;
	unsigned int val;
	glXQueryDrawable(gd->display, gd->target_win, GLX_BACK_BUFFER_AGE_EXT, &val);
	return (int)val ?: -1;
}

struct backend_operations glx_ops = {
    .init = glx_init,
    .deinit = glx_deinit,
    .bind_pixmap = glx_bind_pixmap,
    .release_image = gl_release_image,
    .compose = gl_compose,
    .image_op = gl_image_op,
    .copy = gl_copy,
    .blur = gl_blur,
    .is_image_transparent = gl_is_image_transparent,
    .present = glx_present,
    .buffer_age = glx_buffer_age,
    .render_shadow = default_backend_render_shadow,
    .fill = gl_fill,
    .create_blur_context = gl_create_blur_context,
    .destroy_blur_context = gl_destroy_blur_context,
    .get_blur_size = gl_get_blur_size,
    .max_buffer_age = 5,        // Why?
};

/**
 * Check if a GLX extension exists.
 */
static inline bool glx_has_extension(Display *dpy, int screen, const char *ext) {
	const char *glx_exts = glXQueryExtensionsString(dpy, screen);
	if (!glx_exts) {
		log_error("Failed get GLX extension list.");
		return false;
	}

	auto inlen = strlen(ext);
	const char *curr = glx_exts;
	bool match = false;
	while (curr && !match) {
		const char *end = strchr(curr, ' ');
		if (!end) {
			// Last extension string
			match = strcmp(ext, curr) == 0;
		} else if (curr + inlen == end) {
			// Length match, do match string
			match = strncmp(ext, curr, (unsigned long)(end - curr)) == 0;
		}
		curr = end ? end + 1 : NULL;
	}

	if (!match) {
		log_info("Missing GLX extension %s.", ext);
	} else {
		log_info("Found GLX extension %s.", ext);
	}

	return match;
}

struct glxext_info glxext = {0};
PFNGLXGETVIDEOSYNCSGIPROC glXGetVideoSyncSGI;
PFNGLXWAITVIDEOSYNCSGIPROC glXWaitVideoSyncSGI;
PFNGLXGETSYNCVALUESOMLPROC glXGetSyncValuesOML;
PFNGLXWAITFORMSCOMLPROC glXWaitForMscOML;
PFNGLXSWAPINTERVALEXTPROC glXSwapIntervalEXT;
PFNGLXSWAPINTERVALSGIPROC glXSwapIntervalSGI;
PFNGLXSWAPINTERVALMESAPROC glXSwapIntervalMESA;
PFNGLXBINDTEXIMAGEEXTPROC glXBindTexImageEXT;
PFNGLXRELEASETEXIMAGEEXTPROC glXReleaseTexImageEXT;
PFNGLXCREATECONTEXTATTRIBSARBPROC glXCreateContextAttribsARB;

void glxext_init(Display *dpy, int screen) {
	if (glxext.initialized) {
		return;
	}
	glxext.initialized = true;
#define check_ext(name) glxext.has_##name = glx_has_extension(dpy, screen, #name)
	check_ext(GLX_SGI_video_sync);
	check_ext(GLX_SGI_swap_control);
	check_ext(GLX_OML_sync_control);
	check_ext(GLX_MESA_swap_control);
	check_ext(GLX_EXT_swap_control);
	check_ext(GLX_EXT_texture_from_pixmap);
	check_ext(GLX_ARB_create_context);
	check_ext(GLX_EXT_buffer_age);
#undef check_ext

#define lookup(name) (name = (__typeof__(name))glXGetProcAddress((GLubyte *)#name))
	// Checking if the returned function pointer is NULL is not really necessary,
	// or maybe not even useful, since glXGetProcAddress might always return
	// something. We are doing it just for completeness' sake.
	if (!lookup(glXGetVideoSyncSGI) || !lookup(glXWaitVideoSyncSGI)) {
		glxext.has_GLX_SGI_video_sync = false;
	}
	if (!lookup(glXSwapIntervalEXT)) {
		glxext.has_GLX_EXT_swap_control = false;
	}
	if (!lookup(glXSwapIntervalMESA)) {
		glxext.has_GLX_MESA_swap_control = false;
	}
	if (!lookup(glXSwapIntervalSGI)) {
		glxext.has_GLX_SGI_swap_control = false;
	}
	if (!lookup(glXWaitForMscOML) || !lookup(glXGetSyncValuesOML)) {
		glxext.has_GLX_OML_sync_control = false;
	}
	if (!lookup(glXBindTexImageEXT) || !lookup(glXReleaseTexImageEXT)) {
		glxext.has_GLX_EXT_texture_from_pixmap = false;
	}
	if (!lookup(glXCreateContextAttribsARB)) {
		glxext.has_GLX_ARB_create_context = false;
	}
#undef lookup
}
