/*
 * Copyright (c) 2017 Rob Clark <rclark@redhat.com>
 * Copyright (c) 2017 Carlos Rafael Giani <dv@pseudoterminal.org>
 *
 * 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, sub license,
 * 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 NON-INFRINGEMENT. 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 <assert.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "common.h"

#include <drm_fourcc.h>

#include <gst/gst.h>
#include <gst/gl/gl.h>
#include <gst/gl/egl/gstgldisplay_egl.h>
#include <gst/gstmemory.h>
#include <gst/gstpad.h>
#include <gst/allocators/gstdmabuf.h>
#include <gst/app/gstappsink.h>
#include <gst/video/gstvideometa.h>

GST_DEBUG_CATEGORY_EXTERN(kmscube_debug);
#define GST_CAT_DEFAULT kmscube_debug

struct decoder {
	GMainLoop          *loop;
	GstElement         *pipeline;
	GstElement         *sink;
	pthread_t           gst_thread;

	GstVideoInfo        info;

	unsigned            frame;

	GstSample          *last_samp;
};

static GstPadProbeReturn
pad_probe(GstPad *pad, GstPadProbeInfo *info, gpointer user_data)
{
	struct decoder *dec = user_data;
	GstEvent *event = GST_PAD_PROBE_INFO_EVENT(info);
	GstCaps *caps;

	(void)pad;

	if (GST_EVENT_TYPE(event) != GST_EVENT_CAPS)
		return GST_PAD_PROBE_OK;

	gst_event_parse_caps(event, &caps);

	if (!caps) {
		GST_ERROR("caps event without caps");
		return GST_PAD_PROBE_OK;
	}

	if (!gst_video_info_from_caps(&dec->info, caps)) {
		GST_ERROR("caps event with invalid video caps");
		return GST_PAD_PROBE_OK;
	}

	if(!GST_VIDEO_INFO_IS_RGB(&(dec->info))) {
		GST_ERROR("not RGB format\n");
		return GST_PAD_PROBE_OK;
	}

	return GST_PAD_PROBE_OK;
}

static void *
gst_thread_func(void *args)
{
	struct decoder *dec = args;
	g_main_loop_run(dec->loop);
	return NULL;
}

static void
element_added_cb(GstBin *bin, GstElement *element, gpointer user_data)
{
	GstElementFactory *elem_factory;
	gchar const *factory_name;

	(void)user_data;
	(void)bin;

	elem_factory = gst_element_get_factory(element);
	factory_name = gst_plugin_feature_get_name(elem_factory);

	GST_DEBUG("added element %s (created with factory %s)", GST_OBJECT_NAME(element), factory_name);

	/* v4l2 video decoder factories are generated by the GStreamer v4l probe.
	 * The format is v4l2videoNdec, where N is an integer. So, check if the
	 * element's factory name fits this pattern. */
	if (g_str_has_prefix(factory_name, "v4l2video") && g_str_has_suffix(factory_name, "dec")) {
		/* yes, "capture" rather than "output" because v4l2 is bonkers */
		gst_util_set_object_arg(G_OBJECT(element), "capture-io-mode", "dmabuf");
		printf("found GStreamer V4L2 video decoder element with name \"%s\"\n", GST_OBJECT_NAME(element));
	}
}

static gboolean
bus_watch_cb(GstBus *bus, GstMessage *msg, gpointer user_data)
{
	struct decoder *dec = (struct decoder *)user_data;

	(void)bus;

	switch (GST_MESSAGE_TYPE(msg)) {
	case GST_MESSAGE_STATE_CHANGED: {
		gchar *dotfilename;
		GstState old_gst_state, cur_gst_state, pending_gst_state;

		/* Only consider state change messages coming from
		 * the toplevel element. */
		if (GST_MESSAGE_SRC(msg) != GST_OBJECT(dec->pipeline))
			break;

		gst_message_parse_state_changed(msg, &old_gst_state, &cur_gst_state, &pending_gst_state);

		printf(
			"GStreamer state change:  old: %s  current: %s  pending: %s\n",
			gst_element_state_get_name(old_gst_state),
			gst_element_state_get_name(cur_gst_state),
			gst_element_state_get_name(pending_gst_state)
		);

		dotfilename = g_strdup_printf(
			"statechange__old-%s__cur-%s__pending-%s",
			gst_element_state_get_name(old_gst_state),
			gst_element_state_get_name(cur_gst_state),
			gst_element_state_get_name(pending_gst_state)
		);
		GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS(GST_BIN(dec->pipeline), GST_DEBUG_GRAPH_SHOW_ALL, dotfilename);
		g_free(dotfilename);

		break;
	}
	case GST_MESSAGE_REQUEST_STATE: {
		GstState requested_state;
		gst_message_parse_request_state(msg, &requested_state);
		printf(
			"state change to %s was requested by %s\n",
			gst_element_state_get_name(requested_state),
			GST_MESSAGE_SRC_NAME(msg)
		);
		gst_element_set_state(GST_ELEMENT(dec->pipeline), requested_state);
		break;
	}
	case GST_MESSAGE_LATENCY: {
		printf("redistributing latency\n");
		gst_bin_recalculate_latency(GST_BIN(dec->pipeline));
		break;
	}
	case GST_MESSAGE_INFO:
	case GST_MESSAGE_WARNING:
	case GST_MESSAGE_ERROR: {
		GError *error = NULL;
		gchar *debug_info = NULL;
		gchar const *prefix;

		switch (GST_MESSAGE_TYPE(msg)) {
			case GST_MESSAGE_INFO:
				gst_message_parse_info(msg, &error, &debug_info);
				prefix = "INFO";
				break;
			case GST_MESSAGE_WARNING:
				gst_message_parse_warning(msg, &error, &debug_info);
				prefix = "WARNING";
				break;
			case GST_MESSAGE_ERROR:
				gst_message_parse_error(msg, &error, &debug_info);
				prefix = "ERROR";
				break;
			default:
				g_assert_not_reached();
		}
		printf("GStreamer %s: %s; debug info: %s", prefix, error->message, debug_info);

		g_clear_error(&error);
		g_free(debug_info);

		if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_ERROR) {
			GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS(GST_BIN(dec->pipeline), GST_DEBUG_GRAPH_SHOW_ALL, "error");
		}

		// TODO: stop mainloop in case of an error

		break;
	}
	default:
		break;
	}

	return TRUE;
}

struct decoder *
video_init(const struct egl *egl, const struct gbm *gbm, const char *filename)
{
	struct decoder *dec;
	GstElement *src, *decodebin;
	GstPad *pad;
	GstBus *bus;

	(void)gbm;

	dec = calloc(1, sizeof(*dec));
	dec->loop = g_main_loop_new(NULL, FALSE);

	/* Setup pipeline: */
	static const char *pipeline =
		"filesrc name=\"src\" ! "
		"decodebin3 name=\"decode\" ! "
		"glupload ! "
		"glcolorconvert ! "
		"video/x-raw(memory:GLMemory),format=RGB,texture-target=\"2D\" ! "
		"appsink sync=false name=\"sink\"";

	dec->pipeline = gst_parse_launch(pipeline, NULL);

	dec->sink = gst_bin_get_by_name(GST_BIN(dec->pipeline), "sink");

	/* Implement the allocation query using a pad probe. This probe will
	 * adverstize support for GstVideoMeta, which avoid hardware accelerated
	 * decoder that produce special strides and offsets from having to
	 * copy the buffers.
	 */
	pad = gst_element_get_static_pad(dec->sink, "sink");
	gst_pad_add_probe(pad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM,
		pad_probe, dec, NULL);
	gst_object_unref(pad);

	src = gst_bin_get_by_name(GST_BIN(dec->pipeline), "src");
	g_object_set(G_OBJECT(src), "location", filename, NULL);
	gst_object_unref(src);

	/* Configure the sink like a video sink (mimic GstVideoSink) */
	gst_base_sink_set_max_lateness(GST_BASE_SINK(dec->sink), 20 * GST_MSECOND);
	gst_base_sink_set_qos_enabled(GST_BASE_SINK(dec->sink), TRUE);

	/* if we don't limit max-buffers then we can let the decoder outrun
	 * vsync and quickly chew up 100's of MB of buffers:
	 */
	g_object_set(G_OBJECT(dec->sink), "max-buffers", 2, NULL);

	/* callback needed to make sure we get dmabuf's from v4l2videoNdec.. */
	decodebin = gst_bin_get_by_name(GST_BIN(dec->pipeline), "decode");
	g_signal_connect(decodebin, "element-added", G_CALLBACK(element_added_cb), dec);

	/* add bus to be able to receive error message, handle latency
	 * requests, produce pipeline dumps, etc. */
	bus = gst_pipeline_get_bus(GST_PIPELINE(dec->pipeline));
	gst_bus_add_watch(bus, bus_watch_cb, dec);
	gst_object_unref(GST_OBJECT(bus));


	GstGLPlatform gst_gl_platform = GST_GL_PLATFORM_EGL;
	GstGLDisplay *gst_gl_display = (GstGLDisplay*)gst_gl_display_egl_new_with_egl_display(egl->display);
	GstGLAPI gst_gl_api = gst_gl_context_get_current_gl_api(gst_gl_platform, NULL, NULL);
	GstGLContext *gst_gl_context = gst_gl_context_new_wrapped(gst_gl_display, (guintptr)egl->context, gst_gl_platform, gst_gl_api);
	gst_gl_context_activate(gst_gl_context, TRUE);

	GError *error;
	if (!gst_gl_context_fill_info(gst_gl_context, &error)) {
		fprintf(stderr, "Failed to fill in wrapped GStreamer context: %s\n", error->message);
		g_clear_error(&error);
		return NULL;
	}

	/* set context here so we don't have to do it later in the bus request handler */
	GstContext *display_context = gst_context_new (GST_GL_DISPLAY_CONTEXT_TYPE, TRUE);
	gst_context_set_gl_display (display_context, gst_gl_display);
	gst_element_set_context (GST_ELEMENT (dec->pipeline), display_context);

	GstContext *app_context = gst_context_new ("gst.gl.app_context", TRUE);
	gst_structure_set (gst_context_writable_structure (app_context), "context", GST_TYPE_GL_CONTEXT, gst_gl_context, NULL);
	gst_element_set_context (GST_ELEMENT (dec->pipeline), app_context);

	/* let 'er rip! */
	gst_element_set_state(dec->pipeline, GST_STATE_PLAYING);

	pthread_create(&dec->gst_thread, NULL, gst_thread_func, dec);

	return dec;
}

static void
set_last_frame(struct decoder *dec, GstSample *samp)
{
	if (dec->last_samp)
		gst_sample_unref(dec->last_samp);
	dec->last_samp = samp;
}

GLint
video_frame(struct decoder *dec)
{
	GstSample *samp = gst_app_sink_pull_sample(GST_APP_SINK(dec->sink));
	if (!samp) {
		GST_DEBUG("got no appsink sample");
		return -1;
	}

	GstBuffer *buf = gst_sample_get_buffer(samp);

	guint nmems = gst_buffer_n_memory(buf);
	if (nmems > 1) {
		/* this case currently is not defined */
		GST_FIXME("gstbuffers with multiple memory blocks are not supported");
		return -1;
	}

	GstMemory *mem = gst_buffer_peek_memory(buf, 0);
	if (!mem) {
		GST_ERROR("no memory in buffer");
		return -1;
	}

	if (!gst_is_gl_memory(mem)) {
		GST_ERROR("not gl memory");
		return -1;
	}

	/* output some information at the beginning (= when the first frame is handled) */
	if (dec->frame == 0) {
		guint width = GST_VIDEO_INFO_WIDTH(&(dec->info));
		guint height = GST_VIDEO_INFO_HEIGHT(&(dec->info));
		GstVideoFormat pixfmt = GST_VIDEO_INFO_FORMAT(&(dec->info));
		const char *pixfmt_str = gst_video_format_to_string(pixfmt);
		guint nplanes = GST_VIDEO_INFO_N_PLANES(&(dec->info));

		printf("===================================\n");
		printf("GStreamer video stream information:\n");
		printf("  size: %u x %u pixel\n", width, height);
		printf("  pixel format: %s  number of planes: %u\n", pixfmt_str, nplanes);
		printf("===================================\n");
	}

	set_last_frame(dec, samp);

	dec->frame++;

	return ((GstGLMemory *)mem)->tex_id;
}

void video_deinit(struct decoder *dec)
{
	set_last_frame(dec, NULL);
	gst_element_set_state(dec->pipeline, GST_STATE_NULL);
	gst_object_unref(dec->sink);
	gst_object_unref(dec->pipeline);
	g_main_loop_quit(dec->loop);
	g_main_loop_unref(dec->loop);
	pthread_join(dec->gst_thread, 0);
	free(dec);
}
