
#include "config.h"
#include "gskgltextureatlasprivate.h"
#include "gskdebugprivate.h"
#include "gdkglcontextprivate.h"
#include <epoxy/gl.h>

#define ATLAS_SIZE (512)
#define MAX_OLD_RATIO 0.5

static void
free_atlas (gpointer v)
{
  GskGLTextureAtlas *atlas = v;

  gsk_gl_texture_atlas_free (atlas);

  g_free (atlas);
}

GskGLTextureAtlases *
gsk_gl_texture_atlases_new (void)
{
  GskGLTextureAtlases *self;

  self = g_new (GskGLTextureAtlases, 1);
  self->atlases = g_ptr_array_new_with_free_func (free_atlas);

  self->ref_count = 1;

  return self;
}

GskGLTextureAtlases *
gsk_gl_texture_atlases_ref (GskGLTextureAtlases *self)
{
  self->ref_count++;

  return self;
}

void
gsk_gl_texture_atlases_unref (GskGLTextureAtlases *self)
{
  g_assert (self->ref_count > 0);

  if (self->ref_count == 1)
    {
      g_ptr_array_unref (self->atlases);
      g_free (self);
      return;
    }

  self->ref_count--;
}

#if 0
static void
write_atlas_to_png (GskGLTextureAtlas *atlas,
                    const char        *filename)
{
  int stride = cairo_format_stride_for_width (CAIRO_FORMAT_ARGB32, atlas->width);
  guchar *data = g_malloc (atlas->height * stride);
  cairo_surface_t *s;

  glBindTexture (GL_TEXTURE_2D, atlas->texture_id);
  glGetTexImage (GL_TEXTURE_2D, 0, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, data);
  s = cairo_image_surface_create_for_data (data, CAIRO_FORMAT_ARGB32, atlas->width, atlas->height, stride);
  cairo_surface_write_to_png (s, filename);

  cairo_surface_destroy (s);
  g_free (data);
}
#endif

void
gsk_gl_texture_atlases_begin_frame (GskGLTextureAtlases *self,
                                    GPtrArray           *removed)
{
  int i;

  for (i = self->atlases->len - 1; i >= 0; i--)
    {
      GskGLTextureAtlas *atlas = g_ptr_array_index (self->atlases, i);

      if (gsk_gl_texture_atlas_get_unused_ratio (atlas) > MAX_OLD_RATIO)
        {
          GSK_NOTE(GLYPH_CACHE,
                   g_message ("Dropping atlas %d (%g.2%% old)", i,
                              100.0 * gsk_gl_texture_atlas_get_unused_ratio (atlas)));

          if (atlas->texture_id != 0)
            {
              glDeleteTextures (1, &atlas->texture_id);
              atlas->texture_id = 0;
            }

          g_ptr_array_add (removed, atlas);
          g_ptr_array_remove_index (self->atlases, i);
       }
    }

  GSK_NOTE(GLYPH_CACHE, {
    static guint timestamp;
    if (timestamp++ % 60 == 0)
      g_message ("%d atlases", self->atlases->len);
  });


#if 0
  {
    static guint timestamp;

    timestamp++;
    if (timestamp % 10 == 0)
      for (i = 0; i < self->atlases->len; i++)
        {
          GskGLTextureAtlas *atlas = g_ptr_array_index (self->atlases, i);

          if (atlas->texture_id)
            {
              char *filename;

              filename = g_strdup_printf ("textureatlas%d-%u.png", i, timestamp);
              write_atlas_to_png (atlas, filename);
              g_free (filename);
            }
         }
   }
#endif
}

gboolean
gsk_gl_texture_atlases_pack (GskGLTextureAtlases *self,
                             int                  width,
                             int                  height,
                             GskGLTextureAtlas  **atlas_out,
                             int                 *out_x,
                             int                 *out_y)
{
  GskGLTextureAtlas *atlas;
  int x, y;
  int i;

  g_assert (width  < ATLAS_SIZE);
  g_assert (height < ATLAS_SIZE);

  atlas = NULL;

  for (i = 0; i < self->atlases->len; i++)
    {
      atlas = g_ptr_array_index (self->atlases, i);

      if (gsk_gl_texture_atlas_pack (atlas, width, height, &x, &y))
        break;

      atlas = NULL;
    }

  if (atlas == NULL)
    {
      /* No atlas has enough space, so create a new one... */
      atlas = g_malloc (sizeof (GskGLTextureAtlas));
      gsk_gl_texture_atlas_init (atlas, ATLAS_SIZE, ATLAS_SIZE);
      gsk_gl_texture_atlas_realize (atlas);
      g_ptr_array_add (self->atlases, atlas);

      /* Pack it onto that one, which surely has enough space... */
      if (!gsk_gl_texture_atlas_pack (atlas, width, height, &x, &y))
        g_assert_not_reached ();

      GSK_NOTE(GLYPH_CACHE, g_message ("adding new atlas"));
    }

  *atlas_out = atlas;
  *out_x = x;
  *out_y = y;

  return TRUE;
}

void
gsk_gl_texture_atlas_init (GskGLTextureAtlas *self,
                           int                width,
                           int                height)
{
  memset (self, 0, sizeof (*self));

  self->texture_id = 0;
  self->width = width;
  self->height = height;

  /* TODO: We might want to change the strategy about the amount of
   *       nodes here? stb_rect_pack.h says with is optimal. */
  self->nodes = g_malloc0 (sizeof (struct stbrp_node) * width);
  stbrp_init_target (&self->context,
                     width, height,
                     self->nodes,
                     width);

  gsk_gl_texture_atlas_realize (self);
}

void
gsk_gl_texture_atlas_free (GskGLTextureAtlas *self)
{
  if (self->texture_id != 0)
    {
      glDeleteTextures (1, &self->texture_id);
      self->texture_id = 0;
    }

  g_clear_pointer (&self->nodes, g_free);
}

void
gsk_gl_texture_atlas_mark_unused (GskGLTextureAtlas *self,
                                  int                width,
                                  int                height)
{
  self->unused_pixels += (width * height);
}


void
gsk_gl_texture_atlas_mark_used (GskGLTextureAtlas *self,
                                int                width,
                                int                height)
{
  self->unused_pixels -= (width * height);

  g_assert (self->unused_pixels >= 0);
}

gboolean
gsk_gl_texture_atlas_pack (GskGLTextureAtlas *self,
                           int                width,
                           int                height,
                           int               *out_x,
                           int               *out_y)
{
  stbrp_rect rect;

  g_assert (out_x);
  g_assert (out_y);

  rect.w = width;
  rect.h = height;

  stbrp_pack_rects (&self->context, &rect, 1);

  if (rect.was_packed)
    {
      *out_x = rect.x;
      *out_y = rect.y;
    }

  return rect.was_packed;
}

double
gsk_gl_texture_atlas_get_unused_ratio (const GskGLTextureAtlas *self)
{
  if (self->unused_pixels > 0)
    return (double)(self->unused_pixels) / (double)(self->width * self->height);

  return 0.0;
}

/* Not using gdk_gl_driver_create_texture here, since we want
 * this texture to survive the driver and stay around until
 * the display gets closed.
 */
static guint
create_shared_texture (int width,
                       int height)
{
  guint texture_id;

  glGenTextures (1, &texture_id);
  glBindTexture (GL_TEXTURE_2D, texture_id);

  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

  if (gdk_gl_context_get_use_es (gdk_gl_context_get_current ()))
    glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
  else
    glTexImage2D (GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_BGRA, GL_UNSIGNED_BYTE, NULL);

  glBindTexture (GL_TEXTURE_2D, 0);

  return texture_id;
}

void
gsk_gl_texture_atlas_realize (GskGLTextureAtlas *atlas)
{
  if (atlas->texture_id)
    return;

  atlas->texture_id = create_shared_texture (atlas->width, atlas->height);
  gdk_gl_context_label_object_printf (gdk_gl_context_get_current (),
                                      GL_TEXTURE, atlas->texture_id,
                                      "Texture atlas %d", atlas->texture_id);
}
