/* 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-1997 Spencer Kimball and Peter Mattis
 *
 * pikataggedcontainer.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 <gio/gio.h>

#include "core-types.h"

#include "pika.h"
#include "pikatag.h"
#include "pikatagged.h"
#include "pikataggedcontainer.h"


enum
{
  TAG_COUNT_CHANGED,
  LAST_SIGNAL
};


static void      pika_tagged_container_dispose            (GObject               *object);
static gint64    pika_tagged_container_get_memsize        (PikaObject            *object,
                                                           gint64                *gui_size);

static void      pika_tagged_container_clear              (PikaContainer         *container);

static void      pika_tagged_container_src_add            (PikaFilteredContainer *filtered_container,
                                                           PikaObject            *object);
static void      pika_tagged_container_src_remove         (PikaFilteredContainer *filtered_container,
                                                           PikaObject            *object);
static void      pika_tagged_container_src_freeze         (PikaFilteredContainer *filtered_container);
static void      pika_tagged_container_src_thaw           (PikaFilteredContainer *filtered_container);

static gboolean  pika_tagged_container_object_matches     (PikaTaggedContainer   *tagged_container,
                                                           PikaObject            *object);

static void      pika_tagged_container_tag_added          (PikaTagged            *tagged,
                                                           PikaTag               *tag,
                                                           PikaTaggedContainer   *tagged_container);
static void      pika_tagged_container_tag_removed        (PikaTagged            *tagged,
                                                           PikaTag               *tag,
                                                           PikaTaggedContainer   *tagged_container);
static void      pika_tagged_container_ref_tag            (PikaTaggedContainer   *tagged_container,
                                                           PikaTag               *tag);
static void      pika_tagged_container_unref_tag          (PikaTaggedContainer   *tagged_container,
                                                           PikaTag               *tag);
static void      pika_tagged_container_tag_count_changed  (PikaTaggedContainer   *tagged_container,
                                                           gint                   tag_count);


G_DEFINE_TYPE (PikaTaggedContainer, pika_tagged_container,
               PIKA_TYPE_FILTERED_CONTAINER)

#define parent_class pika_tagged_container_parent_class

static guint pika_tagged_container_signals[LAST_SIGNAL] = { 0, };


static void
pika_tagged_container_class_init (PikaTaggedContainerClass *klass)
{
  GObjectClass               *g_object_class    = G_OBJECT_CLASS (klass);
  PikaObjectClass            *pika_object_class = PIKA_OBJECT_CLASS (klass);
  PikaContainerClass         *container_class   = PIKA_CONTAINER_CLASS (klass);
  PikaFilteredContainerClass *filtered_class    = PIKA_FILTERED_CONTAINER_CLASS (klass);

  g_object_class->dispose        = pika_tagged_container_dispose;

  pika_object_class->get_memsize = pika_tagged_container_get_memsize;

  container_class->clear         = pika_tagged_container_clear;

  filtered_class->src_add        = pika_tagged_container_src_add;
  filtered_class->src_remove     = pika_tagged_container_src_remove;
  filtered_class->src_freeze     = pika_tagged_container_src_freeze;
  filtered_class->src_thaw       = pika_tagged_container_src_thaw;

  klass->tag_count_changed       = pika_tagged_container_tag_count_changed;

  pika_tagged_container_signals[TAG_COUNT_CHANGED] =
    g_signal_new ("tag-count-changed",
                  PIKA_TYPE_TAGGED_CONTAINER,
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (PikaTaggedContainerClass, tag_count_changed),
                  NULL, NULL, NULL,
                  G_TYPE_NONE, 1,
                  G_TYPE_INT);
}

static void
pika_tagged_container_init (PikaTaggedContainer *tagged_container)
{
  tagged_container->tag_ref_counts =
    g_hash_table_new_full ((GHashFunc) pika_tag_get_hash,
                           (GEqualFunc) pika_tag_equals,
                           (GDestroyNotify) g_object_unref,
                           (GDestroyNotify) NULL);
}

static void
pika_tagged_container_dispose (GObject *object)
{
  PikaTaggedContainer *tagged_container = PIKA_TAGGED_CONTAINER (object);

  if (tagged_container->filter)
    {
      g_list_free_full (tagged_container->filter,
                        (GDestroyNotify) pika_tag_or_null_unref);
      tagged_container->filter = NULL;
    }

  g_clear_pointer (&tagged_container->tag_ref_counts, g_hash_table_unref);

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

static gint64
pika_tagged_container_get_memsize (PikaObject *object,
                                   gint64     *gui_size)
{
  gint64 memsize = 0;

  /* FIXME take members into account */

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

static void
pika_tagged_container_clear (PikaContainer *container)
{
  PikaFilteredContainer *filtered_container = PIKA_FILTERED_CONTAINER (container);
  PikaTaggedContainer   *tagged_container   = PIKA_TAGGED_CONTAINER (container);
  GList                 *list;

  for (list = PIKA_LIST (filtered_container->src_container)->queue->head;
       list;
       list = g_list_next (list))
    {
      g_signal_handlers_disconnect_by_func (list->data,
                                            pika_tagged_container_tag_added,
                                            tagged_container);
      g_signal_handlers_disconnect_by_func (list->data,
                                            pika_tagged_container_tag_removed,
                                            tagged_container);
    }

  if (tagged_container->tag_ref_counts)
    {
      g_hash_table_remove_all (tagged_container->tag_ref_counts);
      tagged_container->tag_count = 0;
    }

  PIKA_CONTAINER_CLASS (parent_class)->clear (container);
}

static void
pika_tagged_container_src_add (PikaFilteredContainer *filtered_container,
                               PikaObject            *object)
{
  PikaTaggedContainer *tagged_container = PIKA_TAGGED_CONTAINER (filtered_container);
  GList               *list;

  for (list = pika_tagged_get_tags (PIKA_TAGGED (object));
       list;
       list = g_list_next (list))
    {
      pika_tagged_container_ref_tag (tagged_container, list->data);
    }

  g_signal_connect (object, "tag-added",
                    G_CALLBACK (pika_tagged_container_tag_added),
                    tagged_container);
  g_signal_connect (object, "tag-removed",
                    G_CALLBACK (pika_tagged_container_tag_removed),
                    tagged_container);

  if (pika_tagged_container_object_matches (tagged_container, object))
    {
      pika_container_add (PIKA_CONTAINER (tagged_container), object);
    }
}

static void
pika_tagged_container_src_remove (PikaFilteredContainer *filtered_container,
                                  PikaObject            *object)
{
  PikaTaggedContainer *tagged_container = PIKA_TAGGED_CONTAINER (filtered_container);
  GList               *list;

  g_signal_handlers_disconnect_by_func (object,
                                        pika_tagged_container_tag_added,
                                        tagged_container);
  g_signal_handlers_disconnect_by_func (object,
                                        pika_tagged_container_tag_removed,
                                        tagged_container);

  for (list = pika_tagged_get_tags (PIKA_TAGGED (object));
       list;
       list = g_list_next (list))
    {
      pika_tagged_container_unref_tag (tagged_container, list->data);
    }

  if (pika_tagged_container_object_matches (tagged_container, object))
    {
      pika_container_remove (PIKA_CONTAINER (tagged_container), object);
    }
}

static void
pika_tagged_container_src_freeze (PikaFilteredContainer *filtered_container)
{
  pika_container_clear (PIKA_CONTAINER (filtered_container));
}

static void
pika_tagged_container_src_thaw (PikaFilteredContainer *filtered_container)
{
  GList *list;

  for (list = PIKA_LIST (filtered_container->src_container)->queue->head;
       list;
       list = g_list_next (list))
    {
      pika_tagged_container_src_add (filtered_container, list->data);
    }
}

/**
 * pika_tagged_container_new:
 * @src_container: container to be filtered.
 *
 * Creates a new #PikaTaggedContainer object which creates filtered
 * data view of #PikaTagged objects. It filters @src_container for
 * objects containing all of the filtering tags. Synchronization with
 * @src_container data is performed automatically.
 *
 * Returns: a new #PikaTaggedContainer object.
 **/
PikaContainer *
pika_tagged_container_new (PikaContainer *src_container)
{
  PikaTaggedContainer *tagged_container;
  GType                children_type;
  GCompareFunc         sort_func;

  g_return_val_if_fail (PIKA_IS_LIST (src_container), NULL);

  children_type = pika_container_get_children_type (src_container);
  sort_func     = pika_list_get_sort_func (PIKA_LIST (src_container));

  tagged_container = g_object_new (PIKA_TYPE_TAGGED_CONTAINER,
                                   "sort-func",     sort_func,
                                   "children-type", children_type,
                                   "policy",        PIKA_CONTAINER_POLICY_WEAK,
                                   "unique-names",  FALSE,
                                   "src-container", src_container,
                                   NULL);

  return PIKA_CONTAINER (tagged_container);
}

/**
 * pika_tagged_container_set_filter:
 * @tagged_container: a #PikaTaggedContainer object.
 * @tags:             list of #PikaTag objects.
 *
 * Sets list of tags to be used for filtering. Only objects which have
 * all of the tags assigned match filtering criteria.
 **/
void
pika_tagged_container_set_filter (PikaTaggedContainer *tagged_container,
                                  GList               *tags)
{
  GList *new_filter;

  g_return_if_fail (PIKA_IS_TAGGED_CONTAINER (tagged_container));

  if (tags)
    {
      GList *list;

      for (list = tags; list; list = g_list_next (list))
        g_return_if_fail (list->data == NULL || PIKA_IS_TAG (list->data));
    }

  if (! pika_container_frozen (PIKA_FILTERED_CONTAINER (tagged_container)->src_container))
    {
      pika_tagged_container_src_freeze (PIKA_FILTERED_CONTAINER (tagged_container));
    }

  /*  ref new tags first, they could be the same as the old ones  */
  new_filter = g_list_copy (tags);
  g_list_foreach (new_filter, (GFunc) pika_tag_or_null_ref, NULL);

  g_list_free_full (tagged_container->filter,
                    (GDestroyNotify) pika_tag_or_null_unref);
  tagged_container->filter = new_filter;

  if (! pika_container_frozen (PIKA_FILTERED_CONTAINER (tagged_container)->src_container))
    {
      pika_tagged_container_src_thaw (PIKA_FILTERED_CONTAINER (tagged_container));
    }
}

/**
 * pika_tagged_container_get_filter:
 * @tagged_container: a #PikaTaggedContainer object.
 *
 * Returns current tag filter. Tag filter is a list of PikaTag objects, which
 * must be contained by each object matching filter criteria.
 *
 * Returns: a list of PikaTag objects used as filter. This value should
 *               not be modified or freed.
 **/
const GList *
pika_tagged_container_get_filter (PikaTaggedContainer *tagged_container)
{
  g_return_val_if_fail (PIKA_IS_TAGGED_CONTAINER (tagged_container), NULL);

  return tagged_container->filter;
}

static gboolean
pika_tagged_container_object_matches (PikaTaggedContainer *tagged_container,
                                      PikaObject          *object)
{
  GList *filter_tags;

  for (filter_tags = tagged_container->filter;
       filter_tags;
       filter_tags = g_list_next (filter_tags))
    {
      if (! filter_tags->data)
        {
          /* invalid tag - does not match */
          return FALSE;
        }

      if (! pika_tagged_has_tag (PIKA_TAGGED (object),
                                 filter_tags->data))
        {
          /* match for the tag was not found.
           * since query is of type AND, it whole fails.
           */
          return FALSE;
        }
    }

  return TRUE;
}

static void
pika_tagged_container_tag_added (PikaTagged          *tagged,
                                 PikaTag             *tag,
                                 PikaTaggedContainer *tagged_container)
{
  pika_tagged_container_ref_tag (tagged_container, tag);

  if (pika_tagged_container_object_matches (tagged_container,
                                            PIKA_OBJECT (tagged)) &&
      ! pika_container_have (PIKA_CONTAINER (tagged_container),
                             PIKA_OBJECT (tagged)))
    {
      pika_container_add (PIKA_CONTAINER (tagged_container),
                          PIKA_OBJECT (tagged));
    }
}

static void
pika_tagged_container_tag_removed (PikaTagged          *tagged,
                                   PikaTag             *tag,
                                   PikaTaggedContainer *tagged_container)
{
  pika_tagged_container_unref_tag (tagged_container, tag);

  if (! pika_tagged_container_object_matches (tagged_container,
                                              PIKA_OBJECT (tagged)) &&
      pika_container_have (PIKA_CONTAINER (tagged_container),
                           PIKA_OBJECT (tagged)))
    {
      pika_container_remove (PIKA_CONTAINER (tagged_container),
                             PIKA_OBJECT (tagged));
    }
}

static void
pika_tagged_container_ref_tag (PikaTaggedContainer *tagged_container,
                               PikaTag             *tag)
{
  gint ref_count;

  ref_count = GPOINTER_TO_INT (g_hash_table_lookup (tagged_container->tag_ref_counts,
                                                    tag));
  ref_count++;
  g_hash_table_insert (tagged_container->tag_ref_counts,
                       g_object_ref (tag),
                       GINT_TO_POINTER (ref_count));

  if (ref_count == 1)
    {
      tagged_container->tag_count++;
      g_signal_emit (tagged_container,
                     pika_tagged_container_signals[TAG_COUNT_CHANGED], 0,
                     tagged_container->tag_count);
    }
}

static void
pika_tagged_container_unref_tag (PikaTaggedContainer *tagged_container,
                                 PikaTag             *tag)
{
  gint ref_count;

  ref_count = GPOINTER_TO_INT (g_hash_table_lookup (tagged_container->tag_ref_counts,
                                                    tag));
  ref_count--;

  if (ref_count > 0)
    {
      g_hash_table_insert (tagged_container->tag_ref_counts,
                           g_object_ref (tag),
                           GINT_TO_POINTER (ref_count));
    }
  else
    {
      if (g_hash_table_remove (tagged_container->tag_ref_counts, tag))
        {
          tagged_container->tag_count--;
          g_signal_emit (tagged_container,
                         pika_tagged_container_signals[TAG_COUNT_CHANGED], 0,
                         tagged_container->tag_count);
        }
    }
}

static void
pika_tagged_container_tag_count_changed (PikaTaggedContainer *container,
                                         gint                 tag_count)
{
}

/**
 * pika_tagged_container_get_tag_count:
 * @container:  a #PikaTaggedContainer object.
 *
 * Get number of distinct tags that are currently assigned to all
 * objects in the container. The count is independent of currently
 * used filter, it is provided for all available objects (ie. empty
 * filter).
 *
 * Returns: number of distinct tags assigned to all objects in the
 *               container.
 **/
gint
pika_tagged_container_get_tag_count (PikaTaggedContainer *container)
{
  g_return_val_if_fail (PIKA_IS_TAGGED_CONTAINER (container), 0);

  return container->tag_count;
}
