/*
 * uterm - Linux User-Space Terminal
 *
 * Copyright (c) 2011-2013 David Herrmann <dh.herrmann@googlemail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * DRM Video backend
 */

#define EGL_EGLEXT_PROTOTYPES
#define GL_GLEXT_PROTOTYPES

#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES2/gl2.h>
#include <GLES2/gl2ext.h>
#include <drm/drm_fourcc.h>
#include <errno.h>
#include <fcntl.h>
#include <gbm.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include "eloop.h"
#include "shl_log.h"
#include "shl_misc.h"
#include "uterm_drm3d_internal.h"
#include "uterm_drm_shared_internal.h"
#include "uterm_video.h"
#include "uterm_video_internal.h"

#define LOG_SUBSYSTEM "uterm_drm3d_video"

static void bo_destroy_event(struct gbm_bo *bo, void *data)
{
	struct uterm_drm3d_rb *rb = data;
	struct uterm_drm_video *vdrm;

	if (!rb)
		return;

	vdrm = rb->disp->video->data;
	drmModeRmFB(vdrm->fd, rb->id);
	free(rb);
}

static int drm_addfb2(int fd, struct uterm_drm3d_rb *rb)
{
	uint32_t handles[4] = {gbm_bo_get_handle(rb->bo).u32, 0, 0, 0};
	uint32_t pitches[4] = {gbm_bo_get_stride(rb->bo), 0, 0, 0};
	uint32_t offsets[4] = {0, 0, 0, 0};

	return drmModeAddFB2(fd, gbm_bo_get_width(rb->bo), gbm_bo_get_height(rb->bo),
			     DRM_FORMAT_XRGB8888, handles, pitches, offsets, &rb->id, 0);
}

static struct uterm_drm3d_rb *bo_to_rb(struct uterm_display *disp, struct gbm_bo *bo)
{
	struct uterm_drm3d_rb *rb = gbm_bo_get_user_data(bo);
	struct uterm_video *video = disp->video;
	struct uterm_drm_video *vdrm = video->data;
	int ret;

	if (rb)
		return rb;

	rb = malloc(sizeof(*rb));
	if (!rb) {
		log_err("cannot allocate memory for render buffer");
		return NULL;
	}
	rb->disp = disp;
	rb->bo = bo;

	ret = drm_addfb2(vdrm->fd, rb);
	if (ret) {
		log_err("cannot add drm-fb %d", ret);
		free(rb);
		return NULL;
	}

	gbm_bo_set_user_data(bo, rb, bo_destroy_event);
	return rb;
}

static int display_allocfb(struct uterm_display *disp)
{
	struct uterm_video *video = disp->video;
	struct uterm_drm3d_video *v3d;
	struct uterm_drm3d_display *d3d = disp->data;
	int ret;
	struct gbm_bo *bo;
	drmModeModeInfo *minfo;

	v3d = uterm_drm_video_get_data(video);

	minfo = d3d->ddrm.current_mode;
	disp->width = minfo->hdisplay;
	disp->height = minfo->vdisplay;

	log_debug("preparefb display %p to %ux%u", disp, minfo->hdisplay, minfo->vdisplay);

	d3d->current = NULL;
	d3d->next = NULL;

	d3d->gbm =
		gbm_surface_create(v3d->gbm, minfo->hdisplay, minfo->vdisplay, GBM_FORMAT_XRGB8888,
				   GBM_BO_USE_SCANOUT | GBM_BO_USE_RENDERING);
	if (!d3d->gbm) {
		log_err("cannot create gbm surface");
		ret = -EFAULT;
		goto err_saved;
	}

	d3d->surface =
		eglCreateWindowSurface(v3d->disp, v3d->conf, (EGLNativeWindowType)d3d->gbm, NULL);
	if (d3d->surface == EGL_NO_SURFACE) {
		log_err("cannot create EGL window surface");
		ret = -EFAULT;
		goto err_gbm;
	}

	if (!eglMakeCurrent(v3d->disp, d3d->surface, d3d->surface, v3d->ctx)) {
		log_err("cannot activate EGL context");
		ret = -EFAULT;
		goto err_surface;
	}

	glClearColor(0, 0, 0, 0);
	glClear(GL_COLOR_BUFFER_BIT);
	if (!eglSwapBuffers(v3d->disp, d3d->surface)) {
		log_err("cannot swap buffers");
		ret = -EFAULT;
		goto err_noctx;
	}

	bo = gbm_surface_lock_front_buffer(d3d->gbm);
	if (!bo) {
		log_err("cannot lock front buffer during creation");
		ret = -EFAULT;
		goto err_noctx;
	}

	d3d->current = bo_to_rb(disp, bo);
	if (!d3d->current) {
		log_err("cannot lock front buffer");
		ret = -EFAULT;
		goto err_bo;
	}
	return 0;

err_bo:
	gbm_surface_release_buffer(d3d->gbm, bo);
err_noctx:
	eglMakeCurrent(v3d->disp, EGL_NO_SURFACE, EGL_NO_SURFACE, v3d->ctx);
err_surface:
	eglDestroySurface(v3d->disp, d3d->surface);
err_gbm:
	gbm_surface_destroy(d3d->gbm);
err_saved:
	disp->width = 0;
	disp->height = 0;
	d3d->ddrm.current_mode = NULL;
	return ret;
}

static void display_freefb(struct uterm_display *disp)
{
	struct uterm_drm3d_display *d3d = disp->data;
	struct uterm_video *video = disp->video;
	struct uterm_drm3d_video *v3d;

	v3d = uterm_drm_video_get_data(video);

	if (v3d->ctx)
		eglMakeCurrent(v3d->disp, EGL_NO_SURFACE, EGL_NO_SURFACE, v3d->ctx);

	if (d3d->current) {
		gbm_surface_release_buffer(d3d->gbm, d3d->current->bo);
		d3d->current = NULL;
	}
	if (d3d->next) {
		gbm_surface_release_buffer(d3d->gbm, d3d->next->bo);
		d3d->next = NULL;
	}
	if (d3d->surface) {
		eglDestroySurface(v3d->disp, d3d->surface);
		d3d->surface = NULL;
	}
	if (d3d->gbm)
		gbm_surface_destroy(d3d->gbm);
}

static int display_prepare_modeset(struct uterm_display *disp, drmModeAtomicReqPtr req)
{
	struct gbm_bo *bo;
	struct uterm_drm3d_display *d3d = disp->data;
	struct uterm_video *video = disp->video;
	struct uterm_drm_video *vdrm = video->data;
	struct uterm_drm3d_video *v3d = uterm_drm_video_get_data(video);
	int ret;

	if (!d3d->gbm) {
		ret = display_allocfb(disp);
		if (ret)
			return ret;
	} else {
		if (!gbm_surface_has_free_buffers(d3d->gbm))
			return -EBUSY;

		if (!eglMakeCurrent(v3d->disp, d3d->surface, d3d->surface, v3d->ctx)) {
			log_err("cannot activate EGL context");
			return -EFAULT;
		}

		if (!eglSwapBuffers(v3d->disp, d3d->surface)) {
			log_err("cannot swap EGL buffers");
			return -EFAULT;
		}
		if (d3d->current) {
			gbm_surface_release_buffer(d3d->gbm, d3d->current->bo);
			d3d->current = NULL;
		}
		bo = gbm_surface_lock_front_buffer(d3d->gbm);
		if (!bo) {
			log_err("cannot lock front buffer");
			return -EFAULT;
		}

		d3d->current = bo_to_rb(disp, bo);
		if (!d3d->current) {
			log_err("cannot lock front gbm buffer");
			gbm_surface_release_buffer(d3d->gbm, bo);
			return -EFAULT;
		}
	}
	ret = uterm_drm_prepare_commit(vdrm->fd, &d3d->ddrm, req, d3d->current->id, disp->width,
				       disp->height, vdrm->cursor_hotspot);
	if (ret) {
		gbm_surface_release_buffer(d3d->gbm, d3d->current->bo);
		return ret;
	}
	return 0;
}

static void display_done_modeset(struct uterm_display *disp, int status)
{
	struct uterm_drm3d_display *d3d = disp->data;

	if (status) {
		gbm_surface_release_buffer(d3d->gbm, d3d->current->bo);
		d3d->current = NULL;
		return;
	}

	if (d3d->next) {
		gbm_surface_release_buffer(d3d->gbm, d3d->next->bo);
		d3d->next = NULL;
	}
}

static int display_init(struct uterm_display *disp)
{
	struct uterm_drm3d_display *d3d;

	d3d = malloc(sizeof(*d3d));
	if (!d3d)
		return -ENOMEM;
	memset(d3d, 0, sizeof(*d3d));

	disp->flags |= DISPLAY_OPENGL;
	disp->data = d3d;
	d3d->ddrm.prepare_modeset = display_prepare_modeset;
	d3d->ddrm.done_modeset = display_done_modeset;
	return 0;
}

static void display_destroy(struct uterm_display *disp)
{
	display_freefb(disp);
	uterm_drm_display_free_properties(disp);
	free(disp->data);
}

/*
 * Enable opengl context
 */
int uterm_drm3d_display_use(struct uterm_display *disp)
{
	struct uterm_drm3d_display *d3d = disp->data;
	struct uterm_drm3d_video *v3d;

	v3d = uterm_drm_video_get_data(disp->video);
	if (!eglMakeCurrent(v3d->disp, d3d->surface, d3d->surface, v3d->ctx)) {
		log_err("cannot activate EGL context");
		return -EFAULT;
	}
	return 0;
}

static int display_swap(struct uterm_display *disp)
{
	int ret;
	struct gbm_bo *bo;
	struct uterm_drm3d_rb *rb;
	struct uterm_drm3d_display *d3d = disp->data;
	struct uterm_video *video = disp->video;
	struct uterm_drm3d_video *v3d = uterm_drm_video_get_data(video);

	if (!gbm_surface_has_free_buffers(d3d->gbm))
		return -EBUSY;

	if (!eglSwapBuffers(v3d->disp, d3d->surface)) {
		log_err("cannot swap EGL buffers");
		return -EFAULT;
	}

	bo = gbm_surface_lock_front_buffer(d3d->gbm);
	if (!bo) {
		log_err("cannot lock front buffer");
		return -EFAULT;
	}

	rb = bo_to_rb(disp, bo);
	if (!rb) {
		log_err("cannot lock front gbm buffer");
		gbm_surface_release_buffer(d3d->gbm, bo);
		return -EFAULT;
	}
	ret = uterm_drm_display_swap(disp, rb->id);
	if (ret) {
		gbm_surface_release_buffer(d3d->gbm, bo);
		return ret;
	}

	if (d3d->next) {
		gbm_surface_release_buffer(d3d->gbm, d3d->next->bo);
		d3d->next = NULL;
	}

	d3d->next = rb;
	return 0;
}

static const struct display_ops drm_display_ops = {
	.init = display_init,
	.destroy = display_destroy,
	.set_dpms = uterm_drm_display_set_dpms,
	.use = uterm_drm3d_display_use,
	.swap = display_swap,
	.is_swapping = uterm_drm_is_swapping,
	.fake_blendv = uterm_drm3d_display_fake_blendv,
	.clear = uterm_drm3d_display_clear,
	.set_damage = NULL,
	.has_damage = NULL,
	.setup_cursor = uterm_drm_display_setup_cursor,
	.destroy_cursor = uterm_drm_display_destroy_cursor,
	.show_cursor = uterm_drm_display_show_cursor,
	.hide_cursor = uterm_drm_display_hide_cursor,
	.set_cursor_offset = uterm_drm_display_set_cursor_offset,
};

static void show_displays(struct uterm_video *video)
{
	int ret;
	struct uterm_display *iter;
	struct shl_dlist *i;

	if (!video_is_awake(video))
		return;

	shl_dlist_for_each(i, &video->displays)
	{
		iter = shl_dlist_entry(i, struct uterm_display, list);

		if (!display_is_online(iter))
			continue;
		if (iter->dpms != UTERM_DPMS_ON)
			continue;

		ret = uterm_drm3d_display_use(iter);
		if (ret)
			continue;

		glClearColor(0, 0, 0, 1);
		glClear(GL_COLOR_BUFFER_BIT);
		display_swap(iter);
	}
}

static void page_flip_handler(struct uterm_display *disp)
{
	struct uterm_drm3d_display *d3d = disp->data;

	if (d3d->next) {
		if (d3d->current)
			gbm_surface_release_buffer(d3d->gbm, d3d->current->bo);
		d3d->current = d3d->next;
		d3d->next = NULL;
	}
}

static int video_init(struct uterm_video *video, const char *node)
{
	static const EGLint conf_att[] = {
		EGL_SURFACE_TYPE,
		EGL_WINDOW_BIT,
		EGL_RENDERABLE_TYPE,
		EGL_OPENGL_ES2_BIT,
		EGL_RED_SIZE,
		1,
		EGL_GREEN_SIZE,
		1,
		EGL_BLUE_SIZE,
		1,
		EGL_ALPHA_SIZE,
		0,
		EGL_NONE,
	};
	static const EGLint ctx_att[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};
	const char *ext;
	int ret;
	EGLint major, minor, n = 0;
	EGLenum api;
	EGLBoolean b;
	struct uterm_drm_video *vdrm;
	struct uterm_drm3d_video *v3d;
	EGLConfig *cfgs = NULL;
	EGLint gbmFmt;
	EGLint cfgId;
	EGLint err;

	v3d = malloc(sizeof(*v3d));
	if (!v3d)
		return -ENOMEM;
	memset(v3d, 0, sizeof(*v3d));

	ret = uterm_drm_video_init(video, node, &drm_display_ops, page_flip_handler, v3d);
	if (ret)
		goto err_free;
	vdrm = video->data;

	log_debug("initialize 3D layer on %p", video);

	v3d->gbm = gbm_create_device(vdrm->fd);
	if (!v3d->gbm) {
		log_err("cannot create gbm device for %s (permission denied)", node);
		ret = -EFAULT;
		goto err_video;
	}

	v3d->disp = eglGetDisplay((EGLNativeDisplayType)v3d->gbm);
	if (v3d->disp == EGL_NO_DISPLAY) {
		log_err("cannot retrieve egl display for %s", node);
		ret = -EFAULT;
		goto err_gbm;
	}

	b = eglInitialize(v3d->disp, &major, &minor);
	if (!b) {
		log_err("cannot init egl display for %s", node);
		ret = -EFAULT;
		goto err_gbm;
	}

	log_debug("EGL Init %d.%d", major, minor);
	log_debug("EGL Version %s", eglQueryString(v3d->disp, EGL_VERSION));
	log_debug("EGL Vendor %s", eglQueryString(v3d->disp, EGL_VENDOR));
	ext = eglQueryString(v3d->disp, EGL_EXTENSIONS);
	log_debug("EGL Extensions %s", ext);

	if (!ext || !strstr(ext, "EGL_KHR_surfaceless_context")) {
		log_err("surfaceless opengl not supported");
		ret = -EFAULT;
		goto err_disp;
	}

	api = EGL_OPENGL_ES_API;
	if (!eglBindAPI(api)) {
		log_err("cannot bind opengl-es api");
		ret = -EFAULT;
		goto err_disp;
	}

	b = eglChooseConfig(v3d->disp, conf_att, cfgs, n, &n);
	if (!b || n == 0) {
		log_err("no EGL configs found");
		ret = -EFAULT;
		goto err_disp;
	}

	cfgs = malloc(sizeof(EGLConfig) * n);
	if (cfgs == NULL) {
		log_err("failed to allocate memory for %d configs", n);
		ret = -ENOMEM;
		goto err_disp;
	}

	b = eglChooseConfig(v3d->disp, conf_att, cfgs, n, &n);
	if (!b) {
		log_err("failed to load EGL configs");
		ret = -EFAULT;
		goto err_cfgs;
	}

	log_debug("got %d EGL configs", n);
	b = false;
	for (EGLint i = 0; i < n; i++) {
		if (!eglGetConfigAttrib(v3d->disp, cfgs[i], EGL_NATIVE_VISUAL_ID, &gbmFmt)) {
			err = eglGetError();
			log_warn("cfgs[%d] failed to get format (error %x), skipping...", i, err);
			continue;
		}
		log_debug("cfgs[%d] format %x", i, gbmFmt);
		if (gbmFmt == GBM_FORMAT_XRGB8888 || gbmFmt == GBM_FORMAT_ARGB8888) {
			if (!eglGetConfigAttrib(v3d->disp, cfgs[i], EGL_CONFIG_ID, &cfgId)) {
				err = eglGetError();
				log_warning("cfgs[%d] matched, but failed to get ID (error %x).", i,
					    err);
			} else {
				log_debug("config with ID %x matched", cfgId);
			}
			memcpy(&v3d->conf, &cfgs[i], sizeof(EGLConfig));
			b = true;
			break;
		}
	}

	if (!b) {
		log_err("no config had matching gbm format");
		ret = -EFAULT;
		goto err_cfgs;
	}

	v3d->ctx = eglCreateContext(v3d->disp, v3d->conf, EGL_NO_CONTEXT, ctx_att);
	if (v3d->ctx == EGL_NO_CONTEXT) {
		log_err("cannot create egl context");
		ret = -EFAULT;
		goto err_cfgs;
	}

	if (!eglMakeCurrent(v3d->disp, EGL_NO_SURFACE, EGL_NO_SURFACE, v3d->ctx)) {
		log_err("cannot activate surfaceless EGL context");
		ret = -EFAULT;
		goto err_ctx;
	}

	ext = (const char *)glGetString(GL_EXTENSIONS);
	if (ext && strstr((const char *)ext, "GL_EXT_unpack_subimage"))
		v3d->supports_rowlen = true;
	else
		log_warning("your GL implementation does not support GL_EXT_unpack_subimage, "
			    "rendering may be slower than usual");

	free(cfgs);
	return 0;

err_ctx:
	eglDestroyContext(v3d->disp, v3d->ctx);
err_cfgs:
	free(cfgs);
err_disp:
	eglTerminate(v3d->disp);
err_gbm:
	gbm_device_destroy(v3d->gbm);
err_video:
	uterm_drm_video_destroy(video);
err_free:
	free(v3d);
	return ret;
}

static void video_destroy(struct uterm_video *video)
{
	struct uterm_drm3d_video *v3d = uterm_drm_video_get_data(video);

	log_info("free drm video device %p", video);

	if (!eglMakeCurrent(v3d->disp, EGL_NO_SURFACE, EGL_NO_SURFACE, v3d->ctx))
		log_err("cannot activate GL context during destruction");
	uterm_drm3d_deinit_shaders(video);

	eglMakeCurrent(v3d->disp, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
	eglDestroyContext(v3d->disp, v3d->ctx);
	eglTerminate(v3d->disp);
	gbm_device_destroy(v3d->gbm);
	free(v3d);
	uterm_drm_video_destroy(video);
}

static int video_poll(struct uterm_video *video)
{
	return uterm_drm_video_poll(video);
}

static void video_sleep(struct uterm_video *video)
{
	show_displays(video);
	uterm_drm_video_sleep(video);
}

static int video_wake_up(struct uterm_video *video)
{
	int ret;

	ret = uterm_drm_video_wake_up(video);
	if (ret) {
		uterm_drm_video_arm_vt_timer(video);
		return ret;
	}

	show_displays(video);
	return 0;
}

SHL_EXPORT
struct uterm_video_module drm3d_module = {
	.name = "drm3d",
	.owner = NULL,
	.ops =
		{
			.init = video_init,
			.destroy = video_destroy,
			.poll = video_poll,
			.sleep = video_sleep,
			.wake_up = video_wake_up,
		},
};
