/*
 * uterm - Linux User-Space Terminal drm2d module
 *
 * 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 using dumb buffer objects
 */

#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <libdrm/drm_fourcc.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <unistd.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include "eloop.h"
#include "shl_log.h"
#include "shl_misc.h"
#include "uterm_drm2d_internal.h"
#include "uterm_drm_shared_internal.h"
#include "uterm_video.h"
#include "uterm_video_internal.h"

#define LOG_SUBSYSTEM "video_drm2d"

static int drm_addfb2(int fd, uint32_t width, uint32_t height, struct uterm_drm2d_rb *rb)
{
	uint32_t handles[4] = {rb->handle, 0, 0, 0};
	uint32_t pitches[4] = {rb->stride, 0, 0, 0};
	uint32_t offsets[4] = {0, 0, 0, 0};

	return drmModeAddFB2(fd, width, height, DRM_FORMAT_XRGB8888, handles, pitches, offsets,
			     &rb->id, 0);
}

static int init_rb(int fd, uint32_t width, uint32_t height, struct uterm_drm2d_rb *rb)
{
	uint64_t mmap_offset;
	int ret, r;

	if (drmModeCreateDumbBuffer(fd, width, height, 32, 0, &rb->handle, &rb->stride,
				    &rb->size)) {
		log_err("cannot create dumb drm buffer");
		return -EFAULT;
	}

	ret = drm_addfb2(fd, width, height, rb);
	if (ret) {
		log_err("cannot add drm-fb");
		ret = -EFAULT;
		goto err_buf;
	}

	ret = drmModeMapDumbBuffer(fd, rb->handle, &mmap_offset);
	if (ret) {
		log_err("Cannot map dumb buffer");
		goto err_fb;
	}

	rb->map = mmap(0, rb->size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, mmap_offset);
	if (rb->map == MAP_FAILED) {
		log_err("cannot mmap dumb buffer");
		ret = -EFAULT;
		goto err_fb;
	}
	memset(rb->map, 0, rb->size);

	return 0;

err_fb:
	drmModeRmFB(fd, rb->id);
err_buf:
	r = drmModeDestroyDumbBuffer(fd, rb->handle);
	if (r)
		log_warning("cannot destroy dumb buffer (%d/%d): %m", r, errno);

	rb->size = 0;
	return ret;
}

static void destroy_rb(int fd, struct uterm_drm2d_rb *rb)
{
	int ret;

	if (!rb->size)
		return;

	munmap(rb->map, rb->size);
	drmModeRmFB(fd, rb->id);
	ret = drmModeDestroyDumbBuffer(fd, rb->handle);
	if (ret)
		log_warning("cannot destroy dumb buffer (%d/%d): %m", ret, errno);
	rb->size = 0;
}

static int display_allocfb(struct uterm_display *disp)
{
	struct uterm_drm_video *vdrm = disp->video->data;
	struct uterm_drm2d_display *d2d = disp->data;
	int ret;

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

	d2d->current_rb = 0;

	ret = init_rb(vdrm->fd, disp->width, disp->height, &d2d->rb[0]);
	if (ret)
		return ret;

	ret = init_rb(vdrm->fd, disp->width, disp->height, &d2d->rb[1]);
	if (ret)
		goto free_rb0;
	return 0;

free_rb0:
	destroy_rb(vdrm->fd, &d2d->rb[0]);
	return 0;
}

static void display_freefb(struct uterm_display *disp)
{
	struct uterm_drm_video *vdrm = disp->video->data;
	struct uterm_drm2d_display *d2d = disp->data;

	destroy_rb(vdrm->fd, &d2d->rb[0]);
	destroy_rb(vdrm->fd, &d2d->rb[1]);
}

static int display_prepare_modeset(struct uterm_display *disp, drmModeAtomicReqPtr req)
{
	struct uterm_drm_video *vdrm = disp->video->data;
	struct uterm_drm2d_display *d2d = disp->data;
	int rb, ret;

	if (!d2d->rb[0].size) {
		ret = display_allocfb(disp);
		if (ret)
			return ret;
	}
	rb = d2d->current_rb ^ 1;

	ret = uterm_drm_prepare_commit(vdrm->fd, &d2d->ddrm, req, d2d->rb[rb].id, disp->width,
				       disp->height, vdrm->cursor_hotspot);
	if (ret)
		return ret;
	return 0;
}

static void display_done_modeset(struct uterm_display *disp, int status)
{
	struct uterm_drm2d_display *d2d = disp->data;
	if (status) {
		display_freefb(disp);
	} else {
		d2d->current_rb = d2d->current_rb ^ 1;
	}
}

static int display_init(struct uterm_display *disp)
{
	struct uterm_drm2d_display *d2d;

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

	disp->data = d2d;

	d2d->ddrm.prepare_modeset = display_prepare_modeset;
	d2d->ddrm.done_modeset = display_done_modeset;

	return 0;
}

static void display_destroy(struct uterm_display *disp)
{
	struct uterm_drm2d_display *d2d = disp->data;

	display_freefb(disp);
	uterm_drm_display_free_properties(disp);
	free(d2d);
}

static int display_swap(struct uterm_display *disp)
{
	struct uterm_drm2d_display *d2d = disp->data;
	int ret;
	int rb;

	rb = d2d->current_rb ^ 1;
	ret = uterm_drm_display_swap(disp, d2d->rb[rb].id);
	if (ret)
		return ret;

	d2d->current_rb = rb;
	return 0;
}

static const struct display_ops drm2d_display_ops = {
	.init = display_init,
	.destroy = display_destroy,
	.set_dpms = uterm_drm_display_set_dpms,
	.use = NULL,
	.swap = display_swap,
	.is_swapping = uterm_drm_is_swapping,
	.fake_blendv = uterm_drm2d_display_fake_blendv,
	.clear = uterm_drm2d_display_clear,
	.set_damage = uterm_drm_display_set_damage,
	.has_damage = uterm_drm_display_has_damage,
	.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)
{
	struct uterm_display *iter;
	struct uterm_drm2d_display *d2d;
	struct uterm_drm2d_rb *rb;
	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;

		/* We use double-buffering so there might be no free back-buffer
		 * here. Hence, draw into the current (pending) front-buffer and
		 * wait for possible page-flips to complete. This might cause
		 * tearing but that's acceptable as this is only called during
		 * wakeup/sleep. */

		d2d = iter->data;
		rb = &d2d->rb[d2d->current_rb];
		memset(rb->map, 0, rb->size);
		uterm_drm_display_wait_pflip(iter);
	}
}

static int video_init(struct uterm_video *video, const char *node)
{
	int ret;
	uint64_t has_dumb;
	struct uterm_drm_video *vdrm;

	ret = uterm_drm_video_init(video, node, &drm2d_display_ops, NULL, NULL);
	if (ret)
		return ret;
	vdrm = video->data;

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

	if (drmGetCap(vdrm->fd, DRM_CAP_DUMB_BUFFER, &has_dumb) < 0 || !has_dumb) {
		log_err("driver does not support dumb buffers");
		uterm_drm_video_destroy(video);
		return -EOPNOTSUPP;
	}

	return 0;
}

static void video_destroy(struct uterm_video *video)
{
	log_info("free drm video device %p", video);
	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;
}

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