/* GDK - The GIMP Drawing Kit
 *
 * gdkdrawcontext.c: base class for rendering system support
 *
 * Copyright © 2016  Benjamin Otte
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "gdkdrawcontextprivate.h"

#include "gdkinternals.h"
#include "gdkintl.h"
#include "gdkprofilerprivate.h"

/**
 * SECTION:gdkdrawcontext
 * @Title: GdkDrawContext
 * @Short_description: Base class for draw contexts
 *
 * #GdkDrawContext is the base object used by contexts implementing different
 * rendering methods, such as #GdkGLContext or #GdkVulkanContext. It provides
 * shared functionality between those contexts.
 *
 * You will always interact with one of those s.ubclasses.
 *
 * A GdkDrawContext is always associated with a single toplevel surface.
 */

/**
 * GdkDrawContext:
 *
 * The GdkDrawContext struct contains only private fields and should not
 * be accessed directly.
 */

typedef struct _GdkDrawContextPrivate GdkDrawContextPrivate;

struct _GdkDrawContextPrivate {
  GdkSurface *surface;

  cairo_region_t *frame_region;
};

enum {
  PROP_0,

  PROP_DISPLAY,
  PROP_SURFACE,

  LAST_PROP
};

static GParamSpec *pspecs[LAST_PROP] = { NULL, };

G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GdkDrawContext, gdk_draw_context, G_TYPE_OBJECT)

static void
gdk_draw_context_default_surface_resized (GdkDrawContext *context)
{
}

static void
gdk_draw_context_dispose (GObject *gobject)
{
  GdkDrawContext *context = GDK_DRAW_CONTEXT (gobject);
  GdkDrawContextPrivate *priv = gdk_draw_context_get_instance_private (context);

  if (priv->surface)
    {
      priv->surface->draw_contexts = g_slist_remove (priv->surface->draw_contexts, context);
      g_clear_object (&priv->surface);
    }

  G_OBJECT_CLASS (gdk_draw_context_parent_class)->dispose (gobject);
}

static void
gdk_draw_context_set_property (GObject      *gobject,
                               guint         prop_id,
                               const GValue *value,
                               GParamSpec   *pspec)
{
  GdkDrawContext *context = GDK_DRAW_CONTEXT (gobject);
  GdkDrawContextPrivate *priv = gdk_draw_context_get_instance_private (context);

  switch (prop_id)
    {
    case PROP_SURFACE:
      priv->surface = g_value_dup_object (value);
      g_assert (priv->surface != NULL);
      priv->surface->draw_contexts = g_slist_prepend (priv->surface->draw_contexts, context);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
    }
}

static void
gdk_draw_context_get_property (GObject    *gobject,
                               guint       prop_id,
                               GValue     *value,
                               GParamSpec *pspec)
{
  GdkDrawContext *context = GDK_DRAW_CONTEXT (gobject);
  GdkDrawContextPrivate *priv = gdk_draw_context_get_instance_private (context);

  switch (prop_id)
    {
    case PROP_DISPLAY:
      g_value_set_object (value, gdk_draw_context_get_display (context));
      break;

    case PROP_SURFACE:
      g_value_set_object (value, priv->surface);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
    }
}

static void
gdk_draw_context_class_init (GdkDrawContextClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

  gobject_class->set_property = gdk_draw_context_set_property;
  gobject_class->get_property = gdk_draw_context_get_property;
  gobject_class->dispose = gdk_draw_context_dispose;

  klass->surface_resized = gdk_draw_context_default_surface_resized;

  /**
   * GdkDrawContext:display:
   *
   * The #GdkDisplay used to create the #GdkDrawContext.
   */
  pspecs[PROP_DISPLAY] =
    g_param_spec_object ("display",
                         P_("Display"),
                         P_("The GDK display used to create the context"),
                         GDK_TYPE_DISPLAY,
                         G_PARAM_READABLE |
                         G_PARAM_STATIC_STRINGS);

  /**
   * GdkDrawContext:surface:
   *
   * The #GdkSurface the gl context is bound to.
   */
  pspecs[PROP_SURFACE] =
    g_param_spec_object ("surface",
                         P_("Surface"),
                         P_("The GDK surface bound to the context"),
                         GDK_TYPE_SURFACE,
                         G_PARAM_READWRITE |
                         G_PARAM_CONSTRUCT_ONLY |
                         G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (gobject_class, LAST_PROP, pspecs);
}

static guint pixels_counter;

static void
gdk_draw_context_init (GdkDrawContext *self)
{
#ifdef G_ENABLE_DEBUG
  if (pixels_counter == 0)
    pixels_counter = gdk_profiler_define_int_counter ("frame pixels", "Pixels drawn per frame");
#endif
}

/**
 * gdk_draw_context_is_in_frame:
 * @context: a #GdkDrawContext
 *
 * Returns %TRUE if @context is in the process of drawing to its surface
 * after a call to gdk_draw_context_begin_frame() and not yet having called
 * gdk_draw_context_end_frame().
 * In this situation, drawing commands may be effecting the contents of a
 * @context's surface.
 *
 * Returns: %TRUE if the context is between begin_frame() and end_frame() calls.
 */
gboolean
gdk_draw_context_is_in_frame (GdkDrawContext *context)
{
  GdkDrawContextPrivate *priv = gdk_draw_context_get_instance_private (context);

  g_return_val_if_fail (GDK_IS_DRAW_CONTEXT (context), FALSE);

  return priv->frame_region != NULL;
}

/*< private >
 * gdk_draw_context_surface_resized:
 * @context: a #GdkDrawContext
 *
 * Called by the #GdkSurface the @context belongs to when the size of the surface
 * changes.
 */
void
gdk_draw_context_surface_resized (GdkDrawContext *context)
{
  GDK_DRAW_CONTEXT_GET_CLASS (context)->surface_resized (context);
}

/**
 * gdk_draw_context_get_display:
 * @context: a #GdkDrawContext
 *
 * Retrieves the #GdkDisplay the @context is created for
 *
 * Returns: (nullable) (transfer none): a #GdkDisplay or %NULL
 */
GdkDisplay *
gdk_draw_context_get_display (GdkDrawContext *context)
{
  GdkDrawContextPrivate *priv = gdk_draw_context_get_instance_private (context);

  g_return_val_if_fail (GDK_IS_DRAW_CONTEXT (context), NULL);

  return priv->surface ? gdk_surface_get_display (priv->surface) : NULL;
}

/**
 * gdk_draw_context_get_surface:
 * @context: a #GdkDrawContext
 *
 * Retrieves the #GdkSurface used by the @context.
 *
 * Returns: (nullable) (transfer none): a #GdkSurface or %NULL
 */
GdkSurface *
gdk_draw_context_get_surface (GdkDrawContext *context)
{
  GdkDrawContextPrivate *priv = gdk_draw_context_get_instance_private (context);

  g_return_val_if_fail (GDK_IS_DRAW_CONTEXT (context), NULL);

  return priv->surface;
}

/**
 * gdk_draw_context_begin_frame:
 * @context: the context used to draw the frame
 * @region: minimum region that should be drawn
 *
 * Indicates that you are beginning the process of redrawing @region
 * on the @context's surface.
 *
 * Calling this function begins a drawing operation using @context on the
 * surface that @context was created from. The actual requirements and
 * guarantees for the drawing operation vary for different implementations
 * of drawing, so a #GdkCairoContext and a #GdkGLContext need to be treated
 * differently.
 *
 * A call to this function is a requirement for drawing and must be followed
 * by a call to gdk_draw_context_end_frame(), which will complete the
 * drawing operation and ensure the contents become visible on screen.
 *
 * Note that the @region passed to this function is the minimum region that
 * needs to be drawn and depending on implementation, windowing system and
 * hardware in use, it might be necessary to draw a larger region. Drawing
 * implementation must use gdk_draw_context_get_frame_region() to query the
 * region that must be drawn.
 *
 * When using GTK+, the widget system automatically places calls to
 * gdk_draw_context_begin_frame() and gdk_draw_context_end_frame() via the
 * use of #GskRenderers, so application code does not need to call these
 * functions explicitly.
 */
void
gdk_draw_context_begin_frame (GdkDrawContext       *context,
                              const cairo_region_t *region)
{
  GdkDrawContextPrivate *priv = gdk_draw_context_get_instance_private (context);

  g_return_if_fail (GDK_IS_DRAW_CONTEXT (context));
  g_return_if_fail (region != NULL);

  if (GDK_SURFACE_DESTROYED (priv->surface))
    return;

  if (priv->surface->paint_context != NULL)
    {
      if (priv->surface->paint_context == context)
        {
          g_critical ("The surface %p is already drawing. You must finish the "
                      "previous drawing operation with gdk_draw_context_end_frame() first.",
                      priv->surface);
        }
      else
        {
          g_critical ("The surface %p is already being drawn by %s %p. "
                      "You cannot draw s surface wih multiple contexts at the same time.",
                      priv->surface,
                      G_OBJECT_TYPE_NAME (priv->surface->paint_context), priv->surface->paint_context);
        }
      return;
    }

  priv->frame_region = cairo_region_copy (region);
  priv->surface->paint_context = g_object_ref (context);

  GDK_DRAW_CONTEXT_GET_CLASS (context)->begin_frame (context, priv->frame_region);
}

#ifdef G_ENABLE_DEBUG
static gint64
region_get_pixels (cairo_region_t *region)
{
  int i, n;
  cairo_rectangle_int_t rect;
  gint64 pixels = 0;

  n = cairo_region_num_rectangles (region);
  for (i = 0; i < n; i++)
    {
      cairo_region_get_rectangle (region, i, &rect);
      pixels += rect.width * rect.height;
    }

  return pixels;
}
#endif

/**
 * gdk_draw_context_end_frame:
 * @context: a #GdkDrawContext
 *
 * Ends a drawing operation started with gdk_draw_context_begin_frame()
 * and makes the drawing available on screen. See that function for more
 * details about drawing.
 *
 * When using a #GdkGLContext, this function may call `glFlush()`
 * implicitly before returning; it is not recommended to call `glFlush()`
 * explicitly before calling this function.
 */
void
gdk_draw_context_end_frame (GdkDrawContext *context)
{
  GdkDrawContextPrivate *priv = gdk_draw_context_get_instance_private (context);

  g_return_if_fail (GDK_IS_DRAW_CONTEXT (context));

  if (GDK_SURFACE_DESTROYED (priv->surface))
    return;

  if (priv->surface->paint_context == NULL)
    {
      g_critical ("The surface %p has no drawing context. You must call "
                  "gdk_draw_context_begin_frame() before calling "
                  "gdk_draw_context_end_frame().", priv->surface);
      return;
    }
  else if (priv->surface->paint_context != context)
    {
      g_critical ("The surface %p is not drawn by this context but by %s %p.",
                  priv->surface, 
                  G_OBJECT_TYPE_NAME (priv->surface->paint_context), priv->surface->paint_context);
      return;
    }

  GDK_DRAW_CONTEXT_GET_CLASS (context)->end_frame (context, priv->frame_region);

#ifdef G_ENABLE_DEBUG
  if (gdk_profiler_is_running ())
    gdk_profiler_set_int_counter (pixels_counter,
                                  g_get_monotonic_time () * 1000,
                                  region_get_pixels (priv->frame_region));
#endif

  g_clear_pointer (&priv->frame_region, cairo_region_destroy);
  g_clear_object (&priv->surface->paint_context);
}

/**
 * gdk_draw_context_get_frame_region:
 * @context: a #GdkDrawContext
 *
 * Retrieves the region that is currently in the process of being repainted.
 *
 * After a call to gdk_draw_context_begin_frame() this function will return
 * a union of the region passed to that function and the area of the surface
 * that the @context determined needs to be repainted.
 *
 * If @context is not inbetween calls to gdk_draw_context_begin_frame() and
 * gdk_draw_context_end_frame(), %NULL will be returned.
 *
 * Returns: (transfer none) (nullable): a Cairo region or %NULL if not drawing
 *     a frame.
 */
const cairo_region_t *
gdk_draw_context_get_frame_region (GdkDrawContext *context)
{
  GdkDrawContextPrivate *priv = gdk_draw_context_get_instance_private (context);

  g_return_val_if_fail (GDK_IS_DRAW_CONTEXT (context), NULL);

  return priv->frame_region;
}
