/* 
 * GStreamer
 * Copyright (C) 2013 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:gstglframebuffer
 * @short_description: OpenGL framebuffer abstraction
 * @title: GstGLFramebuffer
 * @see_also: #GstGLBaseMemory, #GstGLMemory, #GstGLContext
 *
 * A #GstGLFramebuffer represents and holds an OpenGL framebuffer object with
 * it's associated attachments.
 *
 * A #GstGLFramebuffer can be created with gst_gl_framebuffer_new() or
 * gst_gl_framebuffer_new_with_default_depth() and bound with
 * gst_gl_framebuffer_bind().  Other resources can be bound with
 * gst_gl_framebuffer_attach()
 *
 * Note: OpenGL framebuffers are not shareable resources so cannot be used
 * between multiple OpenGL contexts.
 *
 * Since: 1.10
 */

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

#include "gstglframebuffer.h"

#include "gstglcontext.h"
#include "gstglcontext_private.h"
#include "gstglfuncs.h"
#include "gstglmemory.h"
#include "gstglrenderbuffer.h"

#ifndef GL_FRAMEBUFFER_UNDEFINED
#define GL_FRAMEBUFFER_UNDEFINED          0x8219
#endif
#ifndef GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT
#define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT 0x8CD6
#endif
#ifndef GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT
#define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 0x8CD7
#endif
#ifndef GL_FRAMEBUFFER_UNSUPPORTED
#define GL_FRAMEBUFFER_UNSUPPORTED        0x8CDD
#endif
#ifndef GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS
#define GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS 0x8CD9
#endif

#ifndef GL_DEPTH_STENCIL_ATTACHMENT
#define GL_DEPTH_STENCIL_ATTACHMENT 0x821A
#endif

#ifndef GL_READ_FRAMEBUFFER
#define GL_READ_FRAMEBUFFER 0x8CA8
#endif
#ifndef GL_DRAW_FRAMEBUFFER
#define GL_DRAW_FRAMEBUFFER 0x8CA9
#endif

GST_DEBUG_CATEGORY_STATIC (gst_gl_framebuffer_debug);
#define GST_CAT_DEFAULT gst_gl_framebuffer_debug

static void gst_gl_framebuffer_finalize (GObject * object);

struct _GstGLFramebufferPrivate
{
  guint effective_width;
  guint effective_height;
};

#define DEBUG_INIT \
  GST_DEBUG_CATEGORY_INIT (gst_gl_framebuffer_debug, "glframebuffer", 0, "GL Framebuffer");

G_DEFINE_TYPE_WITH_CODE (GstGLFramebuffer, gst_gl_framebuffer, GST_TYPE_OBJECT,
    G_ADD_PRIVATE (GstGLFramebuffer) DEBUG_INIT);

struct fbo_attachment
{
  guint attachment_point;
  GstGLBaseMemory *mem;
};

static void
_fbo_attachment_init (struct fbo_attachment *attach, guint point,
    GstGLBaseMemory * mem)
{
  attach->attachment_point = point;
  attach->mem = (GstGLBaseMemory *) gst_memory_ref (GST_MEMORY_CAST (mem));
}

static void
_fbo_attachment_unset (struct fbo_attachment *attach)
{
  if (!attach)
    return;

  if (attach->mem)
    gst_memory_unref (GST_MEMORY_CAST (attach->mem));
  attach->mem = NULL;
}

static void
gst_gl_framebuffer_class_init (GstGLFramebufferClass * klass)
{
  G_OBJECT_CLASS (klass)->finalize = gst_gl_framebuffer_finalize;
}

static void
gst_gl_framebuffer_init (GstGLFramebuffer * fb)
{
  fb->priv = gst_gl_framebuffer_get_instance_private (fb);

  fb->attachments =
      g_array_new (FALSE, FALSE, (sizeof (struct fbo_attachment)));
  g_array_set_clear_func (fb->attachments,
      (GDestroyNotify) _fbo_attachment_unset);
}

static void
_delete_fbo_gl (GstGLContext * context, GstGLFramebuffer * fb)
{
  const GstGLFuncs *gl = context->gl_vtable;

  if (fb->fbo_id)
    gl->DeleteFramebuffers (1, &fb->fbo_id);
  fb->fbo_id = 0;
}

static void
gst_gl_framebuffer_finalize (GObject * object)
{
  GstGLFramebuffer *fb = GST_GL_FRAMEBUFFER (object);

  if (fb->context) {
    if (fb->fbo_id)
      gst_gl_context_thread_add (fb->context,
          (GstGLContextThreadFunc) _delete_fbo_gl, fb);

    gst_object_unref (fb->context);
    fb->context = NULL;
  }

  if (fb->attachments)
    g_array_free (fb->attachments, TRUE);
  fb->attachments = NULL;

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

/**
 * gst_gl_framebuffer_new:
 * @context: a #GstGLContext
 *
 * This function will internally create an OpenGL framebuffer object and must
 * be called on @context's OpenGL thread.
 *
 * Returns: (transfer full): a new #GstGLFramebuffer
 *
 * Since: 1.10
 */
GstGLFramebuffer *
gst_gl_framebuffer_new (GstGLContext * context)
{
  GstGLFramebuffer *fb;
  const GstGLFuncs *gl;

  g_return_val_if_fail (GST_IS_GL_CONTEXT (context), NULL);
  g_return_val_if_fail (gst_gl_context_get_current () == context, NULL);

  gl = context->gl_vtable;

  if (!gl->GenFramebuffers) {
    GST_ERROR_OBJECT (context, "Framebuffers are not supported!");
    return NULL;
  }

  fb = g_object_new (GST_TYPE_GL_FRAMEBUFFER, NULL);
  fb->context = gst_object_ref (context);
  gl->GenFramebuffers (1, &fb->fbo_id);
  gst_object_ref_sink (fb);

  return fb;
}

/**
 * gst_gl_framebuffer_new_with_default_depth:
 * @context: a #GstGLContext
 * @width: width for the depth buffer
 * @height: for the depth buffer
 *
 * This function will internally create an OpenGL framebuffer object and must
 * be called on @context's OpenGL thread.
 *
 * Returns: a new #GstGLFramebuffer with a depth buffer of @width and @height
 *
 * Since: 1.10
 */
GstGLFramebuffer *
gst_gl_framebuffer_new_with_default_depth (GstGLContext * context, guint width,
    guint height)
{
  GstGLFramebuffer *fb = gst_gl_framebuffer_new (context);
  GstGLBaseMemoryAllocator *render_alloc;
  GstGLAllocationParams *params;
  GstGLBaseMemory *renderbuffer;
  guint attach_point, attach_type;

  if (!fb)
    return NULL;

  if (gst_gl_context_get_gl_api (fb->context) & (GST_GL_API_OPENGL |
          GST_GL_API_OPENGL3)) {
    attach_point = GL_DEPTH_STENCIL_ATTACHMENT;
    attach_type = GST_GL_DEPTH24_STENCIL8;
  } else if (gst_gl_context_get_gl_api (fb->context) & GST_GL_API_GLES2) {
    attach_point = GL_DEPTH_ATTACHMENT;
    attach_type = GST_GL_DEPTH_COMPONENT16;
  } else {
    g_assert_not_reached ();
    return NULL;
  }

  render_alloc = (GstGLBaseMemoryAllocator *)
      gst_allocator_find (GST_GL_RENDERBUFFER_ALLOCATOR_NAME);
  params = (GstGLAllocationParams *)
      gst_gl_renderbuffer_allocation_params_new (context, NULL, attach_type,
      width, height);

  renderbuffer = gst_gl_base_memory_alloc (render_alloc, params);
  gst_gl_allocation_params_free (params);
  gst_object_unref (render_alloc);

  gst_gl_framebuffer_bind (fb);
  gst_gl_framebuffer_attach (fb, attach_point, renderbuffer);
  gst_gl_context_clear_framebuffer (fb->context);
  gst_memory_unref (GST_MEMORY_CAST (renderbuffer));

  return fb;
}

/**
 * gst_gl_framebuffer_draw_to_texture:
 * @fb: a #GstGLFramebuffer
 * @mem: the #GstGLMemory to draw to
 * @func: (scope call): the function to run
 * @user_data: data to pass to @func
 *
 * Perform the steps necessary to have the output of a glDraw* command in
 * @func update the contents of @mem.
 *
 * Note: this function does not map @mem for writing with OpenGL and that must
 * be done manually by the caller using any of the mapping functions such as
 * gst_memory_map() with the map flags %GST_MAP_WRITE | %GST_MAP_GL.
 *
 * Must be called with the same OpenGL context current that @fb was created
 * with.
 *
 * Returns: the result of executing @func
 *
 * Since: 1.10
 */
gboolean
gst_gl_framebuffer_draw_to_texture (GstGLFramebuffer * fb, GstGLMemory * mem,
    GstGLFramebufferFunc func, gpointer user_data)
{
  const GstGLFuncs *gl;
  gboolean ret;

  g_return_val_if_fail (GST_IS_GL_FRAMEBUFFER (fb), FALSE);
  g_return_val_if_fail (gst_is_gl_memory (GST_MEMORY_CAST (mem)), FALSE);
  g_return_val_if_fail (gst_gl_context_get_current () == fb->context, FALSE);

  gl = fb->context->gl_vtable;

  GST_TRACE_OBJECT (fb, "drawing to texture %u, dimensions %ix%i", mem->tex_id,
      gst_gl_memory_get_texture_width (mem),
      gst_gl_memory_get_texture_height (mem));

  gst_gl_framebuffer_bind (fb);
  gst_gl_framebuffer_attach (fb, GL_COLOR_ATTACHMENT0, (GstGLBaseMemory *) mem);

  gl->Viewport (0, 0, fb->priv->effective_width, fb->priv->effective_height);
  if (gst_gl_context_get_gl_api (fb->context) & (GST_GL_API_OPENGL |
          GST_GL_API_OPENGL3))
    gl->DrawBuffer (GL_COLOR_ATTACHMENT0);

  ret = func (user_data);

  if (gst_gl_context_get_gl_api (fb->context) & (GST_GL_API_OPENGL |
          GST_GL_API_OPENGL3))
    gl->DrawBuffer (GL_COLOR_ATTACHMENT0);
  gst_gl_context_clear_framebuffer (fb->context);

  return ret;
}

/**
 * gst_gl_framebuffer_bind:
 * @fb: a #GstGLFramebuffer
 *
 * Bind @fb into the current thread
 *
 * Must be called with the same OpenGL context current that @fb was created
 * with.
 *
 * Since: 1.10
 */
void
gst_gl_framebuffer_bind (GstGLFramebuffer * fb)
{
  const GstGLFuncs *gl;

  g_return_if_fail (GST_IS_GL_FRAMEBUFFER (fb));
  g_return_if_fail (gst_gl_context_get_current () == fb->context);
  g_return_if_fail (fb->fbo_id != 0);

  gl = fb->context->gl_vtable;

  gl->BindFramebuffer (GL_FRAMEBUFFER, fb->fbo_id);
}

/**
 * gst_gl_context_clear_framebuffer:
 * @context: a #GstGLContext
 *
 * Unbind the current framebuffer
 *
 * Since: 1.10
 */
void
gst_gl_context_clear_framebuffer (GstGLContext * context)
{
  const GstGLFuncs *gl;

  g_return_if_fail (GST_IS_GL_CONTEXT (context));

  gl = context->gl_vtable;

  gl->BindFramebuffer (GL_FRAMEBUFFER, 0);
}

static void
_update_effective_dimensions (GstGLFramebuffer * fb)
{
  int i;
  guint min_width = -1, min_height = -1;

  /* remove the previous attachment */
  for (i = 0; i < fb->attachments->len; i++) {
    struct fbo_attachment *attach;
    int width, height;

    attach = &g_array_index (fb->attachments, struct fbo_attachment, i);

    if (gst_is_gl_memory (GST_MEMORY_CAST (attach->mem))) {
      GstGLMemory *mem = (GstGLMemory *) attach->mem;

      width = gst_gl_memory_get_texture_width (mem);
      height = gst_gl_memory_get_texture_height (mem);
    } else if (gst_is_gl_renderbuffer (GST_MEMORY_CAST (attach->mem))) {
      GstGLRenderbuffer *mem = (GstGLRenderbuffer *) attach->mem;

      width = mem->width;
      height = mem->height;
    } else {
      g_assert_not_reached ();
    }

    if (width < min_width)
      min_width = width;
    if (height < min_height)
      min_height = height;
  }

  fb->priv->effective_width = min_width;
  fb->priv->effective_height = min_height;
}

static gboolean
_is_valid_attachment_point (guint attachment_point)
{
  /* all 31 possible color attachments */
  if (attachment_point >= 0x8CE0 && attachment_point <= 0x8CFF)
    return TRUE;

  /* depth-stencil attachment */
  if (attachment_point == 0x821A)
    return TRUE;

  /* depth attachment */
  if (attachment_point == 0x8D00)
    return TRUE;

  /* stencil attachment */
  if (attachment_point == 0x8D20)
    return TRUE;

  return FALSE;
}

static void
_attach_gl_memory (GstGLFramebuffer * fb, guint attachment_point,
    GstGLMemory * mem)
{
  struct fbo_attachment attach;
  const GstGLFuncs *gl = fb->context->gl_vtable;
  guint gl_target = gst_gl_texture_target_to_gl (mem->tex_target);

  gst_gl_framebuffer_bind (fb);

  gl->FramebufferTexture2D (GL_FRAMEBUFFER, attachment_point, gl_target,
      mem->tex_id, 0);

  _fbo_attachment_init (&attach, attachment_point, (GstGLBaseMemory *) mem);
  fb->attachments = g_array_append_val (fb->attachments, attach);
}

static void
_attach_renderbuffer (GstGLFramebuffer * fb, guint attachment_point,
    GstGLRenderbuffer * rb)
{
  struct fbo_attachment attach;
  const GstGLFuncs *gl = fb->context->gl_vtable;

  gst_gl_framebuffer_bind (fb);
  gl->BindRenderbuffer (GL_RENDERBUFFER, rb->renderbuffer_id);

  gl->FramebufferRenderbuffer (GL_FRAMEBUFFER, attachment_point,
      GL_RENDERBUFFER, rb->renderbuffer_id);

  _fbo_attachment_init (&attach, attachment_point, (GstGLBaseMemory *) rb);
  fb->attachments = g_array_append_val (fb->attachments, attach);
}

/**
 * gst_gl_framebuffer_attach:
 * @fb: a #GstGLFramebuffer
 * @attachment_point: the OpenGL attachment point to bind @mem to
 * @mem: the memory object to bind to @attachment_point
 *
 * attach @mem to @attachment_point
 *
 * Must be called with the same OpenGL context current that @fb was created
 * with.
 *
 * Since: 1.10
 */
void
gst_gl_framebuffer_attach (GstGLFramebuffer * fb, guint attachment_point,
    GstGLBaseMemory * mem)
{
  int i;

  g_return_if_fail (GST_IS_GL_FRAMEBUFFER (fb));
  g_return_if_fail (gst_gl_context_get_current () == fb->context);
  g_return_if_fail (_is_valid_attachment_point (attachment_point));

  /* remove the previous attachment */
  for (i = 0; i < fb->attachments->len; i++) {
    struct fbo_attachment *attach;

    attach = &g_array_index (fb->attachments, struct fbo_attachment, i);

    if (attach->attachment_point == attachment_point) {
      g_array_remove_index_fast (fb->attachments, i);
      break;
    }
  }

  if (gst_is_gl_memory (GST_MEMORY_CAST (mem))) {
    _attach_gl_memory (fb, attachment_point, (GstGLMemory *) mem);
  } else if (gst_is_gl_renderbuffer (GST_MEMORY_CAST (mem))) {
    _attach_renderbuffer (fb, attachment_point, (GstGLRenderbuffer *) mem);
  } else {
    g_assert_not_reached ();
    return;
  }

  _update_effective_dimensions (fb);
}

/**
 * gst_gl_framebuffer_get_effective_dimensions:
 * @fb: a #GstGLFramebuffer
 * @width: (out) (allow-none): output width
 * @height: (out) (allow-none): output height
 *
 * Retrieve the effective dimensions from the current attachments attached to
 * @fb.
 *
 * Since: 1.10
 */
void
gst_gl_framebuffer_get_effective_dimensions (GstGLFramebuffer * fb,
    guint * width, guint * height)
{
  g_return_if_fail (GST_IS_GL_FRAMEBUFFER (fb));

  if (width)
    *width = fb->priv->effective_width;
  if (height)
    *height = fb->priv->effective_height;
}

/**
 * gst_gl_context_check_framebuffer_status:
 * @context: a #GstGLContext
 * @fbo_target: the GL value of the framebuffer target, GL_FRAMEBUFFER,
 *              GL_READ_FRAMEBUFFER, GL_DRAW_FRAMEBUFFER
 *
 * Must be called with @context current.
 *
 * Returns: whether whether the current framebuffer is complete
 *
 * Since: 1.10
 */
gboolean
gst_gl_context_check_framebuffer_status (GstGLContext * context,
    guint fbo_target)
{
  GLenum ret;

  g_return_val_if_fail (GST_IS_GL_CONTEXT (context), FALSE);
  g_return_val_if_fail (gst_gl_context_get_current () == context, FALSE);

  if (fbo_target != GL_FRAMEBUFFER && fbo_target != GL_READ_FRAMEBUFFER
      && fbo_target != GL_DRAW_FRAMEBUFFER) {
    GST_ERROR_OBJECT (context, "fbo target is invalid");
    return FALSE;
  }

  /* Don't do expensive framebuffer checks when debugging is disabled */
  if (!_gst_gl_context_debug_is_enabled (context))
    return TRUE;

  ret = context->gl_vtable->CheckFramebufferStatus (fbo_target);
  switch (ret) {
    case GL_FRAMEBUFFER_COMPLETE:
      return TRUE;
      break;
    case GL_FRAMEBUFFER_UNSUPPORTED:
      GST_WARNING_OBJECT (context, "GL_FRAMEBUFFER_UNSUPPORTED");
      break;
    case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT:
      GST_WARNING_OBJECT (context, "GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT");
      break;
    case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT:
      GST_WARNING_OBJECT (context,
          "GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT");
      break;
    case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS:
      GST_WARNING_OBJECT (context, "GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS");
      break;
#if GST_GL_HAVE_OPENGL
    case GL_FRAMEBUFFER_UNDEFINED:
      GST_WARNING_OBJECT (context, "GL_FRAMEBUFFER_UNDEFINED");
      break;
#endif
    default:
      GST_WARNING_OBJECT (context, "Unknown FBO error: %d (0x%x)", ret, ret);
      break;
  }

  return FALSE;
}

/**
 * gst_gl_framebuffer_get_id:
 * @fb: a #GstGLFramebuffer
 *
 * Returns: the OpenGL id for @fb
 *
 * Since: 1.10
 */
guint
gst_gl_framebuffer_get_id (GstGLFramebuffer * fb)
{
  g_return_val_if_fail (GST_IS_GL_FRAMEBUFFER (fb), 0);

  return fb->fbo_id;
}
