/*
 * Copyright © 2013 Intel Corporation
 *
 * 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 (including the next
 * paragraph) 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.
 */

#include "piglit-util-gl.h"
#include "piglit_drm_dma_buf.h"
#include "drm_fourcc.h"
#ifdef PIGLIT_HAS_GBM_BO_MAP
#include <gbm.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <xf86drm.h>
#include <stdbool.h>
#include <xcb/dri2.h>
#include <drm.h>
#include <unistd.h>

#define ALIGN(value, alignment) (((value) + alignment - 1) & ~(alignment - 1))

struct piglit_drm_driver {
	int fd;
	char *name;

	bool
	(*create)(unsigned w, unsigned h, unsigned cpp,
		  const unsigned char *src_data,
		  struct piglit_dma_buf *buf);

	bool
	(*export)(struct piglit_dma_buf *buf);

	void
	(*destroy)(struct piglit_dma_buf *buf);
};

static const struct piglit_drm_driver *piglit_drm_get_driver(void);

static bool
piglit_drm_x11_authenticate(int fd)
{
	drm_magic_t magic;
	xcb_connection_t *conn;
	int screen;
	const xcb_setup_t *setup;
	xcb_screen_iterator_t screen_iter;
	xcb_dri2_authenticate_cookie_t auth_cookie;
	xcb_dri2_authenticate_reply_t *auth_reply;
	int ret = 0;

	conn = xcb_connect(NULL, &screen);
	if (!conn) {
		printf("piglit: failed to connect to X server for DRI2 "
		       "authentication\n");
		return false;
	}

	ret = drmGetMagic(fd, &magic);
	if (ret) {
		printf("piglit: failed to get DRM magic\n");
		return false;
	}

	setup = xcb_get_setup(conn);
	if (!setup) {
		printf("piglit: xcb_get_setup() failed\n");
		return false;
	}

	screen_iter = xcb_setup_roots_iterator(setup);
	auth_cookie = xcb_dri2_authenticate_unchecked(conn,
	                                              screen_iter.data->root,
	                                              magic);
	auth_reply = xcb_dri2_authenticate_reply(conn, auth_cookie, NULL);

	if (auth_reply == NULL || !auth_reply->authenticated) {
		printf("piglit: failed to authenticate with DRI2\n");
		return false;
	}
	free(auth_reply);

	return true;
}

#ifdef PIGLIT_HAS_GBM_BO_MAP
static struct gbm_device *
piglit_gbm_get(void)
{
	const struct piglit_drm_driver *drv = piglit_drm_get_driver();
	static struct gbm_device *gbm = NULL;

	if (gbm)
		return gbm;

	gbm = gbm_create_device(drv->fd);

	return gbm;
}

static bool
piglit_gbm_buf_create(unsigned w, unsigned h, unsigned fourcc,
			const unsigned char *src_data, struct piglit_dma_buf *buf)
{
	unsigned i;
	struct gbm_bo *bo;
	uint32_t dst_stride;
	struct gbm_device *gbm = piglit_gbm_get();
	void *dst_data;
	void *map_data = NULL;
	enum gbm_bo_format format;
	unsigned cpp = 0, src_stride;
	unsigned buf_w = w;
	unsigned buf_h = h;

	if (!gbm || h % 2 || w % 2)
		return false;

	switch (fourcc) {
	case DRM_FORMAT_R8:
		format = GBM_FORMAT_R8;
		cpp = 1;
		src_stride = cpp * w;
		break;
	case DRM_FORMAT_GR88:
	case DRM_FORMAT_RG88:
	case DRM_FORMAT_YUYV:
	case DRM_FORMAT_UYVY:
		format = GBM_FORMAT_GR88;
		cpp = 2;
		src_stride = cpp * w;
		break;
	case DRM_FORMAT_XRGB8888:
	case DRM_FORMAT_XBGR8888:
	case DRM_FORMAT_RGBX8888:
	case DRM_FORMAT_BGRX8888:
	case DRM_FORMAT_ARGB8888:
	case DRM_FORMAT_ABGR8888:
	case DRM_FORMAT_RGBA8888:
	case DRM_FORMAT_BGRA8888:
	case DRM_FORMAT_AYUV:
	case DRM_FORMAT_XYUV8888:
		format = GBM_BO_FORMAT_ARGB8888;
		cpp = 4;
		src_stride = cpp * w;
		break;
	case DRM_FORMAT_Y410:
		format = GBM_FORMAT_ABGR2101010;
		cpp = 4;
		src_stride = cpp * w;
		break;
	case DRM_FORMAT_Y412:
	case DRM_FORMAT_Y416:
		format = GBM_BO_FORMAT_ARGB8888;
		buf_w = w * 2;
		cpp = 8;
		src_stride = cpp * w;
		break;
	case DRM_FORMAT_Y210:
	case DRM_FORMAT_Y212:
	case DRM_FORMAT_Y216:
		format = GBM_BO_FORMAT_ARGB8888;
		cpp = 4;
		src_stride = cpp * w;
		break;

	/* For YUV formats, the U/V planes might have a greater relative
	 * pitch.  For example, if the driver needs pitch aligned to 32
	 * pixels, for a 4x4 YUV image, the stride of both the Y and U/V
	 * planes will be 32 bytes.  Not 32 for Y and 16 for U/V.  To
	 * account for this, use a 2cpp format with half the width.  For
	 * hardware that only has stride requirements in bytes (rather
	 * than pixels) this will work out the same.  For hardware that
	 * has pitch alignment requirements in pixels, this will give an
	 * overly conservative alignment for Y but a sufficient alignment
	 * for U/V.
	 */
	case DRM_FORMAT_NV12:
		format = GBM_FORMAT_GR88;
		buf_w = w / 2;
		buf_h = h * 3 / 2;
		src_stride = w;
		cpp = 1;
		break;
	case DRM_FORMAT_P010:
	case DRM_FORMAT_P012:
	case DRM_FORMAT_P016:
		format = GBM_FORMAT_GR88;
		buf_h = h * 3 / 2;
		cpp = 2;
		src_stride = cpp * w;
		break;
	case DRM_FORMAT_YUV420:
	case DRM_FORMAT_YVU420:
		format = GBM_FORMAT_GR88;
		buf_w = w / 2;
		buf_h = h * 2;    /* U/V not interleaved */
		src_stride = w;
		cpp = 1;
		break;
	default:
		fprintf(stderr, "invalid fourcc: %.4s\n", (char *)&fourcc);
		return false;
	}

	bo = gbm_bo_create(gbm, buf_w, buf_h, format,
			   GBM_BO_USE_RENDERING | GBM_BO_USE_LINEAR);
	if (!bo)
		return false;

	dst_data = gbm_bo_map(bo, 0, 0, buf_w, buf_h, GBM_BO_TRANSFER_WRITE,
			      &dst_stride, &map_data);
	if (!dst_data) {
		fprintf(stderr, "Failed to map GBM bo\n");
		gbm_bo_destroy(bo);
		return NULL;
	}

	buf->w = w;
	buf->h = h;
	buf->offset[0] = gbm_bo_get_offset(bo, 0);
	buf->stride[0] = gbm_bo_get_stride_for_plane(bo, 0);
	buf->fd = -1;
	buf->priv = bo;

	for (i = 0; i < h; ++i) {
		memcpy((char *)dst_data + i * dst_stride,
		       src_data + i * src_stride,
		       w * cpp);
	}

	switch (fourcc) {
	case DRM_FORMAT_NV12:
	case DRM_FORMAT_P010:
	case DRM_FORMAT_P012:
	case DRM_FORMAT_P016:
		for (i = 0; i < h/2; ++i) {
			memcpy(((char *)dst_data + dst_stride * h) + i * dst_stride,
				(src_data + (w * h * cpp)) + i * src_stride, w * cpp);
		}
		buf->offset[1] = buf->stride[0] * h;
		buf->stride[1] = buf->stride[0];
		break;
	case DRM_FORMAT_YUV420:
	case DRM_FORMAT_YVU420:
		for (i = 0; i < h/2; ++i) {
			memcpy(((char *)dst_data + dst_stride * h) + i * dst_stride,
				(src_data + (w*h)) + i * src_stride / 2, w / 2);
		}
		unsigned cpu_offset2 = dst_stride * h + dst_stride * h / 2;
		for (i = 0; i < h/2; ++i) {
			memcpy(((char *)dst_data + cpu_offset2) + i * dst_stride,
				(src_data + (w*h) + (w*h/4)) + i * src_stride / 2, w / 2);
		}
		buf->offset[1] = buf->stride[0] * h;
		buf->stride[1] = buf->stride[0];
		buf->offset[2] = buf->stride[0] * h + buf->stride[0] * h / 2;
		buf->stride[2] = buf->stride[1];
		break;
	default:
		break;
	}

	gbm_bo_unmap(bo, map_data);


	return true;
}

static bool
piglit_gbm_buf_export(struct piglit_dma_buf *buf)
{
	struct gbm_bo *bo = buf->priv;

	buf->fd = gbm_bo_get_fd(bo);

	return buf->fd >= 0;
}

static void
piglit_gbm_buf_destroy(struct piglit_dma_buf *buf)
{
	struct gbm_bo *bo = buf->priv;

	gbm_bo_destroy(bo);
}
#endif /* PIGLIT_HAS_GBM_BO_MAP */

static int
piglit_drm_get_user_fd(void)
{
	int fd_res = -1;
	char *nodename = getenv("WAFFLE_GBM_DEVICE");
	if (nodename && strlen(nodename))
		fd_res = open(nodename, O_RDWR);

	return fd_res;
}

static const struct piglit_drm_driver *
piglit_drm_get_driver(void)
{
	static struct piglit_drm_driver drv = { /* fd */ -1 };
	drmVersionPtr version = NULL;

	if (drv.fd != -1)
		return &drv;

	drv.fd = piglit_drm_get_user_fd();
	if (drv.fd == -1) {
		drv.fd = open("/dev/dri/renderD128", O_RDWR);

		if (drv.fd == -1) {
			drv.fd = open("/dev/dri/card0", O_RDWR);
			if (drv.fd == -1) {
				fprintf(stderr, "error: failed to open /dev/dri/renderD128 and "
					"/dev/dri/card0\n");
				goto fail;

			}

			if (!piglit_drm_x11_authenticate(drv.fd))
				goto fail;
		}
	}
	version = drmGetVersion(drv.fd);

	if (!version || !version->name) {
		fprintf(stderr, "error: drmGetVersion() failed\n");
		goto fail;
	} else {
#if 0
		fprintf(stderr, "version: %s %d:%d\n",
			version->name, version->version_major, version->version_minor);
#endif
	}

	drv.name = strdup(version->name);
	if (!drv.name) {
		drmFreeVersion(version);
		fprintf(stderr, "out of memory\n");
		abort();
	}

	if (0) {
		/* empty */
	}
#ifdef PIGLIT_HAS_GBM_BO_MAP
	else if (true) {
		drv.create = piglit_gbm_buf_create;
		drv.export = piglit_gbm_buf_export;
		drv.destroy = piglit_gbm_buf_destroy;
	}
#endif
	else {
		fprintf(stderr, "error: unrecognized DRM driver name %s\n",
			version->name);
		goto fail;
	}

	drmFreeVersion(version);
	return &drv;

  fail:
	if (drv.fd != -1) {
		close(drv.fd);
		drv.fd = -1;
	}

	if (drv.name) {
		free(drv.name);
		drv.name = NULL;
	}

	drmFreeVersion(version);
	return NULL;
}

enum piglit_result
piglit_drm_create_dma_buf(unsigned w, unsigned h, unsigned fourcc,
			const void *src_data,  struct piglit_dma_buf **buf)
{
	struct piglit_dma_buf *drm_buf;
	const struct piglit_drm_driver *drv = piglit_drm_get_driver();

	if (!drv)
		return PIGLIT_SKIP;

	drm_buf = calloc(sizeof(struct piglit_dma_buf), 1);
	if (!drm_buf)
		return PIGLIT_FAIL;

	if (!drv->create(w, h, fourcc, src_data, drm_buf)) {
		free(drm_buf);
		return PIGLIT_FAIL;
	}

	if (!drv->export(drm_buf)) {
		free(drm_buf);
		return PIGLIT_FAIL;
	}

	*buf = drm_buf;

	return PIGLIT_PASS;
}

void
piglit_drm_destroy_dma_buf(struct piglit_dma_buf *buf)
{
	const struct piglit_drm_driver *drv;

	if (!buf)
		return;

	drv = piglit_drm_get_driver();
	if (!drv)
		return;

	drv->destroy(buf);
	free(buf);
}
