/*
 * glade-gtk-table.c - GladeWidgetAdaptor for GtkTable widget
 *
 * Copyright (C) 2008 Tristan Van Berkom
 *
 * Author(s):
 *      Tristan Van Berkom <tvb@gnome.org>
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public 
 * License along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include <config.h>

#include <gtk/gtk.h>
#include <glib/gi18n-lib.h>
#include <string.h>

#include "glade-fixed.h"

typedef struct
{
  /* comparable part: */
  GladeWidget *widget;
  gint left_attach;
  gint right_attach;
  gint top_attach;
  gint bottom_attach;
} GladeGtkTableChild;

typedef enum
{
  DIR_UP,
  DIR_DOWN,
  DIR_LEFT,
  DIR_RIGHT
} GladeTableDir;

typedef enum
{
  GROUP_ACTION_INSERT_ROW,
  GROUP_ACTION_INSERT_COLUMN,
  GROUP_ACTION_REMOVE_COLUMN,
  GROUP_ACTION_REMOVE_ROW
} GroupAction;

/* Redefine GTK_TABLE() macro, as GtkTable is deprecated */
#undef GTK_TABLE
#define GTK_TABLE(obj) ((GtkTable *)obj)

static void
glade_gtk_table_get_child_attachments (GtkWidget     *table,
                                       GtkWidget     *child,
                                       GtkTableChild *tchild)
{
  guint left, right, top, bottom;

  gtk_container_child_get (GTK_CONTAINER (table), child,
                           "left-attach", (guint *) & left,
                           "right-attach", (guint *) & right,
                           "bottom-attach", (guint *) & bottom,
                           "top-attach", (guint *) & top, NULL);

  tchild->widget = child;
  tchild->left_attach = left;
  tchild->right_attach = right;
  tchild->top_attach = top;
  tchild->bottom_attach = bottom;
}

static gboolean
glade_gtk_table_widget_exceeds_bounds (GtkTable *table, gint n_rows, gint n_cols)
{
  GList *list, *children;
  gboolean ret = FALSE;

  children = gtk_container_get_children (GTK_CONTAINER (table));

  for (list = children; list && list->data; list = list->next)
    {
      GtkTableChild child;

      glade_gtk_table_get_child_attachments (GTK_WIDGET (table),
                                             GTK_WIDGET (list->data), &child);

      if (GLADE_IS_PLACEHOLDER (child.widget) == FALSE &&
          (child.right_attach > n_cols || child.bottom_attach > n_rows))
        {
          ret = TRUE;
          break;
        }
    }

  g_list_free (children);

  return ret;
}

#define TABLE_OCCUPIED(occmap, n_columns, col, row) \
    (occmap)[row * n_columns + col]

static void
glade_gtk_table_build_occupation_maps(GtkTable *table,
                                      guint      n_columns, 
                                      guint      n_rows,
                                      gchar    **child_map,
                                      gpointer **placeholder_map)
{
  guint i, j;
  GList *list, *children = gtk_container_get_children (GTK_CONTAINER (table));

  *child_map = g_malloc0(n_columns * n_rows * sizeof(gchar));  /* gchar is smaller than gboolean */
  *placeholder_map = g_malloc0(n_columns * n_rows * sizeof(gpointer));

  for (list = children; list && list->data; list = list->next)
    {
      GtkTableChild child;

      glade_gtk_table_get_child_attachments (GTK_WIDGET (table),
                                             GTK_WIDGET (list->data), &child);

      if (GLADE_IS_PLACEHOLDER(list->data))
        {
          /* assumption: placeholders are always attached to exactly 1 cell */
          TABLE_OCCUPIED(*placeholder_map, n_columns, child.left_attach, child.top_attach) = list->data;
        }
      else
        {
          for (i = child.left_attach; i < child.right_attach && i < n_columns; i++)
            {
              for (j = child.top_attach; j < child.bottom_attach && j < n_rows; j++)
                {
                  TABLE_OCCUPIED(*child_map, n_columns, i, j) = 1;
                }
            }
        }
    }
    g_list_free (children);
}

static void
glade_gtk_table_refresh_placeholders (GtkTable *table)
{
  guint n_columns, n_rows, i, j;
  gchar *child_map;
  gpointer *placeholder_map;

  g_object_get (table, "n-columns", &n_columns, "n-rows", &n_rows, NULL);
  glade_gtk_table_build_occupation_maps (table, n_columns, n_rows,
                                         &child_map, &placeholder_map);

  for (i = 0; i < n_columns; i++)
    {
      for (j = 0; j < n_rows; j++)
        {
          gpointer placeholder = TABLE_OCCUPIED(placeholder_map, n_columns, i, j);

          if (TABLE_OCCUPIED(child_map, n_columns, i, j))
            {
              if (placeholder)
                {
                  gtk_container_remove (GTK_CONTAINER (table), 
                                        GTK_WIDGET (placeholder));
                }
            }
          else
            {
              if (!placeholder)
                {
                  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
                  gtk_table_attach_defaults (table, 
                                             glade_placeholder_new (), 
                                             i, i + 1, j, j + 1);
                  G_GNUC_END_IGNORE_DEPRECATIONS;
                }
            }
        }
    }
  g_free(child_map);
  g_free(placeholder_map);

  if (gtk_widget_get_realized (GTK_WIDGET (table)))
    gtk_container_check_resize (GTK_CONTAINER (table));
}

static void
gtk_table_children_callback (GtkWidget *widget, gpointer client_data)
{
  GList **children;

  children = (GList **) client_data;
  *children = g_list_prepend (*children, widget);
}

GList *
glade_gtk_table_get_children (GladeWidgetAdaptor *adaptor,
                              GtkContainer       *container)
{
  GList *children = NULL;

  gtk_container_forall (container, gtk_table_children_callback, &children);

  /* GtkTable has the children list already reversed */
  return children;
}

void
glade_gtk_table_add_child (GladeWidgetAdaptor *adaptor,
                           GObject            *object,
                           GObject            *child)
{
  gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child));

  glade_gtk_table_refresh_placeholders (GTK_TABLE (object));
}

void
glade_gtk_table_remove_child (GladeWidgetAdaptor *adaptor,
                              GObject            *object,
                              GObject            *child)
{
  gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child));

  glade_gtk_table_refresh_placeholders (GTK_TABLE (object));
}

void
glade_gtk_table_replace_child (GladeWidgetAdaptor *adaptor,
                               GtkWidget          *container,
                               GtkWidget          *current,
                               GtkWidget          *new_widget)
{
  /* Chain Up */
  GLADE_WIDGET_ADAPTOR_GET_ADAPTOR_CLASS
      (GTK_TYPE_CONTAINER)->replace_child (adaptor,
                                           G_OBJECT (container),
                                           G_OBJECT (current),
                                           G_OBJECT (new_widget));

  /* If we are replacing a GladeWidget, we must refresh placeholders
   * because the widget may have spanned multiple rows/columns, we must
   * not do so in the case we are pasting multiple widgets into a table,
   * where destroying placeholders results in default packing properties
   * (since the remaining placeholder templates no longer exist, only the
   * first pasted widget would have proper packing properties).
   */
  if (!GLADE_IS_PLACEHOLDER (new_widget))
    glade_gtk_table_refresh_placeholders (GTK_TABLE (container));

}

static void
glade_gtk_table_set_n_common (GObject      *object,
                              const GValue *value,
                              gboolean      for_rows)
{
  GladeWidget *widget;
  GtkTable *table;
  guint new_size, old_size, n_columns, n_rows;

  table = GTK_TABLE (object);

  g_object_get (table, "n-columns", &n_columns, "n-rows", &n_rows, NULL);

  new_size = g_value_get_uint (value);
  old_size = for_rows ? n_rows : n_columns;

  if (new_size < 1)
    return;

  if (glade_gtk_table_widget_exceeds_bounds
      (table, for_rows ? new_size : n_rows, for_rows ? n_columns : new_size))
    /* Refuse to shrink if it means orphaning widgets */
    return;

  widget = glade_widget_get_from_gobject (GTK_WIDGET (table));
  g_return_if_fail (widget != NULL);

  G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
  if (for_rows)
    gtk_table_resize (table, new_size, n_columns);
  else
    gtk_table_resize (table, n_rows, new_size);
  G_GNUC_END_IGNORE_DEPRECATIONS;

  /* Fill table with placeholders */
  glade_gtk_table_refresh_placeholders (table);

  if (new_size < old_size)
    {
      /* Remove from the bottom up */
      GList *list, *children;
      GList *list_to_free = NULL;

      children = gtk_container_get_children (GTK_CONTAINER (table));

      for (list = children; list && list->data; list = list->next)
        {
          GtkTableChild child;
          guint start, end;

          glade_gtk_table_get_child_attachments (GTK_WIDGET (table),
                                                 GTK_WIDGET (list->data),
                                                 &child);

          start = for_rows ? child.top_attach : child.left_attach;
          end = for_rows ? child.bottom_attach : child.right_attach;

          /* We need to completely remove it */
          if (start >= new_size)
            {
              list_to_free = g_list_prepend (list_to_free, child.widget);
              continue;
            }

          /* If the widget spans beyond the new border,
           * we should resize it to fit on the new table */
          if (end > new_size)
            gtk_container_child_set
                (GTK_CONTAINER (table), GTK_WIDGET (child.widget),
                 for_rows ? "bottom_attach" : "right_attach", new_size, NULL);
        }

      g_list_free (children);

      if (list_to_free)
        {
          for (list = g_list_first (list_to_free);
               list && list->data; list = list->next)
            {
              g_object_ref (G_OBJECT (list->data));
              gtk_container_remove (GTK_CONTAINER (table),
                                    GTK_WIDGET (list->data));
              /* This placeholder is no longer valid, force destroy */
              gtk_widget_destroy (GTK_WIDGET (list->data));
            }
          g_list_free (list_to_free);
        }

      G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
      gtk_table_resize (table,
                        for_rows ? new_size : n_rows,
                        for_rows ? n_columns : new_size);
      G_GNUC_END_IGNORE_DEPRECATIONS;
    }
}

void
glade_gtk_table_set_property (GladeWidgetAdaptor *adaptor,
                              GObject            *object,
                              const gchar        *id,
                              const GValue       *value)
{
  if (!strcmp (id, "n-rows"))
    glade_gtk_table_set_n_common (object, value, TRUE);
  else if (!strcmp (id, "n-columns"))
    glade_gtk_table_set_n_common (object, value, FALSE);
  else
    GLADE_WIDGET_ADAPTOR_GET_ADAPTOR_CLASS (GTK_TYPE_CONTAINER)->set_property (adaptor, object,
                                                      id, value);
}

static gboolean
glade_gtk_table_verify_n_common (GObject      *object,
                                 const GValue *value,
                                 gboolean      for_rows)
{
  GtkTable *table = GTK_TABLE (object);
  guint n_columns, n_rows, new_size = g_value_get_uint (value);

  g_object_get (table, "n-columns", &n_columns, "n-rows", &n_rows, NULL);

  if (glade_gtk_table_widget_exceeds_bounds
      (table, for_rows ? new_size : n_rows, for_rows ? n_columns : new_size))
    /* Refuse to shrink if it means orphaning widgets */
    return FALSE;

  return TRUE;
}

gboolean
glade_gtk_table_verify_property (GladeWidgetAdaptor *adaptor,
                                 GObject            *object,
                                 const gchar        *id,
                                 const GValue       *value)
{
  if (!strcmp (id, "n-rows"))
    return glade_gtk_table_verify_n_common (object, value, TRUE);
  else if (!strcmp (id, "n-columns"))
    return glade_gtk_table_verify_n_common (object, value, FALSE);
  else if (GLADE_WIDGET_ADAPTOR_GET_ADAPTOR_CLASS (GTK_TYPE_CONTAINER)->verify_property)
    GLADE_WIDGET_ADAPTOR_GET_ADAPTOR_CLASS (GTK_TYPE_CONTAINER)->verify_property (adaptor, object,
                                                         id, value);

  return TRUE;
}

void
glade_gtk_table_set_child_property (GladeWidgetAdaptor *adaptor,
                                    GObject            *container,
                                    GObject            *child,
                                    const gchar        *property_name,
                                    GValue             *value)
{
  GLADE_WIDGET_ADAPTOR_GET_ADAPTOR_CLASS
      (GTK_TYPE_CONTAINER)->child_set_property (adaptor,
                                                container, child,
                                                property_name, value);

  if (strcmp (property_name, "bottom-attach") == 0 ||
      strcmp (property_name, "left-attach") == 0 ||
      strcmp (property_name, "right-attach") == 0 ||
      strcmp (property_name, "top-attach") == 0)
    {
      /* Refresh placeholders */
      glade_gtk_table_refresh_placeholders (GTK_TABLE (container));
    }

}

static gboolean
glade_gtk_table_verify_attach_common (GObject     *object,
                                      GValue      *value,
                                      guint       *val,
                                      const gchar *prop,
                                      guint       *prop_val,
                                      const gchar *parent_prop,
                                      guint       *parent_val)
{
  GladeWidget *widget, *parent;

  widget = glade_widget_get_from_gobject (object);
  g_return_val_if_fail (GLADE_IS_WIDGET (widget), TRUE);
  parent = glade_widget_get_parent (widget);
  g_return_val_if_fail (GLADE_IS_WIDGET (parent), TRUE);

  *val = g_value_get_uint (value);
  glade_widget_property_get (widget, prop, prop_val);
  glade_widget_property_get (parent, parent_prop, parent_val);

  return FALSE;
}

static gboolean
glade_gtk_table_verify_left_top_attach (GObject     *object,
                                        GValue      *value,
                                        const gchar *prop,
                                        const gchar *parent_prop)
{
  guint val, prop_val, parent_val;

  if (glade_gtk_table_verify_attach_common (object, value, &val,
                                            prop, &prop_val,
                                            parent_prop, &parent_val))
    return FALSE;

  if (val >= parent_val || val >= prop_val)
    return FALSE;

  return TRUE;
}

static gboolean
glade_gtk_table_verify_right_bottom_attach (GObject     *object,
                                            GValue      *value,
                                            const gchar *prop,
                                            const gchar *parent_prop)
{
  guint val, prop_val, parent_val;

  if (glade_gtk_table_verify_attach_common (object, value, &val,
                                            prop, &prop_val,
                                            parent_prop, &parent_val))
    return FALSE;

  if (val <= prop_val || val > parent_val)
    return FALSE;

  return TRUE;
}

gboolean
glade_gtk_table_child_verify_property (GladeWidgetAdaptor *adaptor,
                                       GObject            *container,
                                       GObject            *child,
                                       const gchar        *id,
                                       GValue             *value)
{
  if (!strcmp (id, "left-attach"))
    return glade_gtk_table_verify_left_top_attach (child,
                                                   value,
                                                   "right-attach", "n-columns");
  else if (!strcmp (id, "right-attach"))
    return glade_gtk_table_verify_right_bottom_attach (child,
                                                       value,
                                                       "left-attach",
                                                       "n-columns");
  else if (!strcmp (id, "top-attach"))
    return glade_gtk_table_verify_left_top_attach (child,
                                                   value,
                                                   "bottom-attach", "n-rows");
  else if (!strcmp (id, "bottom-attach"))
    return glade_gtk_table_verify_right_bottom_attach (child,
                                                       value,
                                                       "top-attach", "n-rows");
  else if (GLADE_WIDGET_ADAPTOR_GET_ADAPTOR_CLASS (GTK_TYPE_CONTAINER)->child_verify_property)
    GLADE_WIDGET_ADAPTOR_GET_ADAPTOR_CLASS
        (GTK_TYPE_CONTAINER)->child_verify_property (adaptor,
                                                     container, child,
                                                     id, value);

  return TRUE;
}

static void
glade_gtk_table_child_insert_remove_action (GladeWidgetAdaptor *adaptor, 
                                            GObject            *container, 
                                            GObject            *object, 
                                            GroupAction         group_action,
                                            const gchar        *n_row_col, 
                                            const gchar        *attach1,    /* should be smaller (top/left) attachment */
                                            const gchar        *attach2,      /* should be larger (bot/right) attachment */
                                            gboolean            remove, 
                                            gboolean            after)
{
  GladeWidget *parent;
  GList *children, *l;
  gint child_pos, size, offset;

  gtk_container_child_get (GTK_CONTAINER (container),
                           GTK_WIDGET (object),
                           after ? attach2 : attach1, &child_pos, NULL);

  parent = glade_widget_get_from_gobject (container);
  switch (group_action)
    {
      case GROUP_ACTION_INSERT_ROW:
        glade_command_push_group (_("Insert Row on %s"), glade_widget_get_name (parent));
        break;
      case GROUP_ACTION_INSERT_COLUMN:
        glade_command_push_group (_("Insert Column on %s"), glade_widget_get_name (parent));
        break;
      case GROUP_ACTION_REMOVE_COLUMN:
        glade_command_push_group (_("Remove Column on %s"), glade_widget_get_name (parent));
        break;
      case GROUP_ACTION_REMOVE_ROW:
        glade_command_push_group (_("Remove Row on %s"), glade_widget_get_name (parent));
        break;
      default:
        g_assert_not_reached ();
    }

  children = glade_widget_adaptor_get_children (adaptor, container);
  /* Make sure widgets does not get destroyed */
  glade_util_list_objects_ref (children);

  glade_widget_property_get (parent, n_row_col, &size);

  if (remove)
    {
      GList *del = NULL;
      /* Remove children first */
      for (l = children; l; l = g_list_next (l))
        {
          GladeWidget *gchild = glade_widget_get_from_gobject (l->data);
          gint pos1, pos2;

          /* Skip placeholders */
          if (gchild == NULL)
            continue;

          glade_widget_pack_property_get (gchild, attach1, &pos1);
          glade_widget_pack_property_get (gchild, attach2, &pos2);
          if ((pos1 + 1 == pos2) && ((after ? pos2 : pos1) == child_pos))
            {
              del = g_list_prepend (del, gchild);
            }
        }
      if (del)
        {
          glade_command_delete (del);
          g_list_free (del);
        }
      offset = -1;
    }
  else
    {
      /* Expand the table */
      glade_command_set_property (glade_widget_get_property (parent, n_row_col),
                                  size + 1);
      offset = 1;
    }

  /* Reorder children */
  for (l = children; l; l = g_list_next (l))
    {
      GladeWidget *gchild = glade_widget_get_from_gobject (l->data);
      gint pos;

      /* Skip placeholders */
      if (gchild == NULL)
        continue;

      /* if removing, do top/left before bot/right */
      if (remove)
        {
          /* adjust top-left attachment */
          glade_widget_pack_property_get (gchild, attach1, &pos);
          if (pos > child_pos || (after && pos == child_pos))
            {
              glade_command_set_property (glade_widget_get_pack_property
                                          (gchild, attach1), pos + offset);
            }

          /* adjust bottom-right attachment */
          glade_widget_pack_property_get (gchild, attach2, &pos);
          if (pos > child_pos || (after && pos == child_pos))
            {
              glade_command_set_property (glade_widget_get_pack_property
                                          (gchild, attach2), pos + offset);
            }

        }
      /* if inserting, do bot/right before top/left */
      else
        {
          /* adjust bottom-right attachment */
          glade_widget_pack_property_get (gchild, attach2, &pos);
          if (pos > child_pos)
            {
              glade_command_set_property (glade_widget_get_pack_property
                                          (gchild, attach2), pos + offset);
            }

          /* adjust top-left attachment */
          glade_widget_pack_property_get (gchild, attach1, &pos);
          if (pos >= child_pos)
            {
              glade_command_set_property (glade_widget_get_pack_property
                                          (gchild, attach1), pos + offset);
            }
        }
    }

  if (remove)
    {
      /* Shrink the table */
      glade_command_set_property (glade_widget_get_property (parent, n_row_col),
                                  size - 1);
    }

  g_list_free_full (children, g_object_unref);

  glade_command_pop_group ();
}

void
glade_gtk_table_child_action_activate (GladeWidgetAdaptor *adaptor,
                                       GObject            *container,
                                       GObject            *object,
                                       const gchar        *action_path)
{
  if (strcmp (action_path, "insert_row/after") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_INSERT_ROW,
                                                  "n-rows", "top-attach",
                                                  "bottom-attach", FALSE, TRUE);
    }
  else if (strcmp (action_path, "insert_row/before") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_INSERT_ROW,
                                                  "n-rows", "top-attach",
                                                  "bottom-attach",
                                                  FALSE, FALSE);
    }
  else if (strcmp (action_path, "insert_column/after") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_INSERT_COLUMN,
                                                  "n-columns", "left-attach",
                                                  "right-attach", FALSE, TRUE);
    }
  else if (strcmp (action_path, "insert_column/before") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_INSERT_COLUMN,
                                                  "n-columns", "left-attach",
                                                  "right-attach", FALSE, FALSE);
    }
  else if (strcmp (action_path, "remove_column") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_REMOVE_COLUMN,
                                                  "n-columns", "left-attach",
                                                  "right-attach", TRUE, FALSE);
    }
  else if (strcmp (action_path, "remove_row") == 0)
    {
      glade_gtk_table_child_insert_remove_action (adaptor, container, object,
                                                  GROUP_ACTION_REMOVE_ROW,
                                                  "n-rows", "top-attach",
                                                  "bottom-attach", TRUE, FALSE);
    }
  else
    GLADE_WIDGET_ADAPTOR_GET_ADAPTOR_CLASS (GTK_TYPE_CONTAINER)->child_action_activate (adaptor,
                                                               container,
                                                               object,
                                                               action_path);
}
