/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995-1997 Spencer Kimball and Peter Mattis
 *
 * gimpitemtree.c
 * Copyright (C) 2010 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 <http://www.gnu.org/licenses/>.
 */

#include "config.h"

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

#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>

#include "core-types.h"

#include "gimpimage.h"
#include "gimpimage-undo-push.h"
#include "gimpitem.h"
#include "gimpitemstack.h"
#include "gimpitemtree.h"


enum
{
  PROP_0,
  PROP_IMAGE,
  PROP_CONTAINER_TYPE,
  PROP_ITEM_TYPE,
  PROP_ACTIVE_ITEM
};


typedef struct _GimpItemTreePrivate GimpItemTreePrivate;

struct _GimpItemTreePrivate
{
  GimpImage  *image;

  GType       container_type;
  GType       item_type;

  GimpItem   *active_item;

  GHashTable *name_hash;
};

#define GIMP_ITEM_TREE_GET_PRIVATE(object) \
        G_TYPE_INSTANCE_GET_PRIVATE (object, \
                                     GIMP_TYPE_ITEM_TREE, \
                                     GimpItemTreePrivate)


/*  local function prototypes  */

static void     gimp_item_tree_constructed   (GObject      *object);
static void     gimp_item_tree_finalize      (GObject      *object);
static void     gimp_item_tree_set_property  (GObject      *object,
                                              guint         property_id,
                                              const GValue *value,
                                              GParamSpec   *pspec);
static void     gimp_item_tree_get_property  (GObject      *object,
                                              guint         property_id,
                                              GValue       *value,
                                              GParamSpec   *pspec);

static gint64   gimp_item_tree_get_memsize   (GimpObject   *object,
                                              gint64       *gui_size);

static void     gimp_item_tree_uniquefy_name (GimpItemTree *tree,
                                              GimpItem     *item,
                                              const gchar  *new_name);


G_DEFINE_TYPE (GimpItemTree, gimp_item_tree, GIMP_TYPE_OBJECT)

#define parent_class gimp_item_tree_parent_class


static void
gimp_item_tree_class_init (GimpItemTreeClass *klass)
{
  GObjectClass    *object_class      = G_OBJECT_CLASS (klass);
  GimpObjectClass *gimp_object_class = GIMP_OBJECT_CLASS (klass);

  object_class->constructed      = gimp_item_tree_constructed;
  object_class->finalize         = gimp_item_tree_finalize;
  object_class->set_property     = gimp_item_tree_set_property;
  object_class->get_property     = gimp_item_tree_get_property;

  gimp_object_class->get_memsize = gimp_item_tree_get_memsize;

  g_object_class_install_property (object_class, PROP_IMAGE,
                                   g_param_spec_object ("image",
                                                        NULL, NULL,
                                                        GIMP_TYPE_IMAGE,
                                                        GIMP_PARAM_READWRITE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_CONTAINER_TYPE,
                                   g_param_spec_gtype ("container-type",
                                                       NULL, NULL,
                                                       GIMP_TYPE_ITEM_STACK,
                                                       GIMP_PARAM_READWRITE |
                                                       G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_ITEM_TYPE,
                                   g_param_spec_gtype ("item-type",
                                                       NULL, NULL,
                                                       GIMP_TYPE_ITEM,
                                                       GIMP_PARAM_READWRITE |
                                                       G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_ACTIVE_ITEM,
                                   g_param_spec_object ("active-item",
                                                        NULL, NULL,
                                                        GIMP_TYPE_ITEM,
                                                        GIMP_PARAM_READWRITE));

  g_type_class_add_private (klass, sizeof (GimpItemTreePrivate));
}

static void
gimp_item_tree_init (GimpItemTree *tree)
{
  GimpItemTreePrivate *private = GIMP_ITEM_TREE_GET_PRIVATE (tree);

  private->name_hash = g_hash_table_new (g_str_hash, g_str_equal);
}

static void
gimp_item_tree_constructed (GObject *object)
{
  GimpItemTree        *tree    = GIMP_ITEM_TREE (object);
  GimpItemTreePrivate *private = GIMP_ITEM_TREE_GET_PRIVATE (tree);

  G_OBJECT_CLASS (parent_class)->constructed (object);

  gimp_assert (GIMP_IS_IMAGE (private->image));
  gimp_assert (g_type_is_a (private->container_type, GIMP_TYPE_ITEM_STACK));
  gimp_assert (g_type_is_a (private->item_type,      GIMP_TYPE_ITEM));
  gimp_assert (private->item_type != GIMP_TYPE_ITEM);

  tree->container = g_object_new (private->container_type,
                                  "name",          g_type_name (private->item_type),
                                  "children-type", private->item_type,
                                  "policy",        GIMP_CONTAINER_POLICY_STRONG,
                                  NULL);
}

static void
gimp_item_tree_finalize (GObject *object)
{
  GimpItemTree        *tree    = GIMP_ITEM_TREE (object);
  GimpItemTreePrivate *private = GIMP_ITEM_TREE_GET_PRIVATE (tree);

  g_clear_pointer (&private->name_hash, g_hash_table_unref);
  g_clear_object (&tree->container);

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

static void
gimp_item_tree_set_property (GObject      *object,
                             guint         property_id,
                             const GValue *value,
                             GParamSpec   *pspec)
{
  GimpItemTreePrivate *private = GIMP_ITEM_TREE_GET_PRIVATE (object);

  switch (property_id)
    {
    case PROP_IMAGE:
      private->image = g_value_get_object (value); /* don't ref */
      break;
    case PROP_CONTAINER_TYPE:
      private->container_type = g_value_get_gtype (value);
      break;
    case PROP_ITEM_TYPE:
      private->item_type = g_value_get_gtype (value);
      break;
    case PROP_ACTIVE_ITEM:
      private->active_item = g_value_get_object (value); /* don't ref */
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gimp_item_tree_get_property (GObject    *object,
                             guint       property_id,
                             GValue     *value,
                             GParamSpec *pspec)
{
  GimpItemTreePrivate *private = GIMP_ITEM_TREE_GET_PRIVATE (object);

  switch (property_id)
    {
    case PROP_IMAGE:
      g_value_set_object (value, private->image);
      break;
    case PROP_CONTAINER_TYPE:
      g_value_set_gtype (value, private->container_type);
      break;
    case PROP_ITEM_TYPE:
      g_value_set_gtype (value, private->item_type);
      break;
    case PROP_ACTIVE_ITEM:
      g_value_set_object (value, private->active_item);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static gint64
gimp_item_tree_get_memsize (GimpObject *object,
                            gint64     *gui_size)
{
  GimpItemTree *tree    = GIMP_ITEM_TREE (object);
  gint64        memsize = 0;

  memsize += gimp_object_get_memsize (GIMP_OBJECT (tree->container), gui_size);

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


/*  public functions  */

GimpItemTree *
gimp_item_tree_new (GimpImage *image,
                    GType      container_type,
                    GType      item_type)
{
  g_return_val_if_fail (GIMP_IS_IMAGE (image), NULL);
  g_return_val_if_fail (g_type_is_a (container_type, GIMP_TYPE_ITEM_STACK), NULL);
  g_return_val_if_fail (g_type_is_a (item_type, GIMP_TYPE_ITEM), NULL);

  return g_object_new (GIMP_TYPE_ITEM_TREE,
                       "image",          image,
                       "container-type", container_type,
                       "item-type",      item_type,
                       NULL);
}

GimpItem *
gimp_item_tree_get_active_item (GimpItemTree *tree)
{
  g_return_val_if_fail (GIMP_IS_ITEM_TREE (tree), NULL);

  return GIMP_ITEM_TREE_GET_PRIVATE (tree)->active_item;
}

void
gimp_item_tree_set_active_item (GimpItemTree *tree,
                                GimpItem     *item)
{
  GimpItemTreePrivate *private;

  g_return_if_fail (GIMP_IS_ITEM_TREE (tree));

  private = GIMP_ITEM_TREE_GET_PRIVATE (tree);

  g_return_if_fail (item == NULL ||
                    G_TYPE_CHECK_INSTANCE_TYPE (item, private->item_type));
  g_return_if_fail (item == NULL || gimp_item_get_tree (item) == tree);

  if (item != private->active_item)
    {
      private->active_item = item;

      g_object_notify (G_OBJECT (tree), "active-item");
    }
}

GimpItem *
gimp_item_tree_get_item_by_name (GimpItemTree *tree,
                                 const gchar  *name)
{
  g_return_val_if_fail (GIMP_IS_ITEM_TREE (tree), NULL);
  g_return_val_if_fail (name != NULL, NULL);

  return g_hash_table_lookup (GIMP_ITEM_TREE_GET_PRIVATE (tree)->name_hash,
                              name);
}

gboolean
gimp_item_tree_get_insert_pos (GimpItemTree  *tree,
                               GimpItem      *item,
                               GimpItem     **parent,
                               gint          *position)
{
  GimpItemTreePrivate *private;
  GimpContainer       *container;

  g_return_val_if_fail (GIMP_IS_ITEM_TREE (tree), FALSE);
  g_return_val_if_fail (parent != NULL, FALSE);

  private = GIMP_ITEM_TREE_GET_PRIVATE (tree);

  g_return_val_if_fail (G_TYPE_CHECK_INSTANCE_TYPE (item, private->item_type),
                        FALSE);
  g_return_val_if_fail (! gimp_item_is_attached (item), FALSE);
  g_return_val_if_fail (gimp_item_get_image (item) == private->image, FALSE);
  g_return_val_if_fail (*parent == NULL ||
                        *parent == GIMP_IMAGE_ACTIVE_PARENT ||
                        G_TYPE_CHECK_INSTANCE_TYPE (*parent, private->item_type),
                        FALSE);
  g_return_val_if_fail (*parent == NULL ||
                        *parent == GIMP_IMAGE_ACTIVE_PARENT ||
                        gimp_item_get_tree (*parent) == tree, FALSE);
  g_return_val_if_fail (*parent == NULL ||
                        *parent == GIMP_IMAGE_ACTIVE_PARENT ||
                        gimp_viewable_get_children (GIMP_VIEWABLE (*parent)),
                        FALSE);
  g_return_val_if_fail (position != NULL, FALSE);

  /*  if we want to insert in the active item's parent container  */
  if (*parent == GIMP_IMAGE_ACTIVE_PARENT)
    {
      if (private->active_item)
        {
          /*  if the active item is a branch, add to the top of that
           *  branch; add to the active item's parent container
           *  otherwise
           */
          if (gimp_viewable_get_children (GIMP_VIEWABLE (private->active_item)))
            {
              *parent   = private->active_item;
              *position = 0;
            }
          else
            {
              *parent = gimp_item_get_parent (private->active_item);
            }
        }
      else
        {
          /*  use the toplevel container if there is no active item  */
          *parent = NULL;
        }
    }

  if (*parent)
    container = gimp_viewable_get_children (GIMP_VIEWABLE (*parent));
  else
    container = tree->container;

  /*  if we want to add on top of the active item  */
  if (*position == -1)
    {
      if (private->active_item)
        *position =
          gimp_container_get_child_index (container,
                                          GIMP_OBJECT (private->active_item));

      /*  if the active item is not in the specified parent container,
       *  fall back to index 0
       */
      if (*position == -1)
        *position = 0;
    }

  /*  don't add at a non-existing index  */
  *position = CLAMP (*position, 0, gimp_container_get_n_children (container));

  return TRUE;
}

void
gimp_item_tree_add_item (GimpItemTree *tree,
                         GimpItem     *item,
                         GimpItem     *parent,
                         gint          position)
{
  GimpItemTreePrivate *private;
  GimpContainer       *container;
  GimpContainer       *children;

  g_return_if_fail (GIMP_IS_ITEM_TREE (tree));

  private = GIMP_ITEM_TREE_GET_PRIVATE (tree);

  g_return_if_fail (G_TYPE_CHECK_INSTANCE_TYPE (item, private->item_type));
  g_return_if_fail (! gimp_item_is_attached (item));
  g_return_if_fail (gimp_item_get_image (item) == private->image);
  g_return_if_fail (parent == NULL ||
                    G_TYPE_CHECK_INSTANCE_TYPE (parent, private->item_type));
  g_return_if_fail (parent == NULL || gimp_item_get_tree (parent) == tree);
  g_return_if_fail (parent == NULL ||
                    gimp_viewable_get_children (GIMP_VIEWABLE (parent)));

  gimp_item_tree_uniquefy_name (tree, item, NULL);

  children = gimp_viewable_get_children (GIMP_VIEWABLE (item));

  if (children)
    {
      GList *list = gimp_item_stack_get_item_list (GIMP_ITEM_STACK (children));

      while (list)
        {
          gimp_item_tree_uniquefy_name (tree, list->data, NULL);

          list = g_list_remove (list, list->data);
        }
    }

  if (parent)
    container = gimp_viewable_get_children (GIMP_VIEWABLE (parent));
  else
    container = tree->container;

  if (parent)
    gimp_viewable_set_parent (GIMP_VIEWABLE (item),
                              GIMP_VIEWABLE (parent));

  gimp_container_insert (container, GIMP_OBJECT (item), position);

  /*  if the item came from the undo stack, reset its "removed" state  */
  if (gimp_item_is_removed (item))
    gimp_item_unset_removed (item);
}

GimpItem *
gimp_item_tree_remove_item (GimpItemTree *tree,
                            GimpItem     *item,
                            GimpItem     *new_active)
{
  GimpItemTreePrivate *private;
  GimpItem            *parent;
  GimpContainer       *container;
  GimpContainer       *children;
  gint                 index;

  g_return_val_if_fail (GIMP_IS_ITEM_TREE (tree), NULL);

  private = GIMP_ITEM_TREE_GET_PRIVATE (tree);

  g_return_val_if_fail (G_TYPE_CHECK_INSTANCE_TYPE (item, private->item_type),
                        NULL);
  g_return_val_if_fail (gimp_item_get_tree (item) == tree, NULL);

  parent    = gimp_item_get_parent (item);
  container = gimp_item_get_container (item);
  index     = gimp_item_get_index (item);

  g_object_ref (item);

  g_hash_table_remove (private->name_hash,
                       gimp_object_get_name (item));

  children = gimp_viewable_get_children (GIMP_VIEWABLE (item));

  if (children)
    {
      GList *list = gimp_item_stack_get_item_list (GIMP_ITEM_STACK (children));

      while (list)
        {
          g_hash_table_remove (private->name_hash,
                               gimp_object_get_name (list->data));

          list = g_list_remove (list, list->data);
        }
    }

  gimp_container_remove (container, GIMP_OBJECT (item));

  if (parent)
    gimp_viewable_set_parent (GIMP_VIEWABLE (item), NULL);

  gimp_item_removed (item);

  if (! new_active)
    {
      gint n_children = gimp_container_get_n_children (container);

      if (n_children > 0)
        {
          index = CLAMP (index, 0, n_children - 1);

          new_active =
            GIMP_ITEM (gimp_container_get_child_by_index (container, index));
        }
      else if (parent)
        {
          new_active = parent;
        }
    }

  g_object_unref (item);

  return new_active;
}

gboolean
gimp_item_tree_reorder_item (GimpItemTree *tree,
                             GimpItem     *item,
                             GimpItem     *new_parent,
                             gint          new_index,
                             gboolean      push_undo,
                             const gchar  *undo_desc)
{
  GimpItemTreePrivate *private;
  GimpContainer       *container;
  GimpContainer       *new_container;
  gint                 n_items;

  g_return_val_if_fail (GIMP_IS_ITEM_TREE (tree), FALSE);

  private = GIMP_ITEM_TREE_GET_PRIVATE (tree);

  g_return_val_if_fail (G_TYPE_CHECK_INSTANCE_TYPE (item, private->item_type),
                        FALSE);
  g_return_val_if_fail (gimp_item_get_tree (item) == tree, FALSE);
  g_return_val_if_fail (new_parent == NULL ||
                        G_TYPE_CHECK_INSTANCE_TYPE (new_parent,
                                                    private->item_type),
                        FALSE);
  g_return_val_if_fail (new_parent == NULL ||
                        gimp_item_get_tree (new_parent) == tree, FALSE);
  g_return_val_if_fail (new_parent == NULL ||
                        gimp_viewable_get_children (GIMP_VIEWABLE (new_parent)),
                        FALSE);
  g_return_val_if_fail (item != new_parent, FALSE);
  g_return_val_if_fail (new_parent == NULL ||
                        ! gimp_viewable_is_ancestor (GIMP_VIEWABLE (item),
                                                     GIMP_VIEWABLE (new_parent)),
                        FALSE);

  container = gimp_item_get_container (item);

  if (new_parent)
    new_container = gimp_viewable_get_children (GIMP_VIEWABLE (new_parent));
  else
    new_container = tree->container;

  n_items = gimp_container_get_n_children (new_container);

  if (new_container == container)
    n_items--;

  new_index = CLAMP (new_index, 0, n_items);

  if (new_container != container ||
      new_index     != gimp_item_get_index (item))
    {
      if (push_undo)
        gimp_image_undo_push_item_reorder (private->image, undo_desc, item);

      if (new_container != container)
        {
          g_object_ref (item);

          gimp_container_remove (container, GIMP_OBJECT (item));

          gimp_viewable_set_parent (GIMP_VIEWABLE (item),
                                    GIMP_VIEWABLE (new_parent));

          gimp_container_insert (new_container, GIMP_OBJECT (item), new_index);

          g_object_unref (item);
        }
      else
        {
          gimp_container_reorder (container, GIMP_OBJECT (item), new_index);
        }
    }

  return TRUE;
}

void
gimp_item_tree_rename_item (GimpItemTree *tree,
                            GimpItem     *item,
                            const gchar  *new_name,
                            gboolean      push_undo,
                            const gchar  *undo_desc)
{
  GimpItemTreePrivate *private;

  g_return_if_fail (GIMP_IS_ITEM_TREE (tree));

  private = GIMP_ITEM_TREE_GET_PRIVATE (tree);

  g_return_if_fail (G_TYPE_CHECK_INSTANCE_TYPE (item, private->item_type));
  g_return_if_fail (gimp_item_get_tree (item) == tree);
  g_return_if_fail (new_name != NULL);

  if (strcmp (new_name, gimp_object_get_name (item)))
    {
      if (push_undo)
        gimp_image_undo_push_item_rename (gimp_item_get_image (item),
                                          undo_desc, item);

      gimp_item_tree_uniquefy_name (tree, item, new_name);
    }
}


/*  private functions  */

static void
gimp_item_tree_uniquefy_name (GimpItemTree *tree,
                              GimpItem     *item,
                              const gchar  *new_name)
{
  GimpItemTreePrivate *private = GIMP_ITEM_TREE_GET_PRIVATE (tree);

  if (new_name)
    {
      g_hash_table_remove (private->name_hash,
                           gimp_object_get_name (item));

      gimp_object_set_name (GIMP_OBJECT (item), new_name);
    }

  /* Remove any trailing whitespace. */
  if (gimp_object_get_name (item))
    {
      gchar *name = g_strchomp (g_strdup (gimp_object_get_name (item)));

      gimp_object_take_name (GIMP_OBJECT (item), name);
    }

  if (g_hash_table_lookup (private->name_hash,
                           gimp_object_get_name (item)))
    {
      gchar      *name        = g_strdup (gimp_object_get_name (item));
      gchar      *new_name    = NULL;
      gint        number      = 0;
      gint        precision   = 1;
      GRegex     *end_numbers = g_regex_new (" ?#([0-9]+)\\s*$", 0, 0, NULL);
      GMatchInfo *match_info  = NULL;

      if (g_regex_match (end_numbers, name, 0, &match_info))
        {
          gchar *match;
          gint   start_pos;

          match  = g_match_info_fetch (match_info, 1);
          if (match && match[0] == '0')
            {
              precision = strlen (match);
            }
          number = atoi (match);
          g_free (match);

          g_match_info_fetch_pos (match_info, 0,
                                  &start_pos, NULL);
          name[start_pos] = '\0';
        }
      g_match_info_free (match_info);
      g_regex_unref (end_numbers);

      do
        {
          number++;

          g_free (new_name);

          new_name = g_strdup_printf ("%s #%.*d",
                                      name,
                                      precision,
                                      number);
        }
      while (g_hash_table_lookup (private->name_hash, new_name));

      g_free (name);

      gimp_object_take_name (GIMP_OBJECT (item), new_name);
    }

  g_hash_table_insert (private->name_hash,
                       (gpointer) gimp_object_get_name (item),
                       item);
}
