/* PIKA - Photo and Image Kooker Application
 * a rebranding of The GNU Image Manipulation Program (created with heckimp)
 * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
 *
 * Original copyright, applying to most contents (license remains unchanged): 
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * pikatilebackendplugin.c
 * Copyright (C) 2011-2019 Øyvind Kolås <pippin@gimp.org>
 *                         Michael Natterer <mitch@gimp.org>
 *
 * 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 <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <stdlib.h>
#include <string.h>

#include "pika.h"

#include "libpikabase/pikaprotocol.h"
#include "libpikabase/pikawire.h"

#include "pika-shm.h"
#include "pikaplugin-private.h"
#include "pikatilebackendplugin.h"


#define TILE_WIDTH  pika_tile_width()
#define TILE_HEIGHT pika_tile_height()


typedef struct _PikaTile PikaTile;

struct _PikaTile
{
  guint   tile_num; /* the number of this tile within the drawable */

  guint   ewidth;   /* the effective width of the tile */
  guint   eheight;  /* the effective height of the tile */

  guchar *data;     /* the pixel data for the tile */
};


struct _PikaTileBackendPluginPrivate
{
  gint32   drawable_id;
  gboolean shadow;
  gint     width;
  gint     height;
  gint     bpp;
  gint     ntile_rows;
  gint     ntile_cols;
};


static gpointer   pika_tile_backend_plugin_command (GeglTileSource  *tile_store,
                                                    GeglTileCommand  command,
                                                    gint             x,
                                                    gint             y,
                                                    gint             z,
                                                    gpointer         data);

static gboolean   pika_tile_write (PikaTileBackendPlugin *backend_plugin,
                                   gint                   x,
                                   gint                   y,
                                   GeglTile              *tile);
static GeglTile * pika_tile_read  (PikaTileBackendPlugin *backend_plugin,
                                   gint                   x,
                                   gint                   y);

static gboolean   pika_tile_init  (PikaTileBackendPlugin *backend_plugin,
                                   PikaTile              *tile,
                                   gint                   row,
                                   gint                   col);
static void       pika_tile_unset (PikaTileBackendPlugin *backend_plugin,
                                   PikaTile              *tile);
static void       pika_tile_get   (PikaTileBackendPlugin *backend_plugin,
                                   PikaTile              *tile);
static void       pika_tile_put   (PikaTileBackendPlugin *backend_plugin,
                                   PikaTile              *tile);


G_DEFINE_TYPE_WITH_PRIVATE (PikaTileBackendPlugin, _pika_tile_backend_plugin,
                            GEGL_TYPE_TILE_BACKEND)

#define parent_class _pika_tile_backend_plugin_parent_class


static GMutex backend_plugin_mutex;


static void
_pika_tile_backend_plugin_class_init (PikaTileBackendPluginClass *klass)
{
}

static void
_pika_tile_backend_plugin_init (PikaTileBackendPlugin *backend)
{
  GeglTileSource *source = GEGL_TILE_SOURCE (backend);

  backend->priv = _pika_tile_backend_plugin_get_instance_private (backend);

  source->command = pika_tile_backend_plugin_command;
}

static gpointer
pika_tile_backend_plugin_command (GeglTileSource  *tile_store,
                                  GeglTileCommand  command,
                                  gint             x,
                                  gint             y,
                                  gint             z,
                                  gpointer         data)
{
  PikaTileBackendPlugin *backend_plugin = PIKA_TILE_BACKEND_PLUGIN (tile_store);
  gpointer               result         = NULL;

  switch (command)
    {
    case GEGL_TILE_GET:
      /* TODO: fetch mipmapped tiles directly from pika, instead of returning
       * NULL to render them locally
       */
      if (z == 0)
        {
          g_mutex_lock (&backend_plugin_mutex);

          result = pika_tile_read (backend_plugin, x, y);

          g_mutex_unlock (&backend_plugin_mutex);
        }
      break;

    case GEGL_TILE_SET:
      /* TODO: actually store mipmapped tiles */
      if (z == 0)
        {
          g_mutex_lock (&backend_plugin_mutex);

          pika_tile_write (backend_plugin, x, y, data);

          g_mutex_unlock (&backend_plugin_mutex);
        }

      gegl_tile_mark_as_stored (data);
      break;

    case GEGL_TILE_FLUSH:
      break;

    default:
      result = gegl_tile_backend_command (GEGL_TILE_BACKEND (tile_store),
                                          command, x, y, z, data);
      break;
    }

  return result;
}


/*  public functions  */

GeglTileBackend *
_pika_tile_backend_plugin_new (PikaDrawable *drawable,
                               gint          shadow)
{
  GeglTileBackend       *backend;
  PikaTileBackendPlugin *backend_plugin;
  const Babl            *format = pika_drawable_get_format (drawable);
  gint                   width  = pika_drawable_get_width  (drawable);
  gint                   height = pika_drawable_get_height (drawable);

  backend = g_object_new (PIKA_TYPE_TILE_BACKEND_PLUGIN,
                          "tile-width",  TILE_WIDTH,
                          "tile-height", TILE_HEIGHT,
                          "format",      format,
                          NULL);

  backend_plugin = PIKA_TILE_BACKEND_PLUGIN (backend);

  backend_plugin->priv->drawable_id = pika_item_get_id (PIKA_ITEM (drawable));
  backend_plugin->priv->shadow      = shadow;
  backend_plugin->priv->width       = width;
  backend_plugin->priv->height      = height;
  backend_plugin->priv->bpp         = pika_drawable_get_bpp (drawable);
  backend_plugin->priv->ntile_rows  = (height + TILE_HEIGHT - 1) / TILE_HEIGHT;
  backend_plugin->priv->ntile_cols  = (width  + TILE_WIDTH  - 1) / TILE_WIDTH;

  gegl_tile_backend_set_extent (backend,
                                GEGL_RECTANGLE (0, 0, width, height));

  return backend;
}


/*  private functions  */

static GeglTile *
pika_tile_read (PikaTileBackendPlugin *backend_plugin,
                gint                   x,
                gint                   y)
{
  PikaTileBackendPluginPrivate *priv    = backend_plugin->priv;
  GeglTileBackend              *backend = GEGL_TILE_BACKEND (backend_plugin);
  GeglTile                     *tile;
  PikaTile                      pika_tile = { 0, };
  gint                          tile_size;
  guchar                       *tile_data;

  if (! pika_tile_init (backend_plugin, &pika_tile, y, x))
    return NULL;

  tile_size  = gegl_tile_backend_get_tile_size (backend);
  tile       = gegl_tile_new (tile_size);
  tile_data  = gegl_tile_get_data (tile);

  pika_tile_get (backend_plugin, &pika_tile);

  if (pika_tile.ewidth * pika_tile.eheight * priv->bpp == tile_size)
    {
      memcpy (tile_data, pika_tile.data, tile_size);
    }
  else
    {
      gint  tile_stride      = TILE_WIDTH * priv->bpp;
      gint  pika_tile_stride = pika_tile.ewidth * priv->bpp;
      guint row;

      for (row = 0; row < pika_tile.eheight; row++)
        {
          memcpy (tile_data      + row * tile_stride,
                  pika_tile.data + row * pika_tile_stride,
                  pika_tile_stride);
        }
    }

  pika_tile_unset (backend_plugin, &pika_tile);

  return tile;
}

static gboolean
pika_tile_write (PikaTileBackendPlugin *backend_plugin,
                 gint                   x,
                 gint                   y,
                 GeglTile              *tile)
{
  PikaTileBackendPluginPrivate *priv      = backend_plugin->priv;
  GeglTileBackend              *backend   = GEGL_TILE_BACKEND (backend_plugin);
  PikaTile                      pika_tile = { 0, };
  gint                          tile_size;
  guchar                       *tile_data;

  if (! pika_tile_init (backend_plugin, &pika_tile, y, x))
    return FALSE;

  tile_size = gegl_tile_backend_get_tile_size (backend);
  tile_data = gegl_tile_get_data (tile);

  pika_tile.data = g_new (guchar,
                          pika_tile.ewidth * pika_tile.eheight *
                          priv->bpp);

  if (pika_tile.ewidth * pika_tile.eheight * priv->bpp == tile_size)
    {
      memcpy (pika_tile.data, tile_data, tile_size);
    }
  else
    {
      gint  tile_stride      = TILE_WIDTH * priv->bpp;
      gint  pika_tile_stride = pika_tile.ewidth * priv->bpp;
      guint row;

      for (row = 0; row < pika_tile.eheight; row++)
        {
          memcpy (pika_tile.data + row * pika_tile_stride,
                  tile_data      + row * tile_stride,
                  pika_tile_stride);
        }
    }

  pika_tile_put (backend_plugin, &pika_tile);
  pika_tile_unset (backend_plugin, &pika_tile);

  return TRUE;
}

static gboolean
pika_tile_init (PikaTileBackendPlugin *backend_plugin,
                PikaTile              *tile,
                gint                   row,
                gint                   col)
{
  PikaTileBackendPluginPrivate *priv = backend_plugin->priv;

  if (row > priv->ntile_rows - 1 ||
      col > priv->ntile_cols - 1)
    {
      return FALSE;
    }

  tile->tile_num = row * priv->ntile_cols + col;

  if (col == (priv->ntile_cols - 1))
    tile->ewidth  = priv->width  - ((priv->ntile_cols - 1) * TILE_WIDTH);
  else
    tile->ewidth  = TILE_WIDTH;

  if (row == (priv->ntile_rows - 1))
    tile->eheight = priv->height - ((priv->ntile_rows - 1) * TILE_HEIGHT);
  else
    tile->eheight = TILE_HEIGHT;

  tile->data = NULL;

  return TRUE;
}

static void
pika_tile_unset (PikaTileBackendPlugin *backend_plugin,
                 PikaTile              *tile)
{
  g_clear_pointer (&tile->data, g_free);
}

static void
pika_tile_get (PikaTileBackendPlugin *backend_plugin,
               PikaTile              *tile)
{
  PikaTileBackendPluginPrivate *priv    = backend_plugin->priv;
  PikaPlugIn                   *plug_in = pika_get_plug_in ();
  GPTileReq                     tile_req;
  GPTileData                   *tile_data;
  PikaWireMessage               msg;

  tile_req.drawable_id = priv->drawable_id;
  tile_req.tile_num    = tile->tile_num;
  tile_req.shadow      = priv->shadow;

  if (! gp_tile_req_write (_pika_plug_in_get_write_channel (plug_in),
                           &tile_req, plug_in))
    pika_quit ();

  _pika_plug_in_read_expect_msg (plug_in, &msg, GP_TILE_DATA);

  tile_data = msg.data;
  if (tile_data->drawable_id != priv->drawable_id ||
      tile_data->tile_num    != tile->tile_num    ||
      tile_data->shadow      != priv->shadow      ||
      tile_data->width       != tile->ewidth      ||
      tile_data->height      != tile->eheight     ||
      tile_data->bpp         != priv->bpp)
    {
#if 0
      g_printerr ("tile_data: %d %d %d %d %d %d\n"
                  "tile:      %d %d %d %d %d %d\n",
                  tile_data->drawable_id,
                  tile_data->tile_num,
                  tile_data->shadow,
                  tile_data->width,
                  tile_data->height,
                  tile_data->bpp,
                  priv->drawable_id,
                  tile->tile_num,
                  priv->shadow,
                  tile->ewidth,
                  tile->eheight,
                  priv->bpp);
#endif
      g_printerr ("received tile info did not match computed tile info");
      pika_quit ();
    }

  if (tile_data->use_shm)
    {
      tile->data = g_memdup2 (_pika_shm_addr (),
                              tile->ewidth * tile->eheight * priv->bpp);
    }
  else
    {
      tile->data = tile_data->data;
      tile_data->data = NULL;
    }

  if (! gp_tile_ack_write (_pika_plug_in_get_write_channel (plug_in),
                           plug_in))
    pika_quit ();

  pika_wire_destroy (&msg);
}

static void
pika_tile_put (PikaTileBackendPlugin *backend_plugin,
               PikaTile              *tile)
{
  PikaTileBackendPluginPrivate *priv    = backend_plugin->priv;
  PikaPlugIn                   *plug_in = pika_get_plug_in ();
  GPTileReq                     tile_req;
  GPTileData                    tile_data;
  GPTileData                   *tile_info;
  PikaWireMessage               msg;

  tile_req.drawable_id = -1;
  tile_req.tile_num    = 0;
  tile_req.shadow      = 0;

  if (! gp_tile_req_write (_pika_plug_in_get_write_channel (plug_in),
                           &tile_req, plug_in))
    pika_quit ();

  _pika_plug_in_read_expect_msg (plug_in, &msg, GP_TILE_DATA);

  tile_info = msg.data;

  tile_data.drawable_id = priv->drawable_id;
  tile_data.tile_num    = tile->tile_num;
  tile_data.shadow      = priv->shadow;
  tile_data.bpp         = priv->bpp;
  tile_data.width       = tile->ewidth;
  tile_data.height      = tile->eheight;
  tile_data.use_shm     = tile_info->use_shm;
  tile_data.data        = NULL;

  if (tile_info->use_shm)
    {
      memcpy (_pika_shm_addr (),
              tile->data,
              tile->ewidth * tile->eheight * priv->bpp);
    }
  else
    {
      tile_data.data = tile->data;
    }

  if (! gp_tile_data_write (_pika_plug_in_get_write_channel (plug_in),
                            &tile_data, plug_in))
    pika_quit ();

  if (! tile_info->use_shm)
    tile_data.data = NULL;

  pika_wire_destroy (&msg);

  _pika_plug_in_read_expect_msg (plug_in, &msg, GP_TILE_ACK);

  pika_wire_destroy (&msg);
}
