/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <string.h>

#include <glib-object.h>

#include "base-types.h"

#include "tile.h"
#include "tile-cache.h"
#include "tile-manager.h"
#include "tile-manager-private.h"
#include "tile-rowhints.h"
#include "tile-swap.h"
#include "tile-private.h"


static void  tile_manager_allocate_tiles (TileManager *tm);

#ifdef TILE_PROFILING
extern gint tile_exist_peak;
extern gint tile_exist_count;
#endif

#ifdef GIMP_UNSTABLE
GList *tile_managers = NULL;
#endif


GType
gimp_tile_manager_get_type (void)
{
  static GType type = 0;

  if (! type)
    type = g_boxed_type_register_static ("TileManager",
                                         (GBoxedCopyFunc) tile_manager_ref,
                                         (GBoxedFreeFunc) tile_manager_unref);

  return type;
}

#ifdef GIMP_UNSTABLE
void
tile_manager_exit (void)
{
  if (tile_managers)
    {
      g_warning ("%d tile managers leaked", g_list_length (tile_managers));

      while (tile_managers)
        {
          g_printerr ("unref tile manager %p (%d x %d)\n",
                      tile_managers->data,
                      tile_manager_width (tile_managers->data),
                      tile_manager_height (tile_managers->data));

          tile_manager_unref (tile_managers->data);
        }
    }
}
#endif

static inline gint
tile_manager_get_tile_num (TileManager *tm,
                           gint         xpixel,
                           gint         ypixel)
{
  if ((xpixel < 0) || (xpixel >= tm->width) ||
      (ypixel < 0) || (ypixel >= tm->height))
    return -1;

  return (ypixel / TILE_HEIGHT) * tm->ntile_cols + (xpixel / TILE_WIDTH);
}

TileManager *
tile_manager_new (gint width,
                  gint height,
                  gint bpp)
{
  TileManager *tm;

  g_return_val_if_fail (width > 0 && height > 0, NULL);
  g_return_val_if_fail (bpp > 0 && bpp <= 4, NULL);

  tm = g_slice_new0 (TileManager);

  tm->ref_count   = 1;
  tm->width       = width;
  tm->height      = height;
  tm->bpp         = bpp;
  tm->ntile_rows  = (height + TILE_HEIGHT - 1) / TILE_HEIGHT;
  tm->ntile_cols  = (width  + TILE_WIDTH  - 1) / TILE_WIDTH;
  tm->cached_num  = -1;

#ifdef GIMP_UNSTABLE
  tile_managers = g_list_prepend (tile_managers, tm);
#endif

  return tm;
}

TileManager *
tile_manager_ref (TileManager *tm)
{
  g_return_val_if_fail (tm != NULL, NULL);

  tm->ref_count++;

  return tm;
}

void
tile_manager_unref (TileManager *tm)
{
  g_return_if_fail (tm != NULL);

  tm->ref_count--;

  if (tm->ref_count < 1)
    {
#ifdef GIMP_UNSTABLE
      tile_managers = g_list_remove (tile_managers, tm);
#endif

      if (tm->cached_tile)
        tile_release (tm->cached_tile, FALSE);

      if (tm->tiles)
        {
          gint ntiles = tm->ntile_rows * tm->ntile_cols;
          gint i;

          for (i = 0; i < ntiles; i++)
            tile_detach (tm->tiles[i], tm, i);

          g_free (tm->tiles);
        }

      g_slice_free (TileManager, tm);
    }
}

TileManager *
tile_manager_duplicate (TileManager *tm)
{
  TileManager *copy;
  gint         n_tiles;
  gint         i;

  g_return_val_if_fail (tm != NULL, NULL);

  copy = tile_manager_new (tm->width, tm->height, tm->bpp);

  tile_manager_allocate_tiles (copy);

  n_tiles = tm->ntile_rows * tm->ntile_cols;

  for (i = 0; i < n_tiles; i++)
    {
      Tile *tile;

      tile = tile_manager_get (tm, i, TRUE, FALSE);
      tile_manager_map (copy, i, tile);
      tile_release (tile, FALSE);
    }

  return copy;
}

void
tile_manager_set_validate_proc (TileManager      *tm,
                                TileValidateProc  proc,
                                gpointer          user_data)
{
  g_return_if_fail (tm != NULL);

  tm->validate_proc = proc;
  tm->user_data     = user_data;
}

Tile *
tile_manager_get_tile (TileManager *tm,
                       gint         xpixel,
                       gint         ypixel,
                       gboolean     wantread,
                       gboolean     wantwrite)
{
  g_return_val_if_fail (tm != NULL, NULL);

  return tile_manager_get (tm,
                           tile_manager_get_tile_num (tm, xpixel, ypixel),
                           wantread, wantwrite);
}

Tile *
tile_manager_get (TileManager *tm,
                  gint         tile_num,
                  gboolean     wantread,
                  gboolean     wantwrite)
{
  Tile *tile;
  gint  ntiles;

  g_return_val_if_fail (tm != NULL, NULL);

  ntiles = tm->ntile_rows * tm->ntile_cols;

  if ((tile_num < 0) || (tile_num >= ntiles))
    return NULL;

  if (! tm->tiles)
    tile_manager_allocate_tiles (tm);

  tile = tm->tiles[tile_num];

  if (G_UNLIKELY (wantwrite && ! wantread))
    g_warning ("WRITE-ONLY TILE... UNTESTED!");

#ifdef DEBUG_TILE_MANAGER
  if (G_UNLIKELY (tile->share_count && tile->write_count))
    g_printerr (">> MEEPITY %d,%d <<\n", tile->share_count, tile->write_count);
#endif

  if (wantread)
    {
      if (wantwrite)
        {
          if (tile_num == tm->cached_num)
            {
              tile_release (tm->cached_tile, FALSE);

              tm->cached_tile = NULL;
              tm->cached_num  = -1;
            }

          if (tile->share_count > 1)
            {
              /* Copy-on-write required */
              Tile *new = tile_new (tile->bpp);

              new->ewidth  = tile->ewidth;
              new->eheight = tile->eheight;
              new->valid   = tile->valid;

              new->size    = new->ewidth * new->eheight * new->bpp;
              new->data    = g_new (guchar, new->size);

#ifdef TILE_PROFILING
              tile_exist_count++;
              if (tile_exist_count > tile_exist_peak)
                tile_exist_peak = tile_exist_count;
#endif

              if (tile->rowhint)
                {
                  tile_allocate_rowhints (new);

                  memcpy (new->rowhint, tile->rowhint,
                          new->eheight * sizeof (TileRowHint));
                }

              if (tile->data)
                {
                  memcpy (new->data, tile->data, new->size);
                }
              else
                {
                  tile_lock (tile);
                  memcpy (new->data, tile->data, new->size);
                  tile_release (tile, FALSE);
                }

              tile_detach (tile, tm, tile_num);
              tile_attach (new, tm, tile_num);

              tile = new;
              tm->tiles[tile_num] = tile;
            }

	  /* must lock before marking dirty */
	  tile_lock (tile);
          tile->write_count++;
          tile->dirty = TRUE;
        }
      else
        {
#ifdef DEBUG_TILE_MANAGER
          if (G_UNLIKELY (tile->write_count))
            g_printerr ("STINK! r/o on r/w tile (%d)\n", tile->write_count);
#endif
          tile_lock (tile);
        }
    }

  return tile;
}

Tile *
tile_manager_get_at (TileManager *tm,
                     gint         tile_col,
                     gint         tile_row,
                     gboolean     wantread,
                     gboolean     wantwrite)
{
  g_return_val_if_fail (tm != NULL, NULL);

  if (tile_col < 0 || tile_col >= tm->ntile_cols ||
      tile_row < 0 || tile_row >= tm->ntile_rows)
    return NULL;

  return tile_manager_get (tm,
                           tile_row * tm->ntile_cols + tile_col,
                           wantread, wantwrite);
}

void
tile_manager_validate_tile (TileManager *tm,
                            Tile        *tile)
{
  g_return_if_fail (tm != NULL);
  g_return_if_fail (tile != NULL);

  tile->valid = TRUE;

  if (tm->validate_proc)
    {
      (* tm->validate_proc) (tm, tile, tm->user_data);
    }
  else
    {
      /*  Set the contents of the tile to empty  */
      memset (tile->data, 0, tile_size (tile));
    }

#ifdef DEBUG_TILE_MANAGER
  g_printerr ("%c", tm->user_data ? 'V' : 'v');
#endif
}

static void
tile_manager_allocate_tiles (TileManager *tm)
{
  Tile       **tiles;
  const gint   nrows       = tm->ntile_rows;
  const gint   ncols       = tm->ntile_cols;
  const gint   right_tile  = tm->width  - ((ncols - 1) * TILE_WIDTH);
  const gint   bottom_tile = tm->height - ((nrows - 1) * TILE_HEIGHT);
  gint         i, j, k;

  g_assert (tm->tiles == NULL);

  tiles = g_new (Tile *, nrows * ncols);

  for (i = 0, k = 0; i < nrows; i++)
    {
      for (j = 0; j < ncols; j++, k++)
        {
          Tile *new = tile_new (tm->bpp);

          tile_attach (new, tm, k);

          if (j == (ncols - 1))
            new->ewidth = right_tile;

          if (i == (nrows - 1))
            new->eheight = bottom_tile;

          new->size = new->ewidth * new->eheight * new->bpp;

          tiles[k] = new;
        }
    }

  tm->tiles = tiles;
}

static void
tile_manager_invalidate_tile (TileManager  *tm,
                              gint          tile_num)
{
  Tile *tile = tm->tiles[tile_num];

  if (! tile->valid)
    return;

  if (tile_num == tm->cached_num)
    {
      tile_release (tm->cached_tile, FALSE);

      tm->cached_tile = NULL;
      tm->cached_num  = -1;
    }

  if (tile->cached)
    tile_cache_flush (tile);

  if (G_UNLIKELY (tile->share_count > 1))
    {
      /* This tile is shared.  Replace it with a new invalid tile. */
      Tile *new = tile_new (tile->bpp);

      new->ewidth  = tile->ewidth;
      new->eheight = tile->eheight;
      new->size    = tile->size;

      tile_detach (tile, tm, tile_num);
      tile_attach (new, tm, tile_num);

      tile = new;
      tm->tiles[tile_num] = tile;
    }

  tile->valid = FALSE;

  if (tile->data)
    {
      g_free (tile->data);
      tile->data = NULL;
#ifdef TILE_PROFILING
      tile_exist_count--;
#endif
    }

  if (tile->swap_offset != -1)
    {
      /* If the tile is on disk, then delete its
       *  presence there.
       */
      tile_swap_delete (tile);
    }
}

static void
tile_manager_invalidate_pixel (TileManager  *tm,
                               gint          xpixel,
                               gint          ypixel)
{
  gint num = tile_manager_get_tile_num (tm, xpixel, ypixel);

  if (num < 0)
    return;

  tile_manager_invalidate_tile (tm, num);
}

void
tile_manager_map_tile (TileManager *tm,
                       gint         xpixel,
                       gint         ypixel,
                       Tile        *srctile)
{
  g_return_if_fail (tm != NULL);
  g_return_if_fail (srctile != NULL);

  tile_manager_map (tm,
                    tile_manager_get_tile_num (tm, xpixel, ypixel),
                    srctile);
}

void
tile_manager_map (TileManager *tm,
                  gint         tile_num,
                  Tile        *srctile)
{
  Tile *tile;

  g_return_if_fail (tm != NULL);
  g_return_if_fail (srctile != NULL);
  g_return_if_fail (tile_num >= 0);
  g_return_if_fail (tile_num < tm->ntile_rows * tm->ntile_cols);

  if (G_UNLIKELY (! tm->tiles))
    {
      g_warning ("%s: empty tile level - initializing", G_STRLOC);

      tile_manager_allocate_tiles (tm);
    }

  tile = tm->tiles[tile_num];

#ifdef DEBUG_TILE_MANAGER
  g_printerr (")");
#endif

  if (G_UNLIKELY (! srctile->valid))
    g_warning("%s: srctile not validated yet!  please report", G_STRLOC);

  if (G_UNLIKELY (tile->ewidth  != srctile->ewidth  ||
                  tile->eheight != srctile->eheight ||
                  tile->bpp     != srctile->bpp))
    {
      g_warning ("%s: nonconformant map (%p -> %p)",
                 G_STRLOC, srctile, tile);
    }

  tile_detach (tile, tm, tile_num);

#ifdef DEBUG_TILE_MANAGER
  g_printerr (">");
#endif

#ifdef DEBUG_TILE_MANAGER
  g_printerr (" [src:%p tm:%p tn:%d] ", srctile, tm, tile_num);
#endif

  tile_attach (srctile, tm, tile_num);

  tm->tiles[tile_num] = srctile;

#ifdef DEBUG_TILE_MANAGER
  g_printerr ("}\n");
#endif
}

void
tile_manager_invalidate_area (TileManager *tm,
                              gint         x,
                              gint         y,
                              gint         w,
                              gint         h)
{
  gint  i;
  gint  j;

  /*  if no tiles have been allocated, there's no need to invalidate any  */
  if (! tm->tiles)
    return;

  for (i = y; i < (y + h); i += (TILE_HEIGHT - (i % TILE_HEIGHT)))
    for (j = x; j < (x + w); j += (TILE_WIDTH - (j % TILE_WIDTH)))
      {
        tile_manager_invalidate_pixel (tm, j, i);
      }
}

gint
tile_manager_width (const TileManager *tm)
{
  g_return_val_if_fail (tm != NULL, 0);

  return tm->width;
}

gint
tile_manager_height (const TileManager *tm)
{
  g_return_val_if_fail (tm != NULL, 0);

  return tm->height;
}

gint
tile_manager_bpp (const TileManager *tm)
{
  g_return_val_if_fail (tm != NULL, 0);

  return tm->bpp;
}

gint
tile_manager_tiles_per_col (const TileManager *tm)
{
  g_return_val_if_fail (tm != NULL, 0);

  return tm->ntile_cols;
}

gint
tile_manager_tiles_per_row (const TileManager *tm)
{
  g_return_val_if_fail (tm != NULL, 0);

  return tm->ntile_rows;
}

void
tile_manager_get_offsets (const TileManager *tm,
                          gint              *x,
                          gint              *y)
{
  g_return_if_fail (tm != NULL);
  g_return_if_fail (x != NULL && y != NULL);

  *x = tm->x;
  *y = tm->y;
}

void
tile_manager_set_offsets (TileManager *tm,
                          gint         x,
                          gint         y)
{
  g_return_if_fail (tm != NULL);

  tm->x = x;
  tm->y = y;
}

gint64
tile_manager_get_memsize (const TileManager *tm,
                          gboolean           sparse)
{
  /*  the tile manager itself  */
  gint64 memsize = sizeof (TileManager);

  if (! tm)
    return 0;

  /*  the array of tiles  */
  memsize += (gint64) tm->ntile_rows * tm->ntile_cols * (sizeof (Tile) +
                                                         sizeof (gpointer));

  /*  the memory allocated for the tiles  */
  if (sparse)
    {
      if (tm->tiles)
        {
          Tile   **tiles = tm->tiles;
          gint64   size  = TILE_WIDTH * TILE_HEIGHT * tm->bpp;
          gint     i, j;

          for (i = 0; i < tm->ntile_rows; i++)
            for (j = 0; j < tm->ntile_cols; j++, tiles++)
              {
                if (tile_is_valid (*tiles))
                  memsize += size;
              }
        }
    }
  else
    {
      memsize += (gint64) tm->width * tm->height * tm->bpp;
    }

  return memsize;
}

static inline gint
tile_manager_locate_tile (TileManager *tm,
                          Tile        *tile)
{
  TileLink *tl;

  for (tl = tile->tlink; tl; tl = tl->next)
    {
      if (tl->tm == tm)
        break;
    }

  if (G_UNLIKELY (tl == NULL))
    {
      g_warning ("%s: tile not attached to manager", G_STRLOC);
      return 0;
    }

  return tl->tile_num;
}

void
tile_manager_get_tile_col_row (TileManager *tm,
                               Tile        *tile,
                               gint        *tile_col,
                               gint        *tile_row)
{
  gint tile_num;

  g_return_if_fail (tm != NULL);
  g_return_if_fail (tile != NULL);
  g_return_if_fail (tile_col != NULL && tile_row != NULL);

  tile_num = tile_manager_locate_tile (tm, tile);

  *tile_col = tile_num % tm->ntile_cols;
  *tile_row = tile_num / tm->ntile_cols;
}

void
tile_manager_get_tile_coordinates (TileManager *tm,
                                   Tile        *tile,
                                   gint        *x,
                                   gint        *y)
{
  gint tile_col;
  gint tile_row;

  g_return_if_fail (tm != NULL);
  g_return_if_fail (tile != NULL);
  g_return_if_fail (x != NULL && y != NULL);

  tile_manager_get_tile_col_row (tm, tile, &tile_col, &tile_row);

  *x = TILE_WIDTH  * tile_col;
  *y = TILE_HEIGHT * tile_row;
}

void
tile_manager_map_over_tile (TileManager *tm,
                            Tile        *tile,
                            Tile        *srctile)
{
  TileLink *tl;

  g_return_if_fail (tm != NULL);
  g_return_if_fail (tile != NULL);
  g_return_if_fail (srctile != NULL);

  for (tl = tile->tlink; tl; tl = tl->next)
    {
      if (tl->tm == tm)
        break;
    }

  if (G_UNLIKELY (tl == NULL))
    {
      g_warning ("%s: tile not attached to manager", G_STRLOC);
      return;
    }

  tile_manager_map (tm, tl->tile_num, srctile);
}

void
read_pixel_data (TileManager *tm,
                 gint         x1,
                 gint         y1,
                 gint         x2,
                 gint         y2,
                 guchar      *buffer,
                 guint        stride)
{
  guint x, y;

  for (y = y1; y <= y2; y += TILE_HEIGHT - (y % TILE_HEIGHT))
    for (x = x1; x <= x2; x += TILE_WIDTH - (x % TILE_WIDTH))
      {
        Tile         *tile = tile_manager_get_tile (tm, x, y, TRUE, FALSE);
        const guchar *s    = TILE_DATA_POINTER (tile, x, y);
        guchar       *d    = buffer + stride * (y - y1) + tm->bpp * (x - x1);
        guint         rows, cols;
        guint         srcstride;

        rows = tile->eheight - y % TILE_HEIGHT;
        if (rows > (y2 - y + 1))
          rows = y2 - y + 1;

        cols = tile->ewidth - x % TILE_WIDTH;
        if (cols > (x2 - x + 1))
          cols = x2 - x + 1;

        srcstride = tile->ewidth * tile->bpp;

        while (rows--)
          {
            memcpy (d, s, cols * tm->bpp);

            s += srcstride;
            d += stride;
          }

        tile_release (tile, FALSE);
      }
}

void
write_pixel_data (TileManager  *tm,
                  gint          x1,
                  gint          y1,
                  gint          x2,
                  gint          y2,
                  const guchar *buffer,
                  guint         stride)
{
  guint x, y;

  for (y = y1; y <= y2; y += TILE_HEIGHT - (y % TILE_HEIGHT))
    for (x = x1; x <= x2; x += TILE_WIDTH - (x % TILE_WIDTH))
      {
        Tile         *tile = tile_manager_get_tile (tm, x, y, TRUE, TRUE);
        const guchar *s    = buffer + stride * (y - y1) + tm->bpp * (x - x1);
        guchar       *d    = TILE_DATA_POINTER (tile, x, y);
        guint         rows, cols;
        guint         dststride;

        rows = tile->eheight - y % TILE_HEIGHT;
        if (rows > (y2 - y + 1))
          rows = y2 - y + 1;

        cols = tile->ewidth - x % TILE_WIDTH;
        if (cols > (x2 - x + 1))
          cols = x2 - x + 1;

        dststride = tile->ewidth * tile->bpp;

        while (rows--)
          {
            memcpy (d, s, cols * tm->bpp);

            s += stride;
            d += dststride;
          }

        tile_release (tile, TRUE);
      }
}

void
read_pixel_data_1 (TileManager *tm,
                   gint         x,
                   gint         y,
                   guchar      *buffer)
{
  const gint num = tile_manager_get_tile_num (tm, x, y);

  if (num < 0)
    return;

  if (num != tm->cached_num)    /* must fetch a new tile */
    {
      Tile *tile;

      if (tm->cached_tile)
        tile_release (tm->cached_tile, FALSE);

      tm->cached_num  = -1;
      tm->cached_tile = NULL;

      /*  use a temporary variable instead of assigning to
       *  tm->cached_tile directly to make sure tm->cached_num
       *  and tm->cached_tile are always in a consistent state.
       *  (the requested tile might be invalid and needs to be
       *  validated, which would call tile_manager_get() recursively,
       *  which in turn would clear the cached tile) See bug #472770.
       */
      tile = tile_manager_get (tm, num, TRUE, FALSE);

      tm->cached_num  = num;
      tm->cached_tile = tile;
    }

  {
    const guchar *src = TILE_DATA_POINTER (tm->cached_tile, x, y);

    switch (tm->bpp)
      {
      case 4:
        *buffer++ = *src++;
      case 3:
        *buffer++ = *src++;
      case 2:
        *buffer++ = *src++;
      case 1:
        *buffer++ = *src++;
      }
  }
}

void
write_pixel_data_1 (TileManager  *tm,
                    gint          x,
                    gint          y,
                    const guchar *buffer)
{
  Tile   *tile = tile_manager_get_tile (tm, x, y, TRUE, TRUE);
  guchar *dest = TILE_DATA_POINTER (tile, x, y);

  switch (tm->bpp)
    {
    case 4:
      *dest++ = *buffer++;
    case 3:
      *dest++ = *buffer++;
    case 2:
      *dest++ = *buffer++;
    case 1:
      *dest++ = *buffer++;
    }

  tile_release (tile, TRUE);
}
