/* 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
 *
 * pikatagcache.c
 * Copyright (C) 2008 Aurimas Juška <aurisj@svn.gnome.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 <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>

#include "libpikabase/pikabase.h"
#include "libpikamath/pikamath.h"
#include "libpikaconfig/pikaconfig.h"

#include "core-types.h"

#include "config/pikaxmlparser.h"

#include "pika-memsize.h"
#include "pikacontext.h"
#include "pikadata.h"
#include "pikalist.h"
#include "pikatag.h"
#include "pikatagcache.h"
#include "pikatagged.h"

#include "pika-intl.h"


#define PIKA_TAG_CACHE_FILE  "tags.xml"

/* #define DEBUG_PIKA_TAG_CACHE  1 */


enum
{
  PROP_0,
  PROP_PIKA
};


typedef struct
{
  GQuark  identifier;
  GQuark  checksum;
  GList  *tags;
  guint   referenced : 1;
} PikaTagCacheRecord;

typedef struct
{
  GArray             *records;
  PikaTagCacheRecord  current_record;
} PikaTagCacheParseData;

struct _PikaTagCachePrivate
{
  GArray *records;
  GList  *containers;
};


static void          pika_tag_cache_finalize           (GObject                *object);

static gint64        pika_tag_cache_get_memsize        (PikaObject             *object,
                                                        gint64                 *gui_size);
static void          pika_tag_cache_object_initialize  (PikaTagged             *tagged,
                                                        PikaTagCache           *cache);
static void          pika_tag_cache_add_object         (PikaTagCache           *cache,
                                                        PikaTagged             *tagged);

static void          pika_tag_cache_load_start_element (GMarkupParseContext    *context,
                                                        const gchar            *element_name,
                                                        const gchar           **attribute_names,
                                                        const gchar           **attribute_values,
                                                        gpointer                user_data,
                                                        GError                **error);
static void          pika_tag_cache_load_end_element   (GMarkupParseContext    *context,
                                                        const gchar            *element_name,
                                                        gpointer                user_data,
                                                        GError                **error);
static void          pika_tag_cache_load_text          (GMarkupParseContext    *context,
                                                        const gchar            *text,
                                                        gsize                   text_len,
                                                        gpointer                user_data,
                                                        GError                **error);
static  void         pika_tag_cache_load_error         (GMarkupParseContext    *context,
                                                        GError                 *error,
                                                        gpointer                user_data);
static const gchar * pika_tag_cache_attribute_name_to_value
                                                       (const gchar           **attribute_names,
                                                        const gchar           **attribute_values,
                                                        const gchar            *name);

static GQuark        pika_tag_cache_get_error_domain   (void);


G_DEFINE_TYPE_WITH_PRIVATE (PikaTagCache, pika_tag_cache, PIKA_TYPE_OBJECT)

#define parent_class pika_tag_cache_parent_class


static void
pika_tag_cache_class_init (PikaTagCacheClass *klass)
{
  GObjectClass    *object_class      = G_OBJECT_CLASS (klass);
  PikaObjectClass *pika_object_class = PIKA_OBJECT_CLASS (klass);

  object_class->finalize         = pika_tag_cache_finalize;

  pika_object_class->get_memsize = pika_tag_cache_get_memsize;
}

static void
pika_tag_cache_init (PikaTagCache *cache)
{
  cache->priv = pika_tag_cache_get_instance_private (cache);

  cache->priv->records    = g_array_new (FALSE, FALSE,
                                         sizeof (PikaTagCacheRecord));
  cache->priv->containers = NULL;
}

static void
pika_tag_cache_finalize (GObject *object)
{
  PikaTagCache *cache = PIKA_TAG_CACHE (object);

  if (cache->priv->records)
    {
      gint i;

      for (i = 0; i < cache->priv->records->len; i++)
        {
          PikaTagCacheRecord *rec = &g_array_index (cache->priv->records,
                                                    PikaTagCacheRecord, i);

          g_list_free_full (rec->tags, (GDestroyNotify) g_object_unref);
        }

      g_array_free (cache->priv->records, TRUE);
      cache->priv->records = NULL;
    }

  if (cache->priv->containers)
    {
      g_list_free (cache->priv->containers);
      cache->priv->containers = NULL;
    }

  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static gint64
pika_tag_cache_get_memsize (PikaObject *object,
                            gint64     *gui_size)
{
  PikaTagCache *cache   = PIKA_TAG_CACHE (object);
  gint64        memsize = 0;

  memsize += pika_g_list_get_memsize (cache->priv->containers, 0);
  memsize += cache->priv->records->len * sizeof (PikaTagCacheRecord);

  return memsize + PIKA_OBJECT_CLASS (parent_class)->get_memsize (object,
                                                                  gui_size);
}

/**
 * pika_tag_cache_new:
 *
 * Returns: creates new PikaTagCache object.
 **/
PikaTagCache *
pika_tag_cache_new (void)
{
  return g_object_new (PIKA_TYPE_TAG_CACHE, NULL);
}

static void
pika_tag_cache_container_add_callback (PikaTagCache  *cache,
                                       PikaTagged    *tagged,
                                       PikaContainer *not_used)
{
  pika_tag_cache_add_object (cache, tagged);
}

/**
 * pika_tag_cache_add_container:
 * @cache:      a PikaTagCache object.
 * @container:  container containing PikaTagged objects.
 *
 * Adds container of PikaTagged objects to tag cache. Before calling this
 * function tag cache must be loaded using pika_tag_cache_load(). When tag
 * cache is saved to file, tags are collected from objects in priv->containers.
 **/
void
pika_tag_cache_add_container (PikaTagCache  *cache,
                              PikaContainer *container)
{
  g_return_if_fail (PIKA_IS_TAG_CACHE (cache));
  g_return_if_fail (PIKA_IS_CONTAINER (container));

  cache->priv->containers = g_list_append (cache->priv->containers, container);
  pika_container_foreach (container, (GFunc) pika_tag_cache_object_initialize,
                          cache);

  g_signal_connect_swapped (container, "add",
                            G_CALLBACK (pika_tag_cache_container_add_callback),
                            cache);
}

static void
pika_tag_cache_add_object (PikaTagCache *cache,
                           PikaTagged   *tagged)
{
  gchar  *identifier;
  GQuark  identifier_quark = 0;
  gchar  *checksum;
  GQuark  checksum_quark = 0;
  GList  *list;
  gint    i;

  identifier = pika_tagged_get_identifier (tagged);

  if (identifier)
    {
      identifier_quark = g_quark_try_string (identifier);
      g_free (identifier);
    }

  if (identifier_quark)
    {
      for (i = 0; i < cache->priv->records->len; i++)
        {
          PikaTagCacheRecord *rec = &g_array_index (cache->priv->records,
                                                    PikaTagCacheRecord, i);

          if (rec->identifier == identifier_quark)
            {
              for (list = rec->tags; list; list = g_list_next (list))
                {
                  pika_tagged_add_tag (tagged, PIKA_TAG (list->data));
                }

              rec->referenced = TRUE;
              return;
            }
        }
    }

  checksum = pika_tagged_get_checksum (tagged);

  if (checksum)
    {
      checksum_quark = g_quark_try_string (checksum);
      g_free (checksum);
    }

  if (checksum_quark)
    {
      for (i = 0; i < cache->priv->records->len; i++)
        {
          PikaTagCacheRecord *rec = &g_array_index (cache->priv->records,
                                                    PikaTagCacheRecord, i);

          if (rec->checksum == checksum_quark)
            {
#if DEBUG_PIKA_TAG_CACHE
              g_printerr ("remapping identifier: %s ==> %s\n",
                          rec->identifier ? g_quark_to_string (rec->identifier) : "(NULL)",
                          identifier_quark ? g_quark_to_string (identifier_quark) : "(NULL)");
#endif

              rec->identifier = identifier_quark;

              for (list = rec->tags; list; list = g_list_next (list))
                {
                  pika_tagged_add_tag (tagged, PIKA_TAG (list->data));
                }

              rec->referenced = TRUE;
              return;
            }
        }
    }

}

static void
pika_tag_cache_object_initialize (PikaTagged   *tagged,
                                  PikaTagCache *cache)
{
  pika_tag_cache_add_object (cache, tagged);
}

static void
pika_tag_cache_tagged_to_cache_record_foreach (PikaTagged  *tagged,
                                               GList      **cache_records)
{
  gchar *identifier = pika_tagged_get_identifier (tagged);

  if (identifier)
    {
      PikaTagCacheRecord *cache_rec = g_new (PikaTagCacheRecord, 1);
      gchar              *checksum;

      checksum = pika_tagged_get_checksum (tagged);

      cache_rec->identifier = g_quark_from_string (identifier);
      cache_rec->checksum   = g_quark_from_string (checksum);
      cache_rec->tags       = g_list_copy (pika_tagged_get_tags (tagged));

      g_free (checksum);

      *cache_records = g_list_prepend (*cache_records, cache_rec);
    }

  g_free (identifier);
}

/**
 * pika_tag_cache_save:
 * @cache:      a PikaTagCache object.
 *
 * Saves tag cache to cache file.
 **/
void
pika_tag_cache_save (PikaTagCache *cache)
{
  GString       *buf;
  GList         *saved_records;
  GList         *iterator;
  GFile         *file;
  GOutputStream *output;
  GError        *error = NULL;
  gint           i;

  g_return_if_fail (PIKA_IS_TAG_CACHE (cache));

  saved_records = NULL;
  for (i = 0; i < cache->priv->records->len; i++)
    {
      PikaTagCacheRecord *current_record = &g_array_index (cache->priv->records,
                                                           PikaTagCacheRecord, i);

      if (! current_record->referenced && current_record->tags)
        {
          /* keep tagged objects which have tags assigned
           * but were not loaded.
           */
          PikaTagCacheRecord *record_copy = g_new (PikaTagCacheRecord, 1);

          record_copy->identifier = current_record->identifier;
          record_copy->checksum   = current_record->checksum;
          record_copy->tags       = g_list_copy (current_record->tags);

          saved_records = g_list_prepend (saved_records, record_copy);
        }
    }

  for (iterator = cache->priv->containers;
       iterator;
       iterator = g_list_next (iterator))
    {
      pika_container_foreach (PIKA_CONTAINER (iterator->data),
                              (GFunc) pika_tag_cache_tagged_to_cache_record_foreach,
                              &saved_records);
    }

  saved_records = g_list_reverse (saved_records);

  buf = g_string_new ("");
  g_string_append (buf, "<?xml version='1.0' encoding='UTF-8'?>\n");
  g_string_append (buf, "<tags>\n");

  for (iterator = saved_records; iterator; iterator = g_list_next (iterator))
    {
      PikaTagCacheRecord *cache_rec = iterator->data;
      GList              *tag_iterator;
      gchar              *identifier_string;
      gchar              *tag_string;

      identifier_string = g_markup_escape_text (g_quark_to_string (cache_rec->identifier), -1);
      g_string_append_printf (buf, "\n  <resource identifier=\"%s\" checksum=\"%s\">\n",
                              identifier_string,
                              g_quark_to_string (cache_rec->checksum));
      g_free (identifier_string);

      for (tag_iterator = cache_rec->tags;
           tag_iterator;
           tag_iterator = g_list_next (tag_iterator))
        {
          PikaTag *tag = PIKA_TAG (tag_iterator->data);

          if (! pika_tag_get_internal (tag))
            {
              tag_string = g_markup_escape_text (pika_tag_get_name (tag), -1);
              g_string_append_printf (buf, "    <tag>%s</tag>\n", tag_string);
              g_free (tag_string);
            }
        }

      g_string_append (buf, "  </resource>\n");
    }

  g_string_append (buf, "</tags>\n");

  file = pika_directory_file (PIKA_TAG_CACHE_FILE, NULL);

  output = G_OUTPUT_STREAM (g_file_replace (file,
                                            NULL, FALSE, G_FILE_CREATE_NONE,
                                            NULL, &error));
  if (! output)
    {
      g_printerr ("%s\n", error->message);
    }
  else if (! g_output_stream_write_all (output, buf->str, buf->len,
                                        NULL, NULL, &error))
    {
      GCancellable *cancellable = g_cancellable_new ();

      g_printerr (_("Error writing '%s': %s\n"),
                  pika_file_get_utf8_name (file), error->message);

      /* Cancel the overwrite initiated by g_file_replace(). */
      g_cancellable_cancel (cancellable);
      g_output_stream_close (output, cancellable, NULL);
      g_object_unref (cancellable);
    }
  else if (! g_output_stream_close (output, NULL, &error))
    {
      g_printerr (_("Error closing '%s': %s\n"),
                  pika_file_get_utf8_name (file), error->message);
    }

  if (output)
    g_object_unref (output);

  g_clear_error (&error);
  g_object_unref (file);
  g_string_free (buf, TRUE);

  for (iterator = saved_records;
       iterator;
       iterator = g_list_next (iterator))
    {
      PikaTagCacheRecord *cache_rec = iterator->data;

      g_list_free (cache_rec->tags);
      g_free (cache_rec);
    }

  g_list_free (saved_records);
}

/**
 * pika_tag_cache_load:
 * @cache:      a PikaTagCache object.
 *
 * Loads tag cache from file.
 **/
void
pika_tag_cache_load (PikaTagCache *cache)
{
  GFile                 *file;
  GMarkupParser          markup_parser;
  PikaXmlParser         *xml_parser;
  PikaTagCacheParseData  parse_data;
  GError                *error = NULL;

  g_return_if_fail (PIKA_IS_TAG_CACHE (cache));

  /* clear any previous priv->records */
  cache->priv->records = g_array_set_size (cache->priv->records, 0);

  parse_data.records = g_array_new (FALSE, FALSE, sizeof (PikaTagCacheRecord));
  memset (&parse_data.current_record, 0, sizeof (PikaTagCacheRecord));

  markup_parser.start_element = pika_tag_cache_load_start_element;
  markup_parser.end_element   = pika_tag_cache_load_end_element;
  markup_parser.text          = pika_tag_cache_load_text;
  markup_parser.passthrough   = NULL;
  markup_parser.error         = pika_tag_cache_load_error;

  xml_parser = pika_xml_parser_new (&markup_parser, &parse_data);

  file = pika_directory_file (PIKA_TAG_CACHE_FILE, NULL);

  if (pika_xml_parser_parse_gfile (xml_parser, file, &error))
    {
      cache->priv->records = g_array_append_vals (cache->priv->records,
                                                  parse_data.records->data,
                                                  parse_data.records->len);
    }
  else
    {
      g_printerr ("Failed to parse tag cache: %s\n",
                  error ? error->message : "WTF unknown error");
      g_clear_error (&error);
    }

  g_object_unref (file);
  pika_xml_parser_free (xml_parser);
  g_array_free (parse_data.records, TRUE);
}

static  void
pika_tag_cache_load_start_element (GMarkupParseContext *context,
                                   const gchar         *element_name,
                                   const gchar        **attribute_names,
                                   const gchar        **attribute_values,
                                   gpointer             user_data,
                                   GError             **error)
{
  PikaTagCacheParseData *parse_data = user_data;

  if (! strcmp (element_name, "resource"))
    {
      const gchar *identifier;
      const gchar *checksum;

      identifier = pika_tag_cache_attribute_name_to_value (attribute_names,
                                                           attribute_values,
                                                           "identifier");
      checksum   = pika_tag_cache_attribute_name_to_value (attribute_names,
                                                           attribute_values,
                                                           "checksum");

      if (! identifier)
        {
          g_set_error (error,
                       pika_tag_cache_get_error_domain (),
                       1001,
                       "Resource tag does not contain required attribute identifier.");
          return;
        }

      memset (&parse_data->current_record, 0, sizeof (PikaTagCacheRecord));

      parse_data->current_record.identifier = g_quark_from_string (identifier);
      parse_data->current_record.checksum   = g_quark_from_string (checksum);
    }
}

static void
pika_tag_cache_load_end_element (GMarkupParseContext *context,
                                 const gchar         *element_name,
                                 gpointer             user_data,
                                 GError             **error)
{
  PikaTagCacheParseData *parse_data = user_data;

  if (strcmp (element_name, "resource") == 0)
    {
      parse_data->records = g_array_append_val (parse_data->records,
                                                parse_data->current_record);
      memset (&parse_data->current_record, 0, sizeof (PikaTagCacheRecord));
    }
}

static void
pika_tag_cache_load_text (GMarkupParseContext  *context,
                          const gchar          *text,
                          gsize                 text_len,
                          gpointer              user_data,
                          GError              **error)
{
  PikaTagCacheParseData *parse_data = user_data;
  const gchar           *current_element;
  gchar                  buffer[2048];
  PikaTag               *tag;

  current_element = g_markup_parse_context_get_element (context);

  if (g_strcmp0 (current_element, "tag") == 0)
    {
      if (text_len >= sizeof (buffer))
        {
          g_set_error (error, pika_tag_cache_get_error_domain (), 1002,
                       "Tag value is too long.");
          return;
        }

      memcpy (buffer, text, text_len);
      buffer[text_len] = '\0';

      tag = pika_tag_new (buffer);
      if (tag)
        {
          parse_data->current_record.tags = g_list_append (parse_data->current_record.tags,
                                                           tag);
        }
      else
        {
          g_warning ("dropping invalid tag '%s' from '%s'\n", buffer,
                     g_quark_to_string (parse_data->current_record.identifier));
        }
    }
}

static  void
pika_tag_cache_load_error (GMarkupParseContext *context,
                           GError              *error,
                           gpointer             user_data)
{
  g_printerr ("Tag cache parse error: %s\n", error->message);
}

static const gchar*
pika_tag_cache_attribute_name_to_value (const gchar **attribute_names,
                                        const gchar **attribute_values,
                                        const gchar  *name)
{
  while (*attribute_names)
    {
      if (! strcmp (*attribute_names, name))
        {
          return *attribute_values;
        }

      attribute_names++;
      attribute_values++;
    }

  return NULL;
}

static GQuark
pika_tag_cache_get_error_domain (void)
{
  return g_quark_from_static_string ("pika-tag-cache-error-quark");
}
