/*
 * GStreamer
 * Copyright (C) 2014 Matthew Waters <ystreet00@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 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
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

/**
 * SECTION:gstgldisplay_egl
 * @short_description: EGL Display connection
 * @title: GstGLDisplayEGL
 * @see_also: #GstGLDisplay
 *
 * #GstGLDisplayEGL represents a connection to an EGL `EGLDisplay` handle created
 * internally (gst_gl_display_egl_new() or gst_gl_display_egl_new_surfaceless())
 * or wrapped by the application (gst_gl_display_egl_new_with_egl_display())
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "gstgldisplay_egl.h"

#include <gst/gl/gstglfeature.h>

#include "gstegl.h"
#include "gsteglimage.h"
#include "gstglmemoryegl.h"

GST_DEBUG_CATEGORY_STATIC (gst_gl_display_egl_debug);
#define GST_CAT_DEFAULT gst_gl_display_egl_debug

#ifndef EGL_PLATFORM_X11
#define EGL_PLATFORM_X11 0x31D5
#endif
#ifndef EGL_PLATFORM_WAYLAND
#define EGL_PLATFORM_WAYLAND 0x31D8
#endif
#ifndef EGL_PLATFORM_GBM_MESA
#define EGL_PLATFORM_GBM_MESA 0x31D7
#endif
#ifndef EGL_PLATFORM_ANDROID
#define EGL_PLATFORM_ANDROID 0x3141
#endif
#ifndef EGL_PLATFORM_DEVICE_EXT
#define EGL_PLATFORM_DEVICE_EXT 0x313F
#endif
#ifndef EGL_PLATFORM_ANGLE_ANGLE
#define EGL_PLATFORM_ANGLE_ANGLE 0x3202
#endif

typedef EGLDisplay (*_gst_eglGetPlatformDisplay_type) (EGLenum platform,
    void *native_display, const EGLint * attrib_list);

G_DEFINE_TYPE (GstGLDisplayEGL, gst_gl_display_egl, GST_TYPE_GL_DISPLAY);

static void gst_gl_display_egl_finalize (GObject * object);
static guintptr gst_gl_display_egl_get_handle (GstGLDisplay * display);

static void
init_debug (void)
{
  static gsize _init = 0;

  if (g_once_init_enter (&_init)) {
    GST_DEBUG_CATEGORY_INIT (GST_CAT_DEFAULT, "gldisplayegl", 0,
        "OpenGL EGL Display");
    g_once_init_leave (&_init, 1);
  }
}

static void
gst_gl_display_egl_class_init (GstGLDisplayEGLClass * klass)
{
  GST_GL_DISPLAY_CLASS (klass)->get_handle =
      GST_DEBUG_FUNCPTR (gst_gl_display_egl_get_handle);

  G_OBJECT_CLASS (klass)->finalize = gst_gl_display_egl_finalize;
}

static void
gst_gl_display_egl_init (GstGLDisplayEGL * display_egl)
{
  GstGLDisplay *display = (GstGLDisplay *) display_egl;

  display->type = GST_GL_DISPLAY_TYPE_EGL;
  display_egl->foreign_display = FALSE;

  gst_gl_memory_egl_init_once ();
}

static void
gst_gl_display_egl_finalize (GObject * object)
{
  GstGLDisplayEGL *display_egl = GST_GL_DISPLAY_EGL (object);

  if (display_egl->display && !display_egl->foreign_display) {
    eglTerminate (display_egl->display);
    display_egl->display = NULL;
  }

  G_OBJECT_CLASS (gst_gl_display_egl_parent_class)->finalize (object);
}

/**
 * gst_gl_display_egl_get_from_native:
 * @type: a #GstGLDisplayType
 * @display: pointer to a display (or 0)
 *
 * Attempts to create a new `EGLDisplay` from @display.  If @type is
 * %GST_GL_DISPLAY_TYPE_ANY or %GST_GL_DISPLAY_TYPE_EGL_SURFACELESS, then
 * @display must be 0. @type must not be %GST_GL_DISPLAY_TYPE_NONE.
 *
 * Returns: (nullable): A `EGLDisplay` or `EGL_NO_DISPLAY`
 *
 * Since: 1.12
 */
gpointer
gst_gl_display_egl_get_from_native (GstGLDisplayType type, guintptr display)
{
  const gchar *egl_exts;
  EGLDisplay ret = EGL_NO_DISPLAY;
  _gst_eglGetPlatformDisplay_type _gst_eglGetPlatformDisplay = NULL;

  g_return_val_if_fail (type != GST_GL_DISPLAY_TYPE_NONE, EGL_NO_DISPLAY);
  g_return_val_if_fail ((type != GST_GL_DISPLAY_TYPE_ANY &&
          type != GST_GL_DISPLAY_TYPE_EGL_SURFACELESS && display != 0)
      || ((type == GST_GL_DISPLAY_TYPE_ANY ||
              type == GST_GL_DISPLAY_TYPE_EGL_SURFACELESS) && display == 0),
      EGL_NO_DISPLAY);

  init_debug ();

  /* given an EGLDisplay already */
  if (type == GST_GL_DISPLAY_TYPE_EGL)
    return (gpointer) display;

  egl_exts = eglQueryString (EGL_NO_DISPLAY, EGL_EXTENSIONS);
  GST_DEBUG ("egl no display extensions: %s", egl_exts);

  if (eglGetError () != EGL_SUCCESS || !egl_exts)
    goto default_display;

  /* check if we can actually choose the egl display type */
  if (!gst_gl_check_extension ("EGL_KHR_client_get_all_proc_addresses",
          egl_exts))
    goto default_display;
  if (!gst_gl_check_extension ("EGL_EXT_platform_base", egl_exts))
    goto default_display;

  /* we need EXT for WinRT to pass attributes */
#if !GST_GL_HAVE_WINDOW_WINRT
  _gst_eglGetPlatformDisplay = (_gst_eglGetPlatformDisplay_type)
      eglGetProcAddress ("eglGetPlatformDisplay");
#endif
  if (!_gst_eglGetPlatformDisplay)
    _gst_eglGetPlatformDisplay = (_gst_eglGetPlatformDisplay_type)
        eglGetProcAddress ("eglGetPlatformDisplayEXT");
  if (!_gst_eglGetPlatformDisplay)
    goto default_display;

  /* try each platform in turn */
#if GST_GL_HAVE_WINDOW_X11
  if (ret == EGL_NO_DISPLAY && (type & GST_GL_DISPLAY_TYPE_X11) &&
      (gst_gl_check_extension ("EGL_KHR_platform_x11", egl_exts) ||
          gst_gl_check_extension ("EGL_EXT_platform_x11", egl_exts))) {
    ret = _gst_eglGetPlatformDisplay (EGL_PLATFORM_X11, (gpointer) display,
        NULL);
  }
#endif
#if GST_GL_HAVE_WINDOW_WAYLAND
  if (ret == EGL_NO_DISPLAY && (type & GST_GL_DISPLAY_TYPE_WAYLAND) &&
      (gst_gl_check_extension ("EGL_KHR_platform_wayland", egl_exts) ||
          gst_gl_check_extension ("EGL_EXT_platform_wayland", egl_exts))) {
    ret = _gst_eglGetPlatformDisplay (EGL_PLATFORM_WAYLAND, (gpointer) display,
        NULL);
  }
#endif
#if GST_GL_HAVE_WINDOW_GBM
  if (ret == EGL_NO_DISPLAY && (type & GST_GL_DISPLAY_TYPE_GBM) &&
      (gst_gl_check_extension ("EGL_MESA_platform_gbm", egl_exts) ||
          gst_gl_check_extension ("EGL_KHR_platform_gbm", egl_exts))) {
    ret = _gst_eglGetPlatformDisplay (EGL_PLATFORM_GBM_MESA, (gpointer) display,
        NULL);
  }
#endif
#if GST_GL_HAVE_WINDOW_WINRT
  if (ret == EGL_NO_DISPLAY && (type & GST_GL_DISPLAY_TYPE_EGL) &&
      (gst_gl_check_extension ("EGL_ANGLE_platform_angle", egl_exts) ||
          gst_gl_check_extension ("EGL_ANGLE_platform_angle", egl_exts))) {
    const EGLint attrs[] = {
      /* These are the default display attributes, used to request ANGLE's
       * D3D11 renderer. eglInitialize will only succeed with these
       * attributes if the hardware supports D3D11 Feature Level 10_0+. */
      EGL_PLATFORM_ANGLE_TYPE_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE,

#ifdef EGL_ANGLE_DISPLAY_ALLOW_RENDER_TO_BACK_BUFFER
      /* EGL_ANGLE_DISPLAY_ALLOW_RENDER_TO_BACK_BUFFER is an optimization
       * that can have large performance benefits on mobile devices. Its
       * syntax is subject to change, though. Please update your Visual
       * Studio templates if you experience compilation issues with it. */
      EGL_ANGLE_DISPLAY_ALLOW_RENDER_TO_BACK_BUFFER, EGL_TRUE,
#endif

      /* EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE is an option that
       * enables ANGLE to automatically call the IDXGIDevice3::Trim method
       * on behalf of the application when it gets suspended. Calling
       * IDXGIDevice3::Trim when an application is suspended is a Windows
       * Store application certification requirement. */
      EGL_PLATFORM_ANGLE_ENABLE_AUTOMATIC_TRIM_ANGLE, EGL_TRUE,
      EGL_NONE,
    };
    ret = _gst_eglGetPlatformDisplay (EGL_PLATFORM_ANGLE_ANGLE,
        (gpointer) display, attrs);
  }
#endif
  if (ret == EGL_NO_DISPLAY && (type & GST_GL_DISPLAY_TYPE_EGL_DEVICE) &&
      (gst_gl_check_extension ("EGL_EXT_device_base", egl_exts) &&
          gst_gl_check_extension ("EGL_EXT_platform_device", egl_exts))) {
    ret =
        _gst_eglGetPlatformDisplay (EGL_PLATFORM_DEVICE_EXT, (gpointer) display,
        NULL);
  }
  /* android only has one winsys/display connection */
  if (ret == EGL_NO_DISPLAY && (type & GST_GL_DISPLAY_TYPE_EGL_SURFACELESS) &&
      gst_gl_check_extension ("EGL_MESA_platform_surfaceless", egl_exts)) {
    ret = _gst_eglGetPlatformDisplay (EGL_PLATFORM_SURFACELESS_MESA,
        (gpointer) display, NULL);
  }

  if (ret != EGL_NO_DISPLAY)
    return ret;

  /* otherwise rely on the implementation to choose the correct display
   * based on the pointer */
default_display:
  return (gpointer) eglGetDisplay ((EGLNativeDisplayType) display);
}

/**
 * gst_gl_display_egl_new:
 *
 * Create a new #GstGLDisplayEGL using the default EGL_DEFAULT_DISPLAY.
 *
 * The returned #GstGLDisplayEGL will by default free all EGL resources when
 * finalized. See gst_gl_display_egl_set_foreign() for details on if you need
 * the EGLDisplay to remain alive.
 *
 * Returns: (transfer full) (nullable): a new #GstGLDisplayEGL or %NULL
 */
GstGLDisplayEGL *
gst_gl_display_egl_new (void)
{
  GstGLDisplayEGL *ret;
  gpointer display;

  init_debug ();

  display = gst_gl_display_egl_get_from_native (GST_GL_DISPLAY_TYPE_ANY, 0);

  if (!display) {
    GST_INFO ("Failed to open EGL display connection");
    return NULL;
  }

  ret = g_object_new (GST_TYPE_GL_DISPLAY_EGL, NULL);
  gst_object_ref_sink (ret);
  ret->display = display;

  return ret;
}

/**
 * gst_gl_display_egl_new_surfaceless:
 *
 * Create a new surfaceless #GstGLDisplayEGL using the Mesa3D
 * EGL_PLATFORM_SURFACELESS_MESA extension.
 *
 * Returns: (transfer full) (nullable): a new #GstGLDisplayEGL or %NULL
 *
 * Since: 1.24
 */
GstGLDisplayEGL *
gst_gl_display_egl_new_surfaceless (void)
{
  GstGLDisplayEGL *ret;
  gpointer display;

  init_debug ();

  display =
      gst_gl_display_egl_get_from_native (GST_GL_DISPLAY_TYPE_EGL_SURFACELESS,
      0);

  if (!display) {
    GST_INFO ("Failed to create a surfaceless EGL display");
    return NULL;
  }

  ret = g_object_new (GST_TYPE_GL_DISPLAY_EGL, NULL);
  gst_object_ref_sink (ret);
  ret->display = display;

  return ret;
}

/**
 * gst_gl_display_egl_new_with_display:
 * @display: an existing and connected EGLDisplay
 *
 * Creates a new display connection from a EGLDisplay. The display will be
 * marked as foreign and freeing some EGL resources must be completed by the
 * application.  See gst_gl_display_egl_set_foreign() for more details.
 *
 * Returns: (transfer full): a new #GstGLDisplayEGL
 *
 * Since: 1.12
 */
GstGLDisplayEGL *
gst_gl_display_egl_new_with_egl_display (gpointer display)
{
  GstGLDisplayEGL *ret;

  g_return_val_if_fail (display != NULL, NULL);

  init_debug ();

  ret = g_object_new (GST_TYPE_GL_DISPLAY_EGL, NULL);
  gst_object_ref_sink (ret);

  ret->display = display;
  ret->foreign_display = TRUE;

  return ret;
}

static gpointer
_ref_if_set (gpointer data, gpointer user_data)
{
  if (data)
    gst_object_ref (data);
  return data;
}

/**
 * gst_gl_display_egl_from_gl_display:
 * @display: (transfer none): an existing #GstGLDisplay
 *
 * Creates a EGL display connection from a native Display.
 *
 * This function will return the same value for multiple calls with the same
 * @display.
 *
 * The returned #GstGLDisplayEGL will *not* be marked as foreign and will free
 * some display global EGL resources on finalization. If an external API/user
 * will be also handling the lifetime of the `EGLDisplay`, you should mark the
 * returned #GstGLDisplayEGL as foreign by calling gst_gl_display_egl_set_foreign().
 *
 * Returns: (transfer full) (nullable): a new #GstGLDisplayEGL
 *
 * Since: 1.12
 */
GstGLDisplayEGL *
gst_gl_display_egl_from_gl_display (GstGLDisplay * display)
{
  GstGLDisplayEGL *ret;
  GstGLDisplayType display_type;
  guintptr native_display;

  g_return_val_if_fail (GST_IS_GL_DISPLAY (display), NULL);

  init_debug ();

  if (GST_IS_GL_DISPLAY_EGL (display)) {
    GST_LOG_OBJECT (display, "display %" GST_PTR_FORMAT "is already a "
        "GstGLDisplayEGL", display);
    return gst_object_ref (display);
  }

  /* try to get a previously set GstGLDisplayEGL */
  ret = g_object_dup_data (G_OBJECT (display), GST_GL_DISPLAY_EGL_NAME,
      (GDuplicateFunc) _ref_if_set, NULL);
  if (ret && GST_IS_GL_DISPLAY_EGL (ret)) {
    GST_LOG_OBJECT (display, "display %" GST_PTR_FORMAT "already has a "
        "GstGLDisplayEGL %" GST_PTR_FORMAT, display, ret);
    return ret;
  }

  if (ret)
    gst_object_unref (ret);

  display_type = gst_gl_display_get_handle_type (display);
  native_display = gst_gl_display_get_handle (display);

  g_return_val_if_fail (native_display != 0, NULL);
  g_return_val_if_fail (display_type != GST_GL_DISPLAY_TYPE_NONE, NULL);

  ret = g_object_new (GST_TYPE_GL_DISPLAY_EGL, NULL);
  gst_object_ref_sink (ret);

  ret->display =
      gst_gl_display_egl_get_from_native (display_type, native_display);

  if (!ret->display) {
    GST_WARNING_OBJECT (ret, "failed to get EGLDisplay from native display");
    gst_object_unref (ret);
    return NULL;
  }
  g_object_set_data_full (G_OBJECT (display), GST_GL_DISPLAY_EGL_NAME,
      gst_object_ref (ret), (GDestroyNotify) gst_object_unref);

  return ret;
}

/**
 * gst_gl_display_egl_set_foreign:
 * @display_egl: a #GstGLDisplayEGL
 * @foreign: whether @display_egl should be marked as containing a foreign
 *           `EGLDisplay`
 *
 * Configure whether or not this EGL display is foreign and is managed by an
 * external application/library.
 *
 * A display marked as foreign will not have display global resources freed when
 * this display is finalized. As such, any external API using the same
 * `EGLDisplay` must keep the `EGLDisplay` alive while GStreamer is using any
 * EGL or GL resources associated with that `EGLDisplay`.  The reverse is also
 * true and a foreign #GstGLDisplayEGL must not be used after the associated
 * `EGLDisplay` has been destroyed externally with `eglTerminate()`.
 *
 * A non-foreign #GstGLDisplayEGL will destroy the associated `EGLDisplay` on
 * finalization. This can also be useful when a user would like GStreamer to
 * assume ownership of the `EGLDisplay` after calling e.g.
 * gst_gl_display_egl_new_with_egl_display().
 *
 * Since: 1.26
 */
void
gst_gl_display_egl_set_foreign (GstGLDisplayEGL * display_egl, gboolean foreign)
{
  g_return_if_fail (GST_IS_GL_DISPLAY_EGL (display_egl));

  GST_OBJECT_LOCK (display_egl);
  display_egl->foreign_display = foreign;
  GST_OBJECT_UNLOCK (display_egl);
}

static guintptr
gst_gl_display_egl_get_handle (GstGLDisplay * display)
{
  return (guintptr) GST_GL_DISPLAY_EGL (display)->display;
}
