/*
 * Cogl
 *
 * An object oriented GL/GLES Abstraction/Utility Layer
 *
 * Copyright (C) 2007,2008,2009 Intel Corporation.
 *
 * 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, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

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

#include "cogl.h"
#include "cogl-internal.h"
#include "cogl-context.h"
#include "cogl-handle.h"
#include "cogl-util.h"
#include "cogl-texture-private.h"
#include "cogl-draw-buffer-private.h"
#include "cogl-clip-stack.h"

#ifdef HAVE_COGL_GLES2

#include "../gles/cogl-gles2-wrapper.h"

#else

#define glGenRenderbuffers                ctx->drv.pf_glGenRenderbuffers
#define glDeleteRenderbuffers             ctx->drv.pf_glDeleteRenderbuffers
#define glBindRenderbuffer                ctx->drv.pf_glBindRenderbuffer
#define glRenderbufferStorage             ctx->drv.pf_glRenderbufferStorage
#define glGenFramebuffers                 ctx->drv.pf_glGenFramebuffers
#define glBindFramebuffer                 ctx->drv.pf_glBindFramebuffer
#define glFramebufferTexture2D            ctx->drv.pf_glFramebufferTexture2D
#define glFramebufferRenderbuffer         ctx->drv.pf_glFramebufferRenderbuffer
#define glCheckFramebufferStatus          ctx->drv.pf_glCheckFramebufferStatus
#define glDeleteFramebuffers              ctx->drv.pf_glDeleteFramebuffers

#endif

#ifndef GL_FRAMEBUFFER
#define GL_FRAMEBUFFER		0x8D40
#endif
#ifndef GL_RENDERBUFFER
#define GL_RENDERBUFFER		0x8D41
#endif
#ifndef GL_STENCIL_ATTACHMENT
#define GL_STENCIL_ATTACHMENT	0x8D00
#endif
#ifndef GL_COLOR_ATTACHMENT0
#define GL_COLOR_ATTACHMENT0	0x8CE0
#endif
#ifndef GL_FRAMEBUFFER_COMPLETE
#define GL_FRAMEBUFFER_COMPLETE 0x8CD5
#endif
#ifndef GL_STENCIL_INDEX8
#define GL_STENCIL_INDEX8       0x8D48
#endif

static void _cogl_draw_buffer_free (CoglDrawBuffer *draw_buffer);
static void _cogl_onscreen_free (CoglOnscreen *onscreen);
static void _cogl_offscreen_free (CoglOffscreen *offscreen);

COGL_HANDLE_DEFINE (Onscreen, onscreen);
COGL_HANDLE_DEFINE (Offscreen, offscreen);

/* XXX:
 * The CoglHandle macros don't support any form of inheritance, so for
 * now we implement the CoglHandle support for the CoglDrawBuffer
 * abstract class manually.
 */

gboolean
cogl_is_draw_buffer (CoglHandle handle)
{
  CoglHandleObject *obj = (CoglHandleObject *)handle;

  if (handle == COGL_INVALID_HANDLE)
    return FALSE;

  return obj->klass->type == _cogl_handle_onscreen_get_type ()
    || obj->klass->type == _cogl_handle_offscreen_get_type ();
}

static void
_cogl_draw_buffer_init (CoglDrawBuffer *draw_buffer,
                        CoglDrawBufferType type,
                        int width,
                        int height)
{
  draw_buffer->type             = type;
  draw_buffer->width            = width;
  draw_buffer->height           = height;
  draw_buffer->viewport_x       = 0;
  draw_buffer->viewport_y       = 0;
  draw_buffer->viewport_width   = width;
  draw_buffer->viewport_height  = height;

  draw_buffer->modelview_stack  = _cogl_matrix_stack_new ();
  draw_buffer->projection_stack = _cogl_matrix_stack_new ();

  /* Initialise the clip stack */
  _cogl_clip_stack_state_init (&draw_buffer->clip_state);
}

void
_cogl_draw_buffer_free (CoglDrawBuffer *draw_buffer)
{
  _cogl_clip_stack_state_destroy (&draw_buffer->clip_state);

  _cogl_matrix_stack_destroy (draw_buffer->modelview_stack);
  draw_buffer->modelview_stack = NULL;

  _cogl_matrix_stack_destroy (draw_buffer->projection_stack);
  draw_buffer->projection_stack = NULL;
}

int
_cogl_draw_buffer_get_width (CoglHandle handle)
{
  CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (handle);
  return draw_buffer->width;
}

int
_cogl_draw_buffer_get_height (CoglHandle handle)
{
  CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (handle);
  return draw_buffer->height;
}

CoglClipStackState *
_cogl_draw_buffer_get_clip_state (CoglHandle handle)
{
  CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (handle);

  return &draw_buffer->clip_state;
}

void
_cogl_draw_buffer_set_viewport (CoglHandle handle,
                                int x,
                                int y,
                                int width,
                                int height)
{
  CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (handle);

  _COGL_GET_CONTEXT (ctx, NO_RETVAL);

  if (draw_buffer->viewport_x == x &&
      draw_buffer->viewport_y == y &&
      draw_buffer->viewport_width == width &&
      draw_buffer->viewport_height == height)
    return;

  _cogl_journal_flush ();

  draw_buffer->viewport_x = x;
  draw_buffer->viewport_y = y;
  draw_buffer->viewport_width = width;
  draw_buffer->viewport_height = height;

  if (_cogl_get_draw_buffer () == draw_buffer)
    ctx->dirty_gl_viewport = TRUE;
}

int
_cogl_draw_buffer_get_viewport_x (CoglHandle handle)
{
  CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (handle);
  return draw_buffer->viewport_x;
}

int
_cogl_draw_buffer_get_viewport_y (CoglHandle handle)
{
  CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (handle);
  return draw_buffer->viewport_y;
}

int
_cogl_draw_buffer_get_viewport_width (CoglHandle handle)
{
  CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (handle);
  return draw_buffer->viewport_width;
}

int
_cogl_draw_buffer_get_viewport_height (CoglHandle handle)
{
  CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (handle);
  return draw_buffer->viewport_height;
}

void
_cogl_draw_buffer_get_viewport4fv (CoglHandle handle, int *viewport)
{
  CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (handle);
  viewport[0] = draw_buffer->viewport_x;
  viewport[1] = draw_buffer->viewport_y;
  viewport[2] = draw_buffer->viewport_width;
  viewport[3] = draw_buffer->viewport_height;
}

CoglMatrixStack *
_cogl_draw_buffer_get_modelview_stack (CoglHandle handle)
{
  CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (handle);
  return draw_buffer->modelview_stack;
}

CoglMatrixStack *
_cogl_draw_buffer_get_projection_stack (CoglHandle handle)
{
  CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (handle);
  return draw_buffer->projection_stack;
}

CoglHandle
cogl_offscreen_new_to_texture (CoglHandle texhandle)
{
  CoglOffscreen    *offscreen;
  int               width;
  int               height;
  GLuint            tex_gl_handle;
  GLenum            tex_gl_target;
  GLuint            fbo_gl_handle;
  GLuint            gl_stencil_handle;
  GLenum            status;

  _COGL_GET_CONTEXT (ctx, COGL_INVALID_HANDLE);

  if (!cogl_features_available (COGL_FEATURE_OFFSCREEN))
    return COGL_INVALID_HANDLE;

  /* Make texhandle is a valid texture object */
  if (!cogl_is_texture (texhandle))
    return COGL_INVALID_HANDLE;

  /* The texture must not be sliced */
  if (cogl_texture_is_sliced (texhandle))
    return COGL_INVALID_HANDLE;

  /* Pick the single texture slice width, height and GL id */

  width = cogl_texture_get_width (texhandle);
  height = cogl_texture_get_height (texhandle);

  if (!cogl_texture_get_gl_texture (texhandle, &tex_gl_handle, &tex_gl_target))
    return COGL_INVALID_HANDLE;

  if (tex_gl_target != GL_TEXTURE_2D)
    return COGL_INVALID_HANDLE;

  /* Create a renderbuffer for stenciling */
  GE (glGenRenderbuffers (1, &gl_stencil_handle));
  GE (glBindRenderbuffer (GL_RENDERBUFFER, gl_stencil_handle));
  GE (glRenderbufferStorage (GL_RENDERBUFFER, GL_STENCIL_INDEX8,
			     cogl_texture_get_width (texhandle),
			     cogl_texture_get_height (texhandle)));
  GE (glBindRenderbuffer (GL_RENDERBUFFER, 0));

  /* We are about to generate and bind a new fbo, so when next flushing the
   * journal, we will need to rebind the current draw buffer... */
  ctx->dirty_bound_framebuffer = 1;

  /* Generate framebuffer */
  glGenFramebuffers (1, &fbo_gl_handle);
  GE (glBindFramebuffer (GL_FRAMEBUFFER, fbo_gl_handle));
  GE (glFramebufferTexture2D (GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
			      tex_gl_target, tex_gl_handle, 0));
  GE (glFramebufferRenderbuffer (GL_FRAMEBUFFER,
				 GL_STENCIL_ATTACHMENT,
				 GL_RENDERBUFFER, gl_stencil_handle));

  /* XXX: The framebuffer_object spec isn't clear in defining whether attaching
   * a texture as a renderbuffer with mipmap filtering enabled while the
   * mipmaps have not been uploaded should result in an incomplete framebuffer
   * object. (different drivers make different decisions)
   *
   * To avoid an error with drivers that do consider this a problem we
   * explicitly set non mipmapped filters here. These will later be reset when
   * the texture is actually used for rendering according to the filters set on
   * the corresponding CoglMaterial.
   */
  _cogl_texture_set_filters (texhandle, GL_NEAREST, GL_NEAREST);

  /* Make sure it's complete */
  status = glCheckFramebufferStatus (GL_FRAMEBUFFER);

  if (status != GL_FRAMEBUFFER_COMPLETE)
    {
      /* Stencil renderbuffers aren't always supported. Try again
	 without the stencil buffer */
      GE (glFramebufferRenderbuffer (GL_FRAMEBUFFER,
				     GL_STENCIL_ATTACHMENT,
				     GL_RENDERBUFFER,
				     0));
      GE (glDeleteRenderbuffers (1, &gl_stencil_handle));
      gl_stencil_handle = 0;

      status = glCheckFramebufferStatus (GL_FRAMEBUFFER);

      if (status != GL_FRAMEBUFFER_COMPLETE)
	{
	  /* Still failing, so give up */
	  GE (glDeleteFramebuffers (1, &fbo_gl_handle));
	  GE (glBindFramebuffer (GL_FRAMEBUFFER, 0));
	  return COGL_INVALID_HANDLE;
	}
    }

  offscreen                     = g_new0 (CoglOffscreen, 1);

  _cogl_draw_buffer_init (COGL_DRAW_BUFFER (offscreen),
                          COGL_DRAW_BUFFER_TYPE_OFFSCREEN,
                          width,
                          height);

  offscreen->fbo_handle         = fbo_gl_handle;
  offscreen->gl_stencil_handle  = gl_stencil_handle;

  /* XXX: Can we get a away with removing this? It wasn't documented, and most
   * users of the API are hopefully setting up the modelview from scratch
   * anyway */
#if 0
  cogl_matrix_translate (&draw_buffer->modelview, -1.0f, -1.0f, 0.0f);
  cogl_matrix_scale (&draw_buffer->modelview,
                     2.0f / draw_buffer->width, 2.0f / draw_buffer->height, 1.0f);
#endif

  return _cogl_offscreen_handle_new (offscreen);
}

static void
_cogl_offscreen_free (CoglOffscreen *offscreen)
{
  _COGL_GET_CONTEXT (ctx, NO_RETVAL);

  /* Chain up to parent */
  _cogl_draw_buffer_free (COGL_DRAW_BUFFER (offscreen));

  if (offscreen->gl_stencil_handle)
    GE (glDeleteRenderbuffers (1, &offscreen->gl_stencil_handle));
  GE (glDeleteFramebuffers (1, &offscreen->fbo_handle));
  g_free (offscreen);
}

CoglHandle
_cogl_onscreen_new (void)
{
  CoglOnscreen *onscreen;

  /* XXX: Until we have full winsys support in Cogl then we can't fully
   * implement CoglOnscreen draw buffers, since we can't, e.g. keep track of
   * the window size. */

  onscreen = g_new0 (CoglOnscreen, 1);
  _cogl_draw_buffer_init (COGL_DRAW_BUFFER (onscreen),
                          COGL_DRAW_BUFFER_TYPE_ONSCREEN,
                          0xdeadbeef, /* width */
                          0xdeadbeef); /* height */

  return _cogl_onscreen_handle_new (onscreen);
}

static void
_cogl_onscreen_free (CoglOnscreen *onscreen)
{
  _COGL_GET_CONTEXT (ctx, NO_RETVAL);

  /* Chain up to parent */
  _cogl_draw_buffer_free (COGL_DRAW_BUFFER (onscreen));

  g_free (onscreen);
}

void
_cogl_onscreen_clutter_backend_set_size (int width, int height)
{
  CoglDrawBuffer *draw_buffer;

  _COGL_GET_CONTEXT (ctx, NO_RETVAL);

  draw_buffer = COGL_DRAW_BUFFER (ctx->window_buffer);

  if (draw_buffer->width == width && draw_buffer->height == height)
    return;

  draw_buffer->width = width;
  draw_buffer->height = height;

  /* We'll need to recalculate the GL viewport state derived
   * from the Cogl viewport */
  ctx->dirty_gl_viewport = 1;
}

GSList *
_cogl_create_draw_buffer_stack (void)
{
  GSList *stack = NULL;
  CoglDrawBufferStackEntry *entry;

  entry = g_slice_new0 (CoglDrawBufferStackEntry);
  entry->target = COGL_WINDOW_BUFFER;
  entry->draw_buffer = COGL_INVALID_HANDLE;

  return g_slist_prepend (stack, entry);
}

void
_cogl_free_draw_buffer_stack (GSList *stack)
{
  GSList *l;

  for (l = stack; l != NULL; l = l->next)
    {
      CoglDrawBufferStackEntry *entry = l->data;
      CoglDrawBuffer *draw_buffer = COGL_DRAW_BUFFER (entry->draw_buffer);
      if (draw_buffer->type == COGL_DRAW_BUFFER_TYPE_OFFSCREEN)
        _cogl_offscreen_free (COGL_OFFSCREEN (draw_buffer));
      else
        _cogl_onscreen_free (COGL_ONSCREEN (draw_buffer));
    }
  g_slist_free (stack);
}

/* XXX: The target argument is redundant; when we break API, we should
 * remove it! */
void
cogl_set_draw_buffer (CoglBufferTarget target, CoglHandle handle)
{
  CoglDrawBuffer *draw_buffer = NULL;
  CoglDrawBufferStackEntry *entry;

  _COGL_GET_CONTEXT (ctx, NO_RETVAL);

  _cogl_journal_flush ();

  g_assert (ctx->draw_buffer_stack != NULL);
  entry = ctx->draw_buffer_stack->data;

  if (target == COGL_WINDOW_BUFFER)
    handle = ctx->window_buffer;
  else if (!cogl_is_draw_buffer (handle))
    return;

  draw_buffer = COGL_DRAW_BUFFER (handle);

  if (entry->draw_buffer != draw_buffer)
    {
      entry->target = target;

      ctx->dirty_bound_framebuffer = 1;
      ctx->dirty_gl_viewport = 1;

      if (draw_buffer != COGL_INVALID_HANDLE)
        cogl_handle_ref (draw_buffer);
      if (entry->draw_buffer != COGL_INVALID_HANDLE)
        cogl_handle_unref (entry->draw_buffer);
      entry->draw_buffer = draw_buffer;

      /* We've effectively just switched the current modelview and
       * projection matrix stacks and clip state so we need to dirty
       * them to ensure they get flushed for the next batch of geometry
       * we flush */
      _cogl_matrix_stack_dirty (draw_buffer->modelview_stack);
      _cogl_matrix_stack_dirty (draw_buffer->projection_stack);
      _cogl_clip_stack_state_dirty (&draw_buffer->clip_state);
    }
}

CoglHandle
_cogl_get_draw_buffer (void)
{
  CoglDrawBufferStackEntry *entry;

  _COGL_GET_CONTEXT (ctx, NULL);

  g_assert (ctx->draw_buffer_stack);
  entry = ctx->draw_buffer_stack->data;

  return entry->draw_buffer;
}

void
cogl_push_draw_buffer (void)
{
  CoglDrawBufferStackEntry *old;
  CoglDrawBufferStackEntry *entry;

  _COGL_GET_CONTEXT (ctx, NO_RETVAL);

  entry = g_slice_new0 (CoglDrawBufferStackEntry);

  g_assert (ctx->draw_buffer_stack);

  old = ctx->draw_buffer_stack->data;
  *entry = *old;

  cogl_handle_ref (entry->draw_buffer);

  ctx->draw_buffer_stack =
    g_slist_prepend (ctx->draw_buffer_stack, entry);
}

void
cogl_pop_draw_buffer (void)
{
  CoglDrawBufferStackEntry *to_pop;
  CoglDrawBufferStackEntry *to_restore;

  _COGL_GET_CONTEXT (ctx, NO_RETVAL);

  g_assert (ctx->draw_buffer_stack != NULL);

  to_pop = ctx->draw_buffer_stack->data;
  to_restore = ctx->draw_buffer_stack->next->data;

  cogl_set_draw_buffer (to_restore->target, to_restore->draw_buffer);

  cogl_handle_unref (to_pop->draw_buffer);
  ctx->draw_buffer_stack =
    g_slist_remove_link (ctx->draw_buffer_stack,
                         ctx->draw_buffer_stack);
  g_slice_free (CoglDrawBufferStackEntry, to_pop);
}

void
_cogl_draw_buffer_flush_state (CoglHandle handle,
                               CoglDrawBufferFlushFlags flags)
{
  CoglDrawBuffer *draw_buffer;

  _COGL_GET_CONTEXT (ctx, NO_RETVAL);

  draw_buffer = COGL_DRAW_BUFFER (handle);

  if (cogl_features_available (COGL_FEATURE_OFFSCREEN) &&
      ctx->dirty_bound_framebuffer)
    {
      if (draw_buffer->type == COGL_DRAW_BUFFER_TYPE_OFFSCREEN)
        {
          GE (glBindFramebuffer (GL_FRAMEBUFFER,
                                 COGL_OFFSCREEN (draw_buffer)->fbo_handle));
        }
      else
        GE (glBindFramebuffer (GL_FRAMEBUFFER, 0));
      ctx->dirty_bound_framebuffer = FALSE;
    }

  if (ctx->dirty_gl_viewport)
    {
      int gl_viewport_y;

      /* Convert the Cogl viewport y offset to an OpenGL viewport y offset
       * NB: OpenGL defines its window and viewport origins to be bottom
       * left, while Cogl defines them to be top left.
       * NB: We render upside down to offscreen draw buffers so we don't
       * need to convert the y offset in this case. */
      if (cogl_is_offscreen (draw_buffer))
        gl_viewport_y = draw_buffer->viewport_y;
      else
        gl_viewport_y = draw_buffer->height -
          (draw_buffer->viewport_y + draw_buffer->viewport_height);

      GE (glViewport (draw_buffer->viewport_x,
                      gl_viewport_y,
                      draw_buffer->viewport_width,
                      draw_buffer->viewport_height));
      ctx->dirty_gl_viewport = FALSE;
    }

  /* XXX: Flushing clip state may trash the modelview and projection
   * matrices so we must do it before flushing the matrices...
   */
  _cogl_flush_clip_state (&draw_buffer->clip_state);

  if (!(flags & COGL_DRAW_BUFFER_FLUSH_SKIP_MODELVIEW))
    _cogl_matrix_stack_flush_to_gl (draw_buffer->modelview_stack,
                                    COGL_MATRIX_MODELVIEW);

  _cogl_matrix_stack_flush_to_gl (draw_buffer->projection_stack,
                                  COGL_MATRIX_PROJECTION);
}