/* gdkgltexture.c
 *
 * Copyright 2016  Benjamin Otte
 *
 * 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 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, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "gdkgltextureprivate.h"

#include "gdkdisplayprivate.h"
#include "gdkglcontextprivate.h"
#include "gdkmemoryformatprivate.h"
#include "gdkmemorytextureprivate.h"
#include "gdktextureprivate.h"

#include <epoxy/gl.h>

/**
 * GdkGLTexture:
 *
 * A GdkTexture representing a GL texture object.
 */

struct _GdkGLTexture {
  GdkTexture parent_instance;

  GdkGLContext *context;
  guint id;

  GdkTexture *saved;

  GDestroyNotify destroy;
  gpointer data;
};

struct _GdkGLTextureClass {
  GdkTextureClass parent_class;
};

G_DEFINE_TYPE (GdkGLTexture, gdk_gl_texture, GDK_TYPE_TEXTURE)

static void
gdk_gl_texture_dispose (GObject *object)
{
  GdkGLTexture *self = GDK_GL_TEXTURE (object);

  if (self->destroy)
    {
      self->destroy (self->data);
      self->destroy = NULL;
      self->data = NULL;
    }

  g_clear_object (&self->context);
  self->id = 0;

  g_clear_object (&self->saved);

  G_OBJECT_CLASS (gdk_gl_texture_parent_class)->dispose (object);
}

typedef struct _InvokeData
{
  GdkGLTexture *self;
  volatile int spinlock;
  GFunc func;
  gpointer data;
} InvokeData;

static gboolean
gdk_gl_texture_invoke_callback (gpointer data)
{
  InvokeData *invoke = data;
  GdkGLContext *context;

  context = gdk_display_get_gl_context (gdk_gl_context_get_display (invoke->self->context));

  gdk_gl_context_make_current (context);
  glBindTexture (GL_TEXTURE_2D, invoke->self->id);

  invoke->func (invoke->self, invoke->data);

  g_atomic_int_set (&invoke->spinlock, 1);

  return FALSE;
}

static void
gdk_gl_texture_run (GdkGLTexture *self,
                    GFunc         func,
                    gpointer      data)
{
  InvokeData invoke = { self, 0, func, data };

  g_main_context_invoke (NULL, gdk_gl_texture_invoke_callback, &invoke);

  while (g_atomic_int_get (&invoke.spinlock) == 0);
}

typedef struct _Download Download;

struct _Download
{
  GdkMemoryFormat format;
  guchar *data;
  gsize stride;
};

static gboolean
gdk_gl_texture_find_format (gboolean         use_es,
                            GLint            gl_format,
                            GLint            gl_type,
                            GdkMemoryFormat *out_format)
{
  GdkMemoryFormat format;

  for (format = 0; format < GDK_MEMORY_N_FORMATS; format++)
    {
      GLenum q_internal_format, q_format, q_type;

      if (!gdk_memory_format_gl_format (format, use_es, &q_internal_format, &q_format, &q_type))
        continue;

      if (q_format != gl_format || q_type != gl_type)
        continue;

      *out_format = format;
      return TRUE;
    }

  return FALSE;
}

static inline void
gdk_gl_texture_do_download (gpointer texture_,
                            gpointer download_)
{
  gsize expected_stride;
  GdkGLTexture *self = texture_;
  GdkTexture *texture = texture_;
  Download *download = download_;
  GLenum gl_internal_format, gl_format, gl_type;

  expected_stride = texture->width * gdk_memory_format_bytes_per_pixel (download->format);

  if (download->stride == expected_stride &&
      !gdk_gl_context_get_use_es (self->context) && 
      gdk_memory_format_gl_format (download->format, TRUE, &gl_internal_format, &gl_format, &gl_type))
    {
      glGetTexImage (GL_TEXTURE_2D,
                     0,
                     gl_format,
                     gl_type,
                     download->data);
    }
  else
    {
      GdkMemoryFormat actual_format;
      GLint gl_read_format, gl_read_type;
      GLuint fbo;

      glGenFramebuffers (1, &fbo);
      glBindFramebuffer (GL_FRAMEBUFFER, fbo);
      glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self->id, 0);
      if (gdk_gl_context_check_version (self->context, 4, 3, 3, 1))
        {
          glGetFramebufferParameteriv (GL_FRAMEBUFFER, GL_IMPLEMENTATION_COLOR_READ_FORMAT, &gl_read_format);
          glGetFramebufferParameteriv (GL_FRAMEBUFFER, GL_IMPLEMENTATION_COLOR_READ_TYPE, &gl_read_type);
          if (!gdk_gl_texture_find_format (gdk_gl_context_get_use_es (self->context), gl_read_format, gl_read_type, &actual_format))
            actual_format = GDK_MEMORY_R8G8B8A8_PREMULTIPLIED; /* pray */
        }
      else
        {
          gl_read_format = GL_RGBA;
          gl_read_type = GL_UNSIGNED_BYTE;
          actual_format = GDK_MEMORY_R8G8B8A8_PREMULTIPLIED;
        }

      if (download->format == actual_format &&
          (download->stride == expected_stride))
        {
          glReadPixels (0, 0,
                        texture->width, texture->height, 
                        gl_read_format,
                        gl_read_type,
                        download->data);
        }
      else
        {
          gsize actual_bpp = gdk_memory_format_bytes_per_pixel (actual_format);
          guchar *pixels = g_malloc_n (texture->width * actual_bpp, texture->height);

          glReadPixels (0, 0,
                        texture->width, texture->height, 
                        gl_read_format,
                        gl_read_type,
                        pixels);

          gdk_memory_convert (download->data,
                              download->stride,
                              download->format,
                              pixels,
                              texture->width * actual_bpp,
                              actual_format,
                              texture->width,
                              texture->height);

          g_free (pixels);
        }
      glBindFramebuffer (GL_FRAMEBUFFER, 0);
      glDeleteFramebuffers (1, &fbo);
    }
}

static void
gdk_gl_texture_download (GdkTexture      *texture,
                         GdkMemoryFormat  format,
                         guchar          *data,
                         gsize            stride)
{
  GdkGLTexture *self = GDK_GL_TEXTURE (texture);
  Download download;

  if (self->saved)
    {
      gdk_texture_do_download (self->saved, format, data, stride);
      return;
    }

  download.format = format;
  download.data = data;
  download.stride = stride;

  gdk_gl_texture_run (self, gdk_gl_texture_do_download, &download);
}

static void
gdk_gl_texture_class_init (GdkGLTextureClass *klass)
{
  GdkTextureClass *texture_class = GDK_TEXTURE_CLASS (klass);
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  texture_class->download = gdk_gl_texture_download;

  gobject_class->dispose = gdk_gl_texture_dispose;
}

static void
gdk_gl_texture_init (GdkGLTexture *self)
{
}

GdkGLContext *
gdk_gl_texture_get_context (GdkGLTexture *self)
{
  return self->context;
}

guint
gdk_gl_texture_get_id (GdkGLTexture *self)
{
  return self->id;
}

/**
 * gdk_gl_texture_release:
 * @self: a `GdkTexture` wrapping a GL texture
 *
 * Releases the GL resources held by a `GdkGLTexture`.
 *
 * The texture contents are still available via the
 * [method@Gdk.Texture.download] function, after this
 * function has been called.
 */
void
gdk_gl_texture_release (GdkGLTexture *self)
{
  GdkTexture *texture;

  g_return_if_fail (GDK_IS_GL_TEXTURE (self));
  g_return_if_fail (self->saved == NULL);

  texture = GDK_TEXTURE (self);
  self->saved = GDK_TEXTURE (gdk_memory_texture_from_texture (texture,
                                                              gdk_texture_get_format (texture)));

  if (self->destroy)
    {
      self->destroy (self->data);
      self->destroy = NULL;
      self->data = NULL;
    }

  g_clear_object (&self->context);
  self->id = 0;
}

static void
gdk_gl_texture_determine_format (GdkGLTexture *self)
{
  GdkTexture *texture = GDK_TEXTURE (self);
  GLint active_texture;
  GLint internal_format;

  /* Abort if somebody else is GL-ing here... */
  if (self->context != gdk_gl_context_get_current () ||
      /* ... or glGetTexLevelParameter() isn't supported */
      !gdk_gl_context_check_version (self->context, 0, 0, 3, 1))
    {
      texture->format = GDK_MEMORY_DEFAULT;
      return;
    }

  /* We need to be careful about modifying the GL context, as this is not
   * expected during construction */
  glGetIntegerv (GL_TEXTURE_BINDING_2D, &active_texture);
  glBindTexture (GL_TEXTURE_2D, self->id);

  glGetTexLevelParameteriv (GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internal_format);

  switch (internal_format)
  {
    case GL_RGB8:
      texture->format = GDK_MEMORY_R8G8B8;
      break;

    case GL_RGBA8:
      texture->format = GDK_MEMORY_R8G8B8A8_PREMULTIPLIED;
      break;

    case GL_RGB16:
      texture->format = GDK_MEMORY_R16G16B16;
      break;

    case GL_RGBA16:
      texture->format = GDK_MEMORY_R16G16B16A16_PREMULTIPLIED;
      break;

    case GL_RGB16F:
      texture->format = GDK_MEMORY_R16G16B16_FLOAT;
      break;

    case GL_RGBA16F:
      texture->format = GDK_MEMORY_R16G16B16A16_FLOAT_PREMULTIPLIED;
      break;

    case GL_RGB32F:
      texture->format = GDK_MEMORY_R32G32B32_FLOAT;
      break;

    case GL_RGBA32F:
      texture->format = GDK_MEMORY_R32G32B32A32_FLOAT_PREMULTIPLIED;
      break;

    default:
      g_warning ("Texture in unexpected format 0x%X (%d). File a bug about adding it to GTK", internal_format, internal_format);
      /* fallback to the dumbest possible format
       * so that even age old GLES can do it */
      texture->format = GDK_MEMORY_R8G8B8A8_PREMULTIPLIED;
      break;
  }

  glBindTexture (GL_TEXTURE_2D, active_texture);
}

/**
 * gdk_gl_texture_new:
 * @context: a `GdkGLContext`
 * @id: the ID of a texture that was created with @context
 * @width: the nominal width of the texture
 * @height: the nominal height of the texture
 * @destroy: a destroy notify that will be called when the GL resources
 *   are released
 * @data: data that gets passed to @destroy
 *
 * Creates a new texture for an existing GL texture.
 *
 * Note that the GL texture must not be modified until @destroy is called,
 * which will happen when the GdkTexture object is finalized, or due to
 * an explicit call of [method@Gdk.GLTexture.release].
 *
 * Return value: (transfer full) (type GdkGLTexture): A newly-created
 *   `GdkTexture`
 */
GdkTexture *
gdk_gl_texture_new (GdkGLContext   *context,
                    guint           id,
                    int             width,
                    int             height,
                    GDestroyNotify  destroy,
                    gpointer        data)
{
  GdkGLTexture *self;

  g_return_val_if_fail (GDK_IS_GL_CONTEXT (context), NULL);
  g_return_val_if_fail (id != 0, NULL);
  g_return_val_if_fail (width > 0, NULL);
  g_return_val_if_fail (height > 0, NULL);

  self = g_object_new (GDK_TYPE_GL_TEXTURE,
                       "width", width,
                       "height", height,
                       NULL);

  self->context = g_object_ref (context);
  self->id = id;
  self->destroy = destroy;
  self->data = data;

  gdk_gl_texture_determine_format (self);

  return GDK_TEXTURE (self);
}