/*
 * gstvaapiutils_egl.c - EGL utilities
 *
 * Copyright (C) 2014 Intel Corporation
 *   Author: Gwenole Beauchesne <gwenole.beauchesne@intel.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2.1
 * of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301
 */

#include "sysdeps.h"
#include "gstvaapiutils_egl.h"

typedef struct egl_message_s EglMessage;
struct egl_message_s
{
  EglObject base;
  EglContextRunFunc func;
  gpointer args;
};

static void
egl_message_finalize (EglMessage * msg)
{
}

/* ------------------------------------------------------------------------- */
// Utility functions

typedef struct gl_version_info_s GlVersionInfo;
struct gl_version_info_s
{
  guint gles_version;
  guint gl_api_bit;
  guint gl_api;
  const gchar *gl_api_name;
};

static const GlVersionInfo gl_version_info[] = {
  {0, EGL_OPENGL_BIT, EGL_OPENGL_API, "OpenGL"},
  {1, EGL_OPENGL_ES_BIT, EGL_OPENGL_ES_API, "OpenGL_ES"},
  {2, EGL_OPENGL_ES2_BIT, EGL_OPENGL_ES_API, "OpenGL_ES2"},
  {3, EGL_OPENGL_ES3_BIT_KHR, EGL_OPENGL_ES_API, "OpenGL_ES3"},
  {0,}
};

static const GlVersionInfo *
gl_version_info_lookup (guint gles_version)
{
  const GlVersionInfo *vinfo;

  for (vinfo = gl_version_info; vinfo->gl_api_bit != 0; vinfo++) {
    if (vinfo->gles_version == gles_version)
      return vinfo;
  }
  return NULL;
}

static const GlVersionInfo *
gl_version_info_lookup_by_api (guint api)
{
  const GlVersionInfo *vinfo;

  for (vinfo = gl_version_info; vinfo->gl_api_bit != 0; vinfo++) {
    if (api & vinfo->gl_api_bit)
      return vinfo;
  }
  return NULL;
}

static const GlVersionInfo *
gl_version_info_lookup_by_api_name (const gchar * name)
{
  const GlVersionInfo *vinfo;

  for (vinfo = gl_version_info; vinfo->gl_api_bit != 0; vinfo++) {
    if (g_strcmp0 (vinfo->gl_api_name, name) == 0)
      return vinfo;
  }
  return NULL;
}

static gboolean
g_strv_match_string (gchar ** extensions_list, const gchar * name)
{
  if (extensions_list) {
    for (; *extensions_list != NULL; extensions_list++) {
      if (g_strcmp0 (*extensions_list, name) == 0)
        return TRUE;
    }
  }
  return FALSE;
}

static gboolean
egl_find_attrib_value (const EGLint * attribs, EGLint type, EGLint * value_ptr)
{
  while (attribs[0] != EGL_NONE) {
    if (attribs[0] == type) {
      if (value_ptr)
        *value_ptr = attribs[1];
      return TRUE;
    }
    attribs += 2;
  }
  return FALSE;
}

/* ------------------------------------------------------------------------- */
// Basic objects

#define EGL_OBJECT(object) ((EglObject *)(object))
#define EGL_OBJECT_CLASS(klass) ((EglObjectClass *)(klass))

#define EGL_OBJECT_DEFINE_CLASS_WITH_CODE(TN, t_n, code)        \
static void                                                     \
G_PASTE(t_n,_finalize) (TN * object);                           \
                                                                \
static inline const EglObjectClass *                            \
G_PASTE(t_n,_class) (void)                                      \
{                                                               \
  static G_PASTE(TN,Class) g_class;                             \
  static gsize g_class_init = FALSE;                            \
                                                                \
  if (g_once_init_enter (&g_class_init)) {                      \
    GstVaapiMiniObjectClass *const object_class =               \
        GST_VAAPI_MINI_OBJECT_CLASS (&g_class);                 \
    code;                                                       \
    object_class->size = sizeof (TN);                           \
    object_class->finalize = (GDestroyNotify)                   \
      G_PASTE(t_n,_finalize);                                   \
    g_once_init_leave (&g_class_init, TRUE);                    \
  }                                                             \
  return EGL_OBJECT_CLASS (&g_class);                           \
}

#define EGL_OBJECT_DEFINE_CLASS(TN, t_n) \
  EGL_OBJECT_DEFINE_CLASS_WITH_CODE (TN, t_n, /**/)

static inline gpointer
egl_object_new (const EglObjectClass * klass)
{
  return gst_vaapi_mini_object_new (GST_VAAPI_MINI_OBJECT_CLASS (klass));
}

static inline gpointer
egl_object_new0 (const EglObjectClass * klass)
{
  return gst_vaapi_mini_object_new0 (GST_VAAPI_MINI_OBJECT_CLASS (klass));
}

typedef struct egl_object_class_s EglMessageClass;
typedef struct egl_object_class_s EglVTableClass;
typedef struct egl_object_class_s EglDisplayClass;
typedef struct egl_object_class_s EglConfigClass;
typedef struct egl_object_class_s EglContextClass;
typedef struct egl_object_class_s EglSurfaceClass;
typedef struct egl_object_class_s EglProgramClass;
typedef struct egl_object_class_s EglWindowClass;

EGL_OBJECT_DEFINE_CLASS (EglMessage, egl_message);
EGL_OBJECT_DEFINE_CLASS (EglVTable, egl_vtable);
EGL_OBJECT_DEFINE_CLASS (EglDisplay, egl_display);
EGL_OBJECT_DEFINE_CLASS (EglConfig, egl_config);
EGL_OBJECT_DEFINE_CLASS (EglContext, egl_context);
EGL_OBJECT_DEFINE_CLASS (EglSurface, egl_surface);
EGL_OBJECT_DEFINE_CLASS (EglProgram, egl_program);
EGL_OBJECT_DEFINE_CLASS (EglWindow, egl_window);

/* ------------------------------------------------------------------------- */
// Desktop OpenGL and OpenGL|ES dispatcher (vtable)

static GMutex gl_vtables_lock;
static EglVTable *gl_vtables[4];

#if (USE_GLES_VERSION_MASK & (1U << 0))
static const gchar *gl_library_names[] = {
  "libGL.la",
  "libGL.so.1",
  NULL
};
#endif

#if (USE_GLES_VERSION_MASK & (1U << 1))
static const gchar *gles1_library_names[] = {
  "libGLESv1_CM.la",
  "libGLESv1_CM.so.1",
  NULL
};
#endif

#if (USE_GLES_VERSION_MASK & (1U << 2))
static const gchar *gles2_library_names[] = {
  "libGLESv2.la",
  "libGLESv2.so.2",
  NULL
};
#endif

static const gchar **gl_library_names_group[] = {
#if (USE_GLES_VERSION_MASK & (1U << 0))
  gl_library_names,
#endif
  NULL
};

static const gchar **gles1_library_names_group[] = {
#if (USE_GLES_VERSION_MASK & (1U << 1))
  gles1_library_names,
#endif
  NULL
};

static const gchar **gles2_library_names_group[] = {
#if (USE_GLES_VERSION_MASK & (1U << 2))
  gles2_library_names,
#endif
  NULL
};

static const gchar **gles3_library_names_group[] = {
#if (USE_GLES_VERSION_MASK & (1U << 3))
  gles2_library_names,
#endif
  NULL
};

static const gchar ***
egl_vtable_get_library_names_group (guint gles_version)
{
  const gchar ***library_names_group;

  switch (gles_version) {
    case 0:
      library_names_group = gl_library_names_group;
      break;
    case 1:
      library_names_group = gles1_library_names_group;
      break;
    case 2:
      library_names_group = gles2_library_names_group;
      break;
    case 3:
      library_names_group = gles3_library_names_group;
      break;
    default:
      library_names_group = NULL;
      break;
  }
  return library_names_group;
}

static gboolean
egl_vtable_check_extension (EglVTable * vtable, EGLDisplay display,
    gboolean is_egl, const gchar * group_name, guint * group_ptr)
{
  gchar ***extensions_list;
  const gchar *extensions;

  g_return_val_if_fail (group_name != NULL, FALSE);
  g_return_val_if_fail (group_ptr != NULL, FALSE);

  if (*group_ptr > 0)
    return TRUE;

  GST_DEBUG ("check for %s extension %s", is_egl ? "EGL" : "GL", group_name);

  if (is_egl) {
    if (!vtable->egl_extensions) {
      extensions = eglQueryString (display, EGL_EXTENSIONS);
      if (!extensions)
        return FALSE;
      GST_DEBUG ("EGL extensions: %s", extensions);
      vtable->egl_extensions = g_strsplit (extensions, " ", 0);
    }
    extensions_list = &vtable->egl_extensions;
  } else {
    if (!vtable->gl_extensions) {
      extensions = (const gchar *) vtable->glGetString (GL_EXTENSIONS);
      if (!extensions)
        return FALSE;
      GST_DEBUG ("GL extensions: %s", extensions);
      vtable->gl_extensions = g_strsplit (extensions, " ", 0);
    }
    extensions_list = &vtable->gl_extensions;
  }
  if (!g_strv_match_string (*extensions_list, group_name))
    return FALSE;

  GST_LOG ("  found %s extension %s", is_egl ? "EGL" : "GL", group_name);
  (*group_ptr)++;
  return TRUE;
}

static gboolean
egl_vtable_load_symbol (EglVTable * vtable, EGLDisplay display, gboolean is_egl,
    const gchar * symbol_name, gpointer * symbol_ptr,
    const gchar * group_name, guint * group_ptr)
{
  void (*symbol) (void);

  if (group_ptr && !*group_ptr) {
    if (!egl_vtable_check_extension (vtable, display, is_egl, group_name,
            group_ptr))
      return FALSE;
  }

  if (is_egl) {
    symbol = eglGetProcAddress (symbol_name);
  } else {
    if (!g_module_symbol (vtable->base.handle.p, symbol_name,
            (gpointer *) & symbol))
      return FALSE;
  }
  if (!symbol)
    return FALSE;

  GST_LOG ("  found symbol %s", symbol_name);
  if (symbol_ptr)
    *symbol_ptr = symbol;
  if (group_ptr)
    (*group_ptr)++;
  return TRUE;
}

static gboolean
egl_vtable_load_egl_symbols (EglVTable * vtable, EGLDisplay display)
{
  guint n = 0;

#define EGL_DEFINE_EXTENSION(NAME) do {                         \
      egl_vtable_check_extension (vtable, display, TRUE,        \
          "EGL_" G_STRINGIFY (NAME),                            \
          &vtable->GL_PROTO_GEN_CONCAT(has_EGL_,NAME));         \
    } while (0);

#define EGL_PROTO_BEGIN(NAME, TYPE, EXTENSION) do {             \
      n += egl_vtable_load_symbol (vtable, display, TRUE,       \
          G_STRINGIFY(GL_PROTO_GEN_CONCAT(egl,NAME)),           \
          (gpointer *) &vtable->GL_PROTO_GEN_CONCAT(egl,NAME),  \
          "EGL_" G_STRINGIFY(EXTENSION),                        \
          &vtable->GL_PROTO_GEN_CONCAT(has_EGL_,EXTENSION));    \
    } while (0);

#include "egl_vtable.h"

  vtable->num_egl_symbols = n;
  return TRUE;
}

static gboolean
egl_vtable_load_gl_symbols (EglVTable * vtable, EGLDisplay display)
{
  guint n = 0;

  vtable->has_GL_CORE_1_0 = 1;
  vtable->has_GL_CORE_1_1 = 1;
  vtable->has_GL_CORE_1_3 = 1;
  vtable->has_GL_CORE_2_0 = 1;

#define GL_DEFINE_EXTENSION(NAME) do {                          \
      egl_vtable_check_extension (vtable, display, FALSE,       \
          "GL_" G_STRINGIFY (NAME),                             \
          &vtable->GL_PROTO_GEN_CONCAT(has_GL_,NAME));          \
    } while (0);

#define GL_PROTO_BEGIN(NAME, TYPE, EXTENSION) do {              \
      n += egl_vtable_load_symbol (vtable, display, FALSE,      \
          G_STRINGIFY(GL_PROTO_GEN_CONCAT(gl,NAME)),            \
          (gpointer *) &vtable->GL_PROTO_GEN_CONCAT(gl,NAME),   \
          "GL_" G_STRINGIFY(EXTENSION),                         \
          &vtable->GL_PROTO_GEN_CONCAT(has_GL_,EXTENSION));     \
    } while (0);

#include "egl_vtable.h"

  --vtable->has_GL_CORE_1_0;
  --vtable->has_GL_CORE_1_1;
  --vtable->has_GL_CORE_1_3;
  --vtable->has_GL_CORE_2_0;

  vtable->num_gl_symbols = n;
  return TRUE;
}

static gboolean
egl_vtable_try_load_library (EglVTable * vtable, const gchar * name)
{
  if (vtable->base.handle.p)
    g_module_close (vtable->base.handle.p);
  vtable->base.handle.p = g_module_open (name,
      G_MODULE_BIND_LAZY | G_MODULE_BIND_LOCAL);
  if (!vtable->base.handle.p)
    return FALSE;

  GST_DEBUG ("loaded backend: %s", g_module_name (vtable->base.handle.p));
  return TRUE;
}

static gboolean
egl_vtable_find_library (EglVTable * vtable)
{
  const gchar ***library_names_ptr =
      egl_vtable_get_library_names_group (vtable->gles_version);

  if (!library_names_ptr)
    return FALSE;

  for (; *library_names_ptr != NULL; library_names_ptr++) {
    const gchar **library_name_ptr = *library_names_ptr;
    for (; *library_name_ptr != NULL; library_name_ptr++) {
      if (egl_vtable_try_load_library (vtable, *library_name_ptr))
        return TRUE;
    }
  }
  return FALSE;
}

static gboolean
egl_vtable_init (EglVTable * vtable, EGLDisplay display, guint gles_version)
{
  GST_DEBUG ("initialize for OpenGL|ES API version %d", gles_version);

  vtable->gles_version = gles_version;
  if (!egl_vtable_find_library (vtable))
    return FALSE;
  if (!egl_vtable_load_egl_symbols (vtable, display))
    return FALSE;
  return TRUE;
}

static void
egl_vtable_finalize (EglVTable * vtable)
{
  g_strfreev (vtable->egl_extensions);
  g_strfreev (vtable->gl_extensions);
  if (vtable->base.handle.p)
    g_module_close (vtable->base.handle.p);

  if (vtable->base.is_wrapped) {
    g_mutex_lock (&gl_vtables_lock);
    gl_vtables[vtable->gles_version] = NULL;
    g_mutex_unlock (&gl_vtables_lock);
  }
}

static EglVTable *
egl_vtable_new (EglDisplay * display, guint gles_version)
{
  EglVTable *vtable;

  g_return_val_if_fail (display != NULL, NULL);

  vtable = egl_object_new0 (egl_vtable_class ());
  if (!vtable
      || !egl_vtable_init (vtable, display->base.handle.p, gles_version))
    goto error;
  return vtable;

error:
  egl_object_replace (&vtable, NULL);
  return NULL;
}

static EglVTable *
egl_vtable_new_cached (EglDisplay * display, guint gles_version)
{
  EglVTable *vtable, **vtable_ptr;

  g_return_val_if_fail (gles_version < G_N_ELEMENTS (gl_vtables), NULL);

  vtable_ptr = &gl_vtables[gles_version];

  g_mutex_lock (&gl_vtables_lock);
  vtable = *vtable_ptr;
  if (vtable)
    egl_object_ref (vtable);
  else {
    vtable = egl_vtable_new (display, gles_version);
    if (vtable) {
      vtable->base.is_wrapped = TRUE;
      *vtable_ptr = vtable;
    }
  }
  g_mutex_unlock (&gl_vtables_lock);
  return vtable;
}

/* ------------------------------------------------------------------------- */
// EGL Display

static gboolean
egl_display_run (EglDisplay * display, EglContextRunFunc func, gpointer args)
{
  EglMessage *msg;

  if (display->gl_thread == g_thread_self ()) {
    func (args);
    return TRUE;
  }

  msg = egl_object_new0 (egl_message_class ());
  if (!msg)
    return FALSE;

  msg->base.is_valid = TRUE;
  msg->func = func;
  msg->args = args;
  g_async_queue_push (display->gl_queue, egl_object_ref (msg));

  g_mutex_lock (&display->mutex);
  while (msg->base.is_valid)
    g_cond_wait (&display->gl_thread_ready, &display->mutex);
  g_mutex_unlock (&display->mutex);
  egl_object_unref (msg);
  return TRUE;
}

static gpointer
egl_display_thread (gpointer data)
{
  EglDisplay *const display = data;
  EGLDisplay gl_display = display->base.handle.p;
  EGLint major_version, minor_version;
  gchar **gl_apis, **gl_api;

  if (!display->base.is_wrapped) {
    gl_display = display->base.handle.p = eglGetDisplay (gl_display);
    if (!gl_display)
      goto error;
    if (!eglInitialize (gl_display, &major_version, &minor_version))
      goto error;
  }

  display->gl_vendor_string =
      g_strdup (eglQueryString (gl_display, EGL_VENDOR));
  display->gl_version_string =
      g_strdup (eglQueryString (gl_display, EGL_VERSION));
  display->gl_apis_string =
      g_strdup (eglQueryString (gl_display, EGL_CLIENT_APIS));

  GST_INFO ("EGL vendor: %s", display->gl_vendor_string);
  GST_INFO ("EGL version: %s", display->gl_version_string);
  GST_INFO ("EGL client APIs: %s", display->gl_apis_string);

  gl_apis = g_strsplit (display->gl_apis_string, " ", 0);
  if (!gl_apis)
    goto error;
  for (gl_api = gl_apis; *gl_api != NULL; gl_api++) {
    const GlVersionInfo *const vinfo =
        gl_version_info_lookup_by_api_name (*gl_api);

    if (vinfo)
      display->gl_apis |= vinfo->gl_api_bit;
  }
  g_strfreev (gl_apis);
  if (!display->gl_apis)
    goto error;

  display->base.is_valid = TRUE;
  g_cond_broadcast (&display->gl_thread_ready);

  while (!display->gl_thread_cancel) {
    EglMessage *const msg =
        g_async_queue_timeout_pop (display->gl_queue, 100000);

    if (msg) {
      if (msg->base.is_valid) {
        msg->func (msg->args);
        msg->base.is_valid = FALSE;
        g_cond_broadcast (&display->gl_thread_ready);
      }
      egl_object_unref (msg);
    }
  }

done:
  if (gl_display != EGL_NO_DISPLAY && !display->base.is_wrapped)
    eglTerminate (gl_display);
  display->base.handle.p = NULL;
  g_cond_broadcast (&display->gl_thread_ready);
  return NULL;

error:
  display->base.is_valid = FALSE;
  goto done;
}

static gboolean
egl_display_init (EglDisplay * display)
{
  display->gl_queue =
      g_async_queue_new_full ((GDestroyNotify) gst_vaapi_mini_object_unref);
  if (!display->gl_queue)
    return FALSE;

  g_mutex_init (&display->mutex);
  g_cond_init (&display->gl_thread_ready);
  display->gl_thread = g_thread_try_new ("OpenGL Thread", egl_display_thread,
      display, NULL);
  if (!display->gl_thread)
    return FALSE;

  g_mutex_lock (&display->mutex);
  g_cond_wait (&display->gl_thread_ready, &display->mutex);
  g_mutex_unlock (&display->mutex);
  return display->base.is_valid;
}

static void
egl_display_finalize (EglDisplay * display)
{
  display->gl_thread_cancel = TRUE;
  g_thread_join (display->gl_thread);
  g_cond_clear (&display->gl_thread_ready);
  g_mutex_clear (&display->mutex);
  g_async_queue_unref (display->gl_queue);

  g_free (display->gl_vendor_string);
  g_free (display->gl_version_string);
  g_free (display->gl_apis_string);
}

static EglDisplay *
egl_display_new_full (gpointer handle, gboolean is_wrapped)
{
  EglDisplay *display;

  display = egl_object_new0 (egl_display_class ());
  if (!display)
    return NULL;

  display->base.handle.p = handle;
  display->base.is_wrapped = is_wrapped;
  if (!egl_display_init (display))
    goto error;
  return display;

error:
  egl_object_unref (display);
  return NULL;
}

EglDisplay *
egl_display_new (gpointer native_display)
{
  g_return_val_if_fail (native_display != NULL, NULL);

  return egl_display_new_full (native_display, FALSE);
}

EglDisplay *
egl_display_new_wrapped (EGLDisplay gl_display)
{
  g_return_val_if_fail (gl_display != EGL_NO_DISPLAY, NULL);

  return egl_display_new_full (gl_display, TRUE);
}

/* ------------------------------------------------------------------------- */
// EGL Config

static gboolean
egl_config_init (EglConfig * config, EglDisplay * display,
    const EGLint * attribs)
{
  EGLDisplay const gl_display = display->base.handle.p;
  const GlVersionInfo *vinfo;
  EGLConfig gl_config;
  EGLint v, gl_apis, num_configs;

  egl_object_replace (&config->display, display);

  if (!eglChooseConfig (gl_display, attribs, &gl_config, 1, &num_configs))
    return FALSE;
  if (num_configs != 1)
    return FALSE;
  config->base.handle.p = gl_config;

  if (!eglGetConfigAttrib (gl_display, gl_config, EGL_CONFIG_ID, &v))
    return FALSE;
  config->config_id = v;

  if (!eglGetConfigAttrib (gl_display, gl_config, EGL_NATIVE_VISUAL_ID, &v))
    return FALSE;
  config->visual_id = v;

  if (!eglGetConfigAttrib (gl_display, gl_config, EGL_RENDERABLE_TYPE, &v))
    return FALSE;
  if (!egl_find_attrib_value (attribs, EGL_RENDERABLE_TYPE, &gl_apis))
    return FALSE;
  vinfo = gl_version_info_lookup_by_api (v & gl_apis);
  if (!vinfo)
    return FALSE;
  config->gles_version = vinfo->gles_version;
  config->gl_api = vinfo->gles_version > 0 ? EGL_OPENGL_ES_API : EGL_OPENGL_API;
  return TRUE;
}

static void
egl_config_finalize (EglConfig * config)
{
  egl_object_replace (&config->display, NULL);
}

EglConfig *
egl_config_new (EglDisplay * display, guint gles_version, GstVideoFormat format)
{
  EGLint attribs[2 * 6 + 1], *attrib = attribs;
  const GstVideoFormatInfo *finfo;
  const GlVersionInfo *vinfo;

  g_return_val_if_fail (display != NULL, NULL);

  finfo = gst_video_format_get_info (format);
  if (!finfo || !GST_VIDEO_FORMAT_INFO_IS_RGB (finfo))
    return NULL;

  vinfo = gl_version_info_lookup (gles_version);
  if (!vinfo)
    return NULL;

  *attrib++ = EGL_COLOR_BUFFER_TYPE;
  *attrib++ = EGL_RGB_BUFFER;
  *attrib++ = EGL_RED_SIZE;
  *attrib++ = GST_VIDEO_FORMAT_INFO_DEPTH (finfo, GST_VIDEO_COMP_R);
  *attrib++ = EGL_GREEN_SIZE;
  *attrib++ = GST_VIDEO_FORMAT_INFO_DEPTH (finfo, GST_VIDEO_COMP_G);
  *attrib++ = EGL_BLUE_SIZE;
  *attrib++ = GST_VIDEO_FORMAT_INFO_DEPTH (finfo, GST_VIDEO_COMP_B);
  *attrib++ = EGL_ALPHA_SIZE;
  *attrib++ = GST_VIDEO_FORMAT_INFO_DEPTH (finfo, GST_VIDEO_COMP_A);
  *attrib++ = EGL_RENDERABLE_TYPE;
  *attrib++ = vinfo->gl_api_bit;
  *attrib++ = EGL_NONE;
  g_assert (attrib - attribs <= G_N_ELEMENTS (attribs));

  return egl_config_new_with_attribs (display, attribs);
}

EglConfig *
egl_config_new_with_attribs (EglDisplay * display, const EGLint * attribs)
{
  EglConfig *config;

  g_return_val_if_fail (display != NULL, NULL);
  g_return_val_if_fail (attribs != NULL, NULL);

  config = egl_object_new0 (egl_config_class ());
  if (!config || !egl_config_init (config, display, attribs))
    goto error;
  return config;

error:
  egl_object_replace (&config, NULL);
  return NULL;
}

static EglConfig *
egl_config_new_from_gl_context (EglDisplay * display, EGLContext gl_context)
{
  EGLDisplay const gl_display = display->base.handle.p;
  EGLint attribs[3 * 2 + 1], *attrib = attribs;
  EGLint config_id, api, v;
  guint gles_version;
  const GlVersionInfo *vinfo;

  if (!eglQueryContext (gl_display, gl_context, EGL_CONFIG_ID, &config_id))
    return NULL;
  if (!eglQueryContext (gl_display, gl_context, EGL_CONTEXT_CLIENT_TYPE, &api))
    return NULL;
  if (!eglQueryContext (gl_display, gl_context, EGL_CONTEXT_CLIENT_VERSION, &v))
    return NULL;

  if (api == EGL_OPENGL_API)
    gles_version = 0;
  else if (api == EGL_OPENGL_ES_API)
    gles_version = v;
  else {
    GST_ERROR ("unsupported EGL client API (%d)", api);
    return NULL;
  }

  vinfo = gl_version_info_lookup (gles_version);
  if (!vinfo)
    return NULL;

  *attrib++ = EGL_COLOR_BUFFER_TYPE;
  *attrib++ = EGL_RGB_BUFFER;
  *attrib++ = EGL_CONFIG_ID;
  *attrib++ = config_id;
  *attrib++ = EGL_RENDERABLE_TYPE;
  *attrib++ = vinfo->gl_api_bit;
  *attrib++ = EGL_NONE;
  g_assert (attrib - attribs <= G_N_ELEMENTS (attribs));

  return egl_config_new_with_attribs (display, attribs);
}

/* ------------------------------------------------------------------------- */
// EGL Surface

static void
egl_surface_finalize (EglSurface * surface)
{
  if (surface->base.handle.p != EGL_NO_SURFACE && !surface->base.is_wrapped)
    eglDestroySurface (surface->display->base.handle.p, surface->base.handle.p);
  egl_object_replace (&surface->display, NULL);
}

static EglSurface *
egl_surface_new_wrapped (EglDisplay * display, EGLSurface gl_surface)
{
  EglSurface *surface;

  g_return_val_if_fail (display != NULL, NULL);

  surface = egl_object_new (egl_surface_class ());
  if (!surface)
    return NULL;

  surface->base.is_wrapped = TRUE;
  surface->base.handle.p = gl_surface;
  surface->display = egl_object_ref (display);
  return surface;
}

/* ------------------------------------------------------------------------- */
// EGL Context

static void egl_context_state_get_current (EglContextState * cs);

static gboolean
egl_context_state_set_current (EglContextState * new_cs,
    EglContextState * old_cs);

static gboolean
ensure_vtable (EglContext * ctx)
{
  if (!ctx->vtable) {
    ctx->vtable = egl_vtable_new_cached (ctx->display,
        ctx->config ? ctx->config->gles_version : 0);
    if (!ctx->vtable)
      return FALSE;
  }
  return TRUE;
}

static gboolean
ensure_context (EglContext * ctx, EGLContext gl_parent_context)
{
  EGLDisplay *const gl_display = ctx->display->base.handle.p;
  EGLContext gl_context = ctx->base.handle.p;
  EGLint gles_attribs[3], *attribs = NULL;

  if (!gl_context) {
    if (ctx->config->gles_version >= 2) {
      attribs = gles_attribs;
      attribs[0] = EGL_CONTEXT_CLIENT_VERSION;
      attribs[1] = ctx->config->gles_version;
      attribs[2] = EGL_NONE;
    }

    gl_context = eglCreateContext (gl_display, ctx->config->base.handle.p,
        gl_parent_context, attribs);
    if (!gl_context)
      goto error_create_context;
    ctx->base.handle.p = gl_context;
  }
  return TRUE;

  /* ERRORS */
error_create_context:
  GST_ERROR ("failed to create EGL context");
  return FALSE;
}

static inline gboolean
ensure_gl_is_surfaceless (EglContext * ctx)
{
  return ctx->vtable->has_EGL_KHR_surfaceless_context ||
      (ctx->read_surface && ctx->draw_surface);
}

static gboolean
ensure_gl_scene (EglContext * ctx)
{
  EglVTable *vtable;

  if (!ensure_gl_is_surfaceless (ctx))
    return FALSE;

  if (ctx->base.is_valid)
    return TRUE;

  vtable = egl_context_get_vtable (ctx, TRUE);
  if (!vtable)
    return FALSE;

  vtable->glClearColor (0.0, 0.0, 0.0, 1.0);
  if (ctx->config && ctx->config->gles_version == 0)
    vtable->glEnable (GL_TEXTURE_2D);
  vtable->glDisable (GL_BLEND);
  vtable->glDisable (GL_DEPTH_TEST);

  ctx->base.is_valid = TRUE;
  return TRUE;
}

/**
 * egl_context_state_get_current:
 * @cs: return location to the current #EglContextState
 *
 * Retrieves the current EGL context, display and surface to pack into
 * the #EglContextState struct.
 */
static void
egl_context_state_get_current (EglContextState * cs)
{
  cs->display = eglGetCurrentDisplay ();
  cs->context = eglGetCurrentContext ();
  if (cs->context) {
    cs->read_surface = eglGetCurrentSurface (EGL_READ);
    cs->draw_surface = eglGetCurrentSurface (EGL_DRAW);
  } else {
    cs->read_surface = EGL_NO_SURFACE;
    cs->draw_surface = EGL_NO_SURFACE;
  }
}

/**
 * egl_context_state_set_current:
 * @new_cs: the requested new #EglContextState
 * @old_cs: return location to the context that was previously current
 *
 * Makes the @new_cs EGL context the current EGL rendering context of
 * the calling thread, replacing the previously current context if
 * there was one.
 *
 * If @old_cs is non %NULL, the previously current EGL context and
 * surface are recorded.
 *
 * Return value: %TRUE on success
 */
static gboolean
egl_context_state_set_current (EglContextState * new_cs,
    EglContextState * old_cs)
{
  /* If display is NULL, this could be that new_cs was retrieved from
     egl_context_state_get_current() with none set previously. If that case,
     the other fields are also NULL and we don't return an error */
  if (!new_cs->display)
    return !new_cs->context && !new_cs->read_surface && !new_cs->draw_surface;

  if (old_cs) {
    if (old_cs == new_cs)
      return TRUE;
    egl_context_state_get_current (old_cs);
    if (old_cs->display == new_cs->display &&
        old_cs->context == new_cs->context &&
        old_cs->read_surface == new_cs->read_surface &&
        old_cs->draw_surface == new_cs->draw_surface)
      return TRUE;
  }
  return eglMakeCurrent (new_cs->display, new_cs->draw_surface,
      new_cs->read_surface, new_cs->context);
}

static gboolean
egl_context_init (EglContext * ctx, EglDisplay * display, EglConfig * config,
    EGLContext gl_parent_context)
{
  egl_object_replace (&ctx->display, display);
  egl_object_replace (&ctx->config, config);

  if (config)
    eglBindAPI (config->gl_api);

  if (!ensure_vtable (ctx))
    return FALSE;
  if (!ensure_context (ctx, gl_parent_context))
    return FALSE;
  return TRUE;
}

static void
egl_context_finalize (EglContext * ctx)
{
  if (ctx->base.handle.p && !ctx->base.is_wrapped)
    eglDestroyContext (ctx->display->base.handle.p, ctx->base.handle.p);
  egl_object_replace (&ctx->read_surface, NULL);
  egl_object_replace (&ctx->draw_surface, NULL);
  egl_object_replace (&ctx->config, NULL);
  egl_object_replace (&ctx->display, NULL);
  egl_object_replace (&ctx->vtable, NULL);
}

typedef struct
{
  EglDisplay *display;
  EglConfig *config;
  EGLContext gl_parent_context;
  EglContext *context;          /* result */
} CreateContextArgs;

static void
do_egl_context_new (CreateContextArgs * args)
{
  EglContext *ctx;

  ctx = egl_object_new0 (egl_context_class ());
  if (!ctx || !egl_context_init (ctx, args->display, args->config,
          args->gl_parent_context))
    goto error;
  args->context = ctx;
  return;

error:
  egl_object_replace (&ctx, NULL);
  args->context = NULL;
}

EglContext *
egl_context_new (EglDisplay * display, EglConfig * config, EglContext * parent)
{
  CreateContextArgs args;

  g_return_val_if_fail (display != NULL, NULL);
  g_return_val_if_fail (config != NULL, NULL);

  args.display = display;
  args.config = config;
  args.gl_parent_context = parent ? parent->base.handle.p : EGL_NO_CONTEXT;
  if (!egl_display_run (display, (EglContextRunFunc) do_egl_context_new, &args))
    return NULL;
  return args.context;
}

EglContext *
egl_context_new_wrapped (EglDisplay * display, EGLContext gl_context)
{
  CreateContextArgs args;
  EglConfig *config;
  gboolean success;

  g_return_val_if_fail (display != NULL, NULL);
  g_return_val_if_fail (gl_context != EGL_NO_CONTEXT, NULL);

  config = egl_config_new_from_gl_context (display, gl_context);
  if (!config)
    return NULL;

  args.display = display;
  args.config = config;
  args.gl_parent_context = gl_context;
  success = egl_display_run (display, (EglContextRunFunc) do_egl_context_new,
      &args);
  egl_object_unref (config);
  if (!success)
    return NULL;
  return args.context;
}

EglVTable *
egl_context_get_vtable (EglContext * ctx, gboolean need_gl_symbols)
{
  g_return_val_if_fail (ctx != NULL, NULL);
  g_return_val_if_fail (ctx->display->gl_thread == g_thread_self (), NULL);

  if (!ensure_vtable (ctx))
    return NULL;

  if (need_gl_symbols && !(ctx->vtable->num_gl_symbols > 0 ||
          egl_vtable_load_gl_symbols (ctx->vtable,
              ctx->display->base.handle.p)))
    return NULL;
  return ctx->vtable;
}

static void
egl_context_set_surface (EglContext * ctx, EglSurface * surface)
{
  g_return_if_fail (ctx != NULL);
  g_return_if_fail (surface != NULL);

  egl_object_replace (&ctx->read_surface, surface);
  egl_object_replace (&ctx->draw_surface, surface);
}

gboolean
egl_context_set_current (EglContext * ctx, gboolean activate,
    EglContextState * old_cs)
{
  EglContextState cs, *new_cs;

  g_return_val_if_fail (ctx != NULL, FALSE);
  g_return_val_if_fail (ctx->display->gl_thread == g_thread_self (), FALSE);

  if (activate) {
    new_cs = &cs;
    new_cs->display = ctx->display->base.handle.p;
    new_cs->context = ctx->base.handle.p;
    new_cs->draw_surface = ctx->draw_surface ?
        ctx->draw_surface->base.handle.p : EGL_NO_SURFACE;
    new_cs->read_surface = ctx->read_surface ?
        ctx->read_surface->base.handle.p : EGL_NO_SURFACE;
  } else if (old_cs) {
    new_cs = old_cs;
    old_cs = NULL;
  } else {
    new_cs = &cs;
    new_cs->display = ctx->display->base.handle.p;
    new_cs->context = EGL_NO_CONTEXT;
    new_cs->draw_surface = EGL_NO_SURFACE;
    new_cs->read_surface = EGL_NO_SURFACE;
    old_cs = NULL;
  }

  if (!egl_context_state_set_current (new_cs, old_cs))
    return FALSE;
  if (activate && !ensure_gl_scene (ctx))
    return FALSE;
  return TRUE;
}

gboolean
egl_context_run (EglContext * ctx, EglContextRunFunc func, gpointer args)
{
  g_return_val_if_fail (ctx != NULL, FALSE);
  g_return_val_if_fail (func != NULL, FALSE);

  return egl_display_run (ctx->display, func, args);
}

/* ------------------------------------------------------------------------- */
// EGL Program

static GLuint
egl_compile_shader (EglContext * ctx, GLenum type, const char *source)
{
  EglVTable *const vtable = egl_context_get_vtable (ctx, TRUE);
  GLuint shader;
  GLint status;
  char log[BUFSIZ];
  GLsizei log_length;

  shader = vtable->glCreateShader (type);
  vtable->glShaderSource (shader, 1, &source, NULL);
  vtable->glCompileShader (shader);
  vtable->glGetShaderiv (shader, GL_COMPILE_STATUS, &status);
  if (!status) {
    GST_ERROR ("failed to compile %s shader",
        type == GL_FRAGMENT_SHADER ? "fragment" :
        type == GL_VERTEX_SHADER ? "vertex" : "<unknown>");

    vtable->glGetShaderInfoLog (shader, sizeof (log), &log_length, log);
    GST_ERROR ("info log: %s", log);
    return 0;
  }
  return shader;
}

static void
egl_program_finalize (EglProgram * program)
{
  EglVTable *const vtable = program->vtable;

  if (program->base.handle.u)
    vtable->glDeleteProgram (program->base.handle.u);
  if (program->frag_shader)
    vtable->glDeleteShader (program->frag_shader);
  if (program->vert_shader)
    vtable->glDeleteShader (program->vert_shader);
  egl_object_replace (&program->vtable, NULL);
}

static gboolean
egl_program_init (EglProgram * program, EglContext * ctx,
    const gchar * frag_shader_text, const gchar * vert_shader_text)
{
  EglVTable *const vtable = egl_context_get_vtable (ctx, TRUE);
  GLuint prog_id;
  char msg[BUFSIZ];
  GLsizei msglen;
  GLint status;

  if (ctx->config->gles_version == 1)
    goto error_unsupported_gles_version;

  program->vtable = egl_object_ref (vtable);

  program->frag_shader =
      egl_compile_shader (ctx, GL_FRAGMENT_SHADER, frag_shader_text);
  if (!program->frag_shader)
    return FALSE;

  program->vert_shader =
      egl_compile_shader (ctx, GL_VERTEX_SHADER, vert_shader_text);
  if (!program->vert_shader)
    return FALSE;

  prog_id = vtable->glCreateProgram ();
  if (!prog_id)
    return FALSE;
  program->base.handle.u = prog_id;

  vtable->glAttachShader (prog_id, program->frag_shader);
  vtable->glAttachShader (prog_id, program->vert_shader);
  vtable->glBindAttribLocation (prog_id, 0, "position");
  vtable->glBindAttribLocation (prog_id, 1, "texcoord");
  vtable->glLinkProgram (prog_id);

  vtable->glGetProgramiv (prog_id, GL_LINK_STATUS, &status);
  if (!status)
    goto error_link_program;
  return TRUE;

  /* ERRORS */
error_unsupported_gles_version:
  GST_ERROR ("unsupported shader with OpenGL|ES version 1");
  return FALSE;
error_link_program:
  vtable->glGetProgramInfoLog (prog_id, sizeof (msg), &msglen, msg);
  GST_ERROR ("failed to link program: %s", msg);
  return FALSE;
}

EglProgram *
egl_program_new (EglContext * ctx, const gchar * frag_shader_text,
    const gchar * vert_shader_text)
{
  EglProgram *program;

  g_return_val_if_fail (ctx != NULL, NULL);
  g_return_val_if_fail (frag_shader_text != NULL, NULL);
  g_return_val_if_fail (vert_shader_text != NULL, NULL);

  program = egl_object_new0 (egl_program_class ());
  if (!program
      || !egl_program_init (program, ctx, frag_shader_text, vert_shader_text))
    goto error;
  return program;

error:
  egl_object_replace (&program, NULL);
  return NULL;
}

/* ------------------------------------------------------------------------- */
// EGL Window

static gboolean
egl_window_init (EglWindow * window, EglContext * ctx, gpointer native_window)
{
  EGLSurface gl_surface;

  window->context = egl_context_new (ctx->display, ctx->config, ctx);
  if (!window->context)
    return FALSE;
  ctx = window->context;

  gl_surface = eglCreateWindowSurface (ctx->display->base.handle.p,
      ctx->config->base.handle.p, (EGLNativeWindowType) native_window, NULL);
  if (!gl_surface)
    return FALSE;

  window->surface = egl_surface_new_wrapped (ctx->display, gl_surface);
  if (!window->surface)
    goto error_create_surface;
  window->base.handle.p = gl_surface;
  window->base.is_wrapped = FALSE;

  egl_context_set_surface (ctx, window->surface);
  return TRUE;

  /* ERRORS */
error_create_surface:
  GST_ERROR ("failed to create EGL wrapper surface");
  eglDestroySurface (ctx->display->base.handle.p, gl_surface);
  return FALSE;
}

static void
egl_window_finalize (EglWindow * window)
{
  if (window->context && window->base.handle.p)
    eglDestroySurface (window->context->display->base.handle.p,
        window->base.handle.p);

  egl_object_replace (&window->surface, NULL);
  egl_object_replace (&window->context, NULL);
}

EglWindow *
egl_window_new (EglContext * ctx, gpointer native_window)
{
  EglWindow *window;

  g_return_val_if_fail (ctx != NULL, NULL);
  g_return_val_if_fail (native_window != NULL, NULL);

  window = egl_object_new0 (egl_window_class ());
  if (!window || !egl_window_init (window, ctx, native_window))
    goto error;
  return window;

error:
  egl_object_replace (&window, NULL);
  return NULL;
}

/* ------------------------------------------------------------------------- */
// Misc utility functions

void
egl_matrix_set_identity (gfloat m[16])
{
#define MAT(m,r,c) (m)[(c) * 4 + (r)]
  MAT (m, 0, 0) = 1.0;
  MAT (m, 0, 1) = 0.0;
  MAT (m, 0, 2) = 0.0;
  MAT (m, 0, 3) = 0.0;
  MAT (m, 1, 0) = 0.0;
  MAT (m, 1, 1) = 1.0;
  MAT (m, 1, 2) = 0.0;
  MAT (m, 1, 3) = 0.0;
  MAT (m, 2, 0) = 0.0;
  MAT (m, 2, 1) = 0.0;
  MAT (m, 2, 2) = 1.0;
  MAT (m, 2, 3) = 0.0;
  MAT (m, 3, 0) = 0.0;
  MAT (m, 3, 1) = 0.0;
  MAT (m, 3, 2) = 0.0;
  MAT (m, 3, 3) = 1.0;
#undef MAT
}

/**
 * egl_create_texture:
 * @ctx: the parent #EglContext object
 * @target: the target to which the texture is bound
 * @format: the format of the pixel data
 * @width: the requested width, in pixels
 * @height: the requested height, in pixels
 *
 * Creates a texture with the specified dimensions and @format. The
 * internal format will be automatically derived from @format.
 *
 * Return value: the newly created texture name
 */
guint
egl_create_texture (EglContext * ctx, guint target, guint format,
    guint width, guint height)
{
  EglVTable *const vtable = egl_context_get_vtable (ctx, TRUE);
  guint internal_format, texture, bytes_per_component;

  internal_format = format;
  switch (format) {
    case GL_LUMINANCE:
      bytes_per_component = 1;
      break;
    case GL_LUMINANCE_ALPHA:
      bytes_per_component = 2;
      break;
    case GL_RGBA:
    case GL_BGRA_EXT:
      internal_format = GL_RGBA;
      bytes_per_component = 4;
      break;
    default:
      bytes_per_component = 0;
      break;
  }
  g_assert (bytes_per_component > 0);

  vtable->glGenTextures (1, &texture);
  vtable->glBindTexture (target, texture);

  if (width > 0 && height > 0)
    vtable->glTexImage2D (target, 0, internal_format, width, height, 0,
        format, GL_UNSIGNED_BYTE, NULL);

  vtable->glTexParameteri (target, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  vtable->glTexParameteri (target, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  vtable->glTexParameteri (target, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  vtable->glTexParameteri (target, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  vtable->glPixelStorei (GL_UNPACK_ALIGNMENT, bytes_per_component);

  return texture;
}

/**
 * egl_destroy_texture:
 * @ctx: the parent #EglContext object
 * @texture: the texture name to delete
 *
 * Destroys the supplied @texture name.
 */
void
egl_destroy_texture (EglContext * ctx, guint texture)
{
  EglVTable *const vtable = egl_context_get_vtable (ctx, TRUE);

  vtable->glDeleteTextures (1, &texture);
}
