/*
  gcc -O2 -g -Wall -I/usr/include/ -I. -Iext/include/ -Iext/include/glib-2.0
      testcase-xcbfail.c -o testcase-xcbfail
      -lglfw -lGL -lpthread  -lX11 -lgstreamer-1.0 -lgstvideo-1.0 -lgstbase-1.0
      -lgobject-2.0 -lglib-2.0 -lgstgl-1.0

  Usage: ./testcase video-file (in current directory)
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <GL/gl.h>
#include <GL/glx.h>
#include <GLFW/glfw3.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <gst/gl/gstglcontext.h>
#include <gst/gl/x11/gstgldisplay_x11.h>

typedef struct {
    GstBus *bus; /* bus for messages, used for context sharing with application */
    GstElement *pipeline; /* the main pipeline for this video */
    GstElement *src; 
    GstElement *sink;
    GstSample *sample, *old_sample; /* current and previous video sample */
    GstVideoFrame v_frame, old_v_frame;  /* current and previous video frame */
    int width, height;
    GLuint tex;
} video_t;
    
/* Helper functions */
gboolean display_init = 0;
GstGLContext *gl_context; /* gstreamer opengl context to wrap application's GLX context */
GstGLDisplay *gl_display; /* app diplay to share with gstreamer */
GLXContext glx_context; /* app GLX context to be shared with gstreamer*/
Display *x11_display; /* app X11 display */
Window x11_window;
const char *platform;

video_t video;
static int win_w, win_h;
static GLFWwindow *window;
char *filename = NULL;

void __attribute__ ((noinline))  display_x11_init() {
  x11_display = glXGetCurrentDisplay();
  x11_window = (Window) glXGetCurrentDrawable();
  glx_context = glXGetCurrentContext ();
  platform = "glx";

  gl_display =
    (GstGLDisplay *) gst_gl_display_x11_new_with_display (x11_display);
  gl_context =
    gst_gl_context_new_wrapped (gl_display, (guintptr) glx_context,
        gst_gl_platform_from_string (platform),
        GST_GL_API_OPENGL);
  display_init = 1;
}

static void video_reshape(GLFWwindow* window, int width, int height) {
  win_w = width;
  win_h = height;
  fprintf(stderr, "resized to %dx%d\n", width, height);
}

static gboolean
video_loop (video_t * video)
{
  //gst_element_set_state (GST_ELEMENT (video->pipeline), GST_STATE_NULL);
  //gst_object_unref (video->pipeline);
  //glDeleteTextures(1, &video->tex);
  g_print ("STUCK EOS message\n");
  video_load(filename);
  return FALSE;
}

/* This function intercepts messages on the bus. Used to share application
 * GLX context and window with gstreamer. */
static gboolean
sync_bus_call (GstBus * bus, GstMessage * msg, gpointer data)
{
  video_t *video = (video_t*) data;
  switch (GST_MESSAGE_TYPE (msg)) {
    case GST_MESSAGE_NEED_CONTEXT:
      {
        const gchar *context_type;

        gst_message_parse_context_type (msg, &context_type);
        g_print ("got need context %s\n", context_type);

        if (g_strcmp0 (context_type, GST_GL_DISPLAY_CONTEXT_TYPE) == 0) {
          GstContext *display_context =
            gst_context_new (GST_GL_DISPLAY_CONTEXT_TYPE, TRUE);
          gst_context_set_gl_display (display_context, gl_display);
          gst_element_set_context (GST_ELEMENT (msg->src), display_context);
          return TRUE;
        } else if (g_strcmp0 (context_type, "gst.gl.app_context") == 0) {
          GstContext *app_context = gst_context_new ("gst.gl.app_context", TRUE);
          GstStructure *s = gst_context_writable_structure (app_context);
          gst_structure_set (s, "context", GST_GL_TYPE_CONTEXT,
              gl_context, NULL);
          gst_element_set_context (GST_ELEMENT (msg->src), app_context);
          return TRUE;
        }
        break;
      }
    case GST_MESSAGE_EOS:
      {
        g_print ("EOS message\n");
        g_main_context_invoke (NULL, (GSourceFunc) video_loop, video);
        break;
      }
    default:
      break;
  }
  return FALSE;
}

static gboolean
on_client_draw (GstElement * glsink, GstGLContext * context, GstSample * sample,
    gpointer data)
{
  video_t *video = (video_t*) data;
  GstVideoInfo v_info;
  GstBuffer *buf;
  GstCaps *caps;

  buf = gst_sample_get_buffer (sample);
  caps = gst_sample_get_caps (sample);

  if (video->sample) {
    video->old_sample = video->sample;
    video->old_v_frame = video->v_frame;
  }
  video->sample = sample;

  gst_video_info_from_caps (&v_info, caps);

  if (!gst_video_frame_map (&video->v_frame, &v_info, buf, GST_MAP_READ | GST_MAP_GL)) {
    fprintf (stderr, "Failed to map the video buffer");
    return TRUE;
  }

  video->tex = *(guint *) video->v_frame.data[0];

  if (video->old_sample) {
    gst_video_frame_unmap (&video->old_v_frame);
  }

  return TRUE;
}



int main(int argc, char **argv)
{
  XInitThreads();
  if (!glfwInit())
    printf("cannot initialize glfw");

  int fullscreen = getenv("INFOBEAMER_FULLSCREEN") != NULL;
  int width = win_w = 1024;
  int height = win_h = 768;

  gst_init (&argc, &argv);

  const char *new_width = getenv("INFOBEAMER_WIDTH");
  if (new_width)
    width = atoi(new_width);

  const char *new_height = getenv("INFOBEAMER_HEIGHT");
  if (new_height)
    height = atoi(new_height);

  fprintf(stderr, "initial size is %dx%d\n", width, height);

  GLFWmonitor* monitor = glfwGetPrimaryMonitor();
  if (!fullscreen)
    monitor = NULL;

  window = glfwCreateWindow(width, height, "test", monitor, NULL);
  if (!window)
    printf("cannot open window");

  glfwSetFramebufferSizeCallback(window, video_reshape);

  glfwMakeContextCurrent(window);
  glfwSwapInterval(1);
  glfwGetFramebufferSize(window, &win_w, &win_h);

  if (fullscreen)
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);

  filename = argv[1];
  video_load(filename);

  GLfloat x1 = 0.0;
  GLfloat x2 = width;
  GLfloat y1 = 0.0;
  GLfloat y2 = height;

  while (!glfwWindowShouldClose(window))
  {
    glEnable(GL_TEXTURE_2D);

    glEnable(GL_BLEND);
    glBlendFuncSeparate(
        GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
        GL_ONE_MINUS_DST_ALPHA, GL_ONE
        );

    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glViewport(0, 0, width, height);
    glOrtho(0, width, height, 0, -1000, 1000);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    glClearColor(0.05, 0.05, 0.05, 1);
    glClear(GL_COLOR_BUFFER_BIT);

    glBindTexture(GL_TEXTURE_2D, video.tex);
    glBegin(GL_QUADS);

    glTexCoord2f(0.0, 0.0); glVertex3f(x1, y1, 0);
    glTexCoord2f(1.0, 0.0); glVertex3f(x2, y1, 0);
    glTexCoord2f(1.0, 1.0); glVertex3f(x2, y2, 0);
    glTexCoord2f(0.0, 1.0); glVertex3f(x1, y2, 0);

    glEnd();
    glfwSwapBuffers(window);
    glfwPollEvents();
  }
  glfwDestroyWindow(window);
  glfwTerminate();

  return 0;
}

int video_load(const char *path) {

  memset(&video, 0, sizeof(video_t));
  char dir[256], uri[256] = "file:///";
  getcwd(dir, sizeof(uri));
  strcat(uri, dir);
  strcat(uri, "/");
  strcat(uri, path);

  if (!display_init)
    display_x11_init();

  video.pipeline = gst_pipeline_new ("pipeline");

  video.src = gst_element_factory_make ("playbin", NULL);
  g_object_set (video.src, "uri", uri, NULL);

  video.sink = gst_element_factory_make ("glimagesink", NULL);
  g_signal_connect (G_OBJECT (video.sink), "client-draw",
      G_CALLBACK (on_client_draw), &video);

  g_object_set (G_OBJECT(video.src), "video-sink", video.sink, NULL);

  gst_bin_add_many (GST_BIN (video.pipeline), 
      video.src,
      NULL);

  video.bus = gst_pipeline_get_bus (GST_PIPELINE (video.pipeline));
  gst_bus_add_signal_watch (video.bus);
  gst_bus_enable_sync_message_emission (video.bus);
  g_signal_connect (video.bus, "sync-message", G_CALLBACK (sync_bus_call), &video);
  gst_object_unref (video.bus);

  gst_element_set_state (GST_ELEMENT (video.pipeline), GST_STATE_PLAYING);

  return 1;
}
