#include <gtk/gtk.h>

/* TestItem {{{1 */

/* This utility struct is used by both the RandomMenu and MirrorMenu
 * class implementations below.
 */
typedef struct {
  GHashTable *attributes;
  GHashTable *links;
} TestItem;

static TestItem *
test_item_new (GHashTable *attributes,
               GHashTable *links)
{
  TestItem *item;

  item = g_slice_new (TestItem);
  item->attributes = g_hash_table_ref (attributes);
  item->links = g_hash_table_ref (links);

  return item;
}

static void
test_item_free (gpointer data)
{
  TestItem *item = data;

  g_hash_table_unref (item->attributes);
  g_hash_table_unref (item->links);

  g_slice_free (TestItem, item);
}

/* RandomMenu {{{1 */
#define MAX_ITEMS 10
#define TOP_ORDER 4

typedef struct {
  GMenuModel parent_instance;

  GSequence *items;
  gint order;
} RandomMenu;

typedef GMenuModelClass RandomMenuClass;

static GType random_menu_get_type (void);
G_DEFINE_TYPE (RandomMenu, random_menu, G_TYPE_MENU_MODEL);

static gboolean
random_menu_is_mutable (GMenuModel *model)
{
  return TRUE;
}

static gint
random_menu_get_n_items (GMenuModel *model)
{
  RandomMenu *menu = (RandomMenu *) model;

  return g_sequence_get_length (menu->items);
}

static void
random_menu_get_item_attributes (GMenuModel  *model,
                                 gint         position,
                                 GHashTable **table)
{
  RandomMenu *menu = (RandomMenu *) model;
  TestItem *item;

  item = g_sequence_get (g_sequence_get_iter_at_pos (menu->items, position));
  *table = g_hash_table_ref (item->attributes);
}

static void
random_menu_get_item_links (GMenuModel  *model,
                            gint         position,
                            GHashTable **table)
{
  RandomMenu *menu = (RandomMenu *) model;
  TestItem *item;

  item = g_sequence_get (g_sequence_get_iter_at_pos (menu->items, position));
  *table = g_hash_table_ref (item->links);
}

static void
random_menu_finalize (GObject *object)
{
  RandomMenu *menu = (RandomMenu *) object;

  g_sequence_free (menu->items);

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

static void
random_menu_init (RandomMenu *menu)
{
}

static void
random_menu_class_init (GMenuModelClass *class)
{
  GObjectClass *object_class = G_OBJECT_CLASS (class);

  class->is_mutable = random_menu_is_mutable;
  class->get_n_items = random_menu_get_n_items;
  class->get_item_attributes = random_menu_get_item_attributes;
  class->get_item_links = random_menu_get_item_links;

  object_class->finalize = random_menu_finalize;
}

static RandomMenu * random_menu_new (GRand *rand, gint order);

static void
random_menu_change (RandomMenu *menu,
                    GRand      *rand)
{
  gint position, removes, adds;
  GSequenceIter *point;
  gint n_items;
  gint i;

  n_items = g_sequence_get_length (menu->items);

  do
    {
      position = g_rand_int_range (rand, 0, n_items + 1);
      removes = g_rand_int_range (rand, 0, n_items - position + 1);
      adds = g_rand_int_range (rand, 0, MAX_ITEMS - (n_items - removes) + 1);
    }
  while (removes == 0 && adds == 0);

  point = g_sequence_get_iter_at_pos (menu->items, position + removes);

  if (removes)
    {
      GSequenceIter *start;

      start = g_sequence_get_iter_at_pos (menu->items, position);
      g_sequence_remove_range (start, point);
    }

  for (i = 0; i < adds; i++)
    {
      const gchar *label;
      GHashTable *links;
      GHashTable *attributes;

      attributes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_variant_unref);
      links = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_object_unref);

      if (menu->order > 0 && g_rand_boolean (rand))
        {
          RandomMenu *child;
          const gchar *subtype;

          child = random_menu_new (rand, menu->order - 1);

          if (g_rand_boolean (rand))
            {
              subtype = G_MENU_LINK_SECTION;
              /* label some section headers */
              if (g_rand_boolean (rand))
                label = "Section";
              else
                label = NULL;
            }
          else
            {
              /* label all submenus */
              subtype = G_MENU_LINK_SUBMENU;
              label = "Submenu";
            }

          g_hash_table_insert (links, g_strdup (subtype), child);
        }
      else
        /* label all terminals */
        label = "Menu Item";

      if (label)
        g_hash_table_insert (attributes, g_strdup ("label"), g_variant_ref_sink (g_variant_new_string (label)));

      g_sequence_insert_before (point, test_item_new (attributes, links));
      g_hash_table_unref (links);
      g_hash_table_unref (attributes);
    }

  g_menu_model_items_changed (G_MENU_MODEL (menu), position, removes, adds);
}

static RandomMenu *
random_menu_new (GRand *rand,
                 gint   order)
{
  RandomMenu *menu;

  menu = g_object_new (random_menu_get_type (), NULL);
  menu->items = g_sequence_new (test_item_free);
  menu->order = order;

  random_menu_change (menu, rand);

  return menu;
}

/* Test cases {{{1 */

static void assert_menu_equality (GtkContainer *container, GMenuModel   *model);

static const gchar *
get_label (GtkMenuItem *item)
{
  GList *children = gtk_container_get_children (GTK_CONTAINER (item));
  const gchar *label = NULL;

  while (children)
    {
      if (GTK_IS_CONTAINER (children->data))
        children = g_list_concat (children, gtk_container_get_children (children->data));
      else if (GTK_IS_LABEL (children->data))
        label = gtk_label_get_text (children->data);
      else if (GTK_IS_ACCEL_LABEL (children->data))
        label = gtk_accel_label_get_label (children->data);

      children = g_list_delete_link (children, children);
    }

  return label;
}

/* a bit complicated with the separators...
 *
 * with_separators are if subsections of this GMenuModel should have
 * separators inserted between them (ie: in the same sense as the
 * 'with_separators' argument to gtk_menu_shell_bind_model().
 *
 * needs_separator is true if this particular section needs to have a
 * separator before it in the case that it is non-empty.  this will be
 * defined for all subsections of a with_separators menu (except the
 * first) or in case section_header is non-%NULL.
 *
 * section_header is the label that must be inside that separator, if it
 * exists.  section_header is only non-%NULL if needs_separator is also
 * TRUE.
 */
static void
assert_section_equality (GSList      **children,
                         gboolean      with_separators,
                         gboolean      needs_separator,
                         const gchar  *section_header,
                         GMenuModel   *model)
{
  gboolean has_separator;
  GSList *our_children;
  gint i, n;

  /* Assuming that we have the possibility of showing a separator, there
   * are two valid situations:
   *
   *  - we have a separator and we have other children
   *
   *  - we have no separator and no children
   *
   * If we see a separator, we suppose that it is ours and that we will
   * encounter children.  In the case that we have no children, the
   * separator may not be ours but may rather belong to a later section.
   *
   * We therefore keep our own copy of the children GSList.  If we
   * encounter children, we will delete the links that this section is
   * responsible for and update the pass-by-reference value.  Otherwise,
   * we will leave everything alone and let the separator be accounted
   * for by a following section.
   */
  our_children = *children;
  if (needs_separator && GTK_IS_SEPARATOR_MENU_ITEM (our_children->data))
    {
       /* We accounted for the separator, at least for now, so remove it
       * from the list.
       *
       * We will check later if we should have actually had a separator
       * and compare the result to has_separator.
       */
      our_children = our_children->next;
      has_separator = TRUE;
    }
  else
    has_separator = FALSE;

  /* Now, iterate the model checking that the items in the GSList line
   * up with our expectations. */
  n = g_menu_model_get_n_items (model);
  for (i = 0; i < n; i++)
    {
      GMenuModel *subsection;
      GMenuModel *submenu;
      gchar *label = NULL;

      subsection = g_menu_model_get_item_link (model, i, G_MENU_LINK_SECTION);
      submenu = g_menu_model_get_item_link (model, i, G_MENU_LINK_SUBMENU);
      g_menu_model_get_item_attribute (model, i, G_MENU_ATTRIBUTE_LABEL, "s", &label);

      if (subsection)
        {
          g_assert (!submenu);
          assert_section_equality (&our_children,
                                   FALSE,                                /* with_separators */
                                   label || (with_separators && i > 0),  /* needs_separator */
                                   label,                                /* section_header */
                                   subsection);
          g_object_unref (subsection);
        }
      else
        {
          GtkWidget *submenu_widget;
          GtkMenuItem *item;

          /* This is a normal item.  Make sure the label is right. */
          item = our_children->data;
          our_children = g_slist_remove (our_children, item);

          /* get_label() returns "" when it ought to return NULL */
          g_assert_cmpstr (get_label (item), ==, label ? label : "");
          submenu_widget = gtk_menu_item_get_submenu (item);

          if (submenu)
            {
              g_assert (submenu_widget != NULL);
              assert_menu_equality (GTK_CONTAINER (submenu_widget), submenu);
              g_object_unref (submenu);
            }
          else
            g_assert (!submenu_widget);
        }

      g_free (label);
    }

  /* If we found a separator but visited no children then the separator
   * was not for us.  Patch that up.
   */
  if (has_separator && our_children == (*children)->next)
    {
      /* Rewind our_children to put the separator we tentatively
       * consumed back into the list.
       */
      our_children = *children;
      has_separator = FALSE;
    }

  if (our_children == *children)
    /* If we had no children then we didn't really need a separator. */
    needs_separator = FALSE;

  g_assert (needs_separator == has_separator);

  if (has_separator)
    {
      GtkWidget *contents;
      const gchar *label;

      /* We needed and had a separator and we visited a child.
       *
       * Make sure that separator was valid.
       */
      contents = gtk_bin_get_child ((*children)->data);
      if (GTK_IS_LABEL (contents))
        label = gtk_label_get_label (GTK_LABEL (contents));
      else if (GTK_IS_ACCEL_LABEL (contents))
        label = gtk_accel_label_get_label (GTK_ACCEL_LABEL (contents));
      else
        label = "";

      /* get_label() returns "" when it ought to return NULL */
      g_assert_cmpstr (label, ==, section_header ? section_header : "");

      /* our_children has already gone (possibly far) past *children, so
       * we need to free up the link that we left behind for the
       * separator in case we wanted to rewind.
       */
      g_slist_free_1 (*children);
    }

  *children = our_children;
}

/* We want to use a GSList here instead of a GList because the ->prev
 * pointer updates cause trouble with the way we speculatively deal with
 * separators by skipping over them and coming back to clean up later.
 */
static void
get_children_into_slist (GtkWidget *widget,
                         gpointer   user_data)
{
  GSList **list_ptr = user_data;

  *list_ptr = g_slist_prepend (*list_ptr, widget);
}

static void
assert_menu_equality (GtkContainer *container,
                      GMenuModel   *model)
{
  GSList *children = NULL;

  gtk_container_foreach (container, get_children_into_slist, &children);
  children = g_slist_reverse (children);

  assert_section_equality (&children, TRUE, FALSE, NULL, model);
  g_assert (children == NULL);
}

static void
test_bind_menu (void)
{
  RandomMenu *model;
  GtkWidget *menu;
  GRand *rand;
  gint i;

  gtk_init ();

  rand = g_rand_new_with_seed (g_test_rand_int ());
  model = random_menu_new (rand, TOP_ORDER);
  menu = gtk_menu_new_from_model (G_MENU_MODEL (model));
  g_object_ref_sink (menu);
  assert_menu_equality (GTK_CONTAINER (menu), G_MENU_MODEL (model));
  for (i = 0; i < 100; i++)
    {
      random_menu_change (model, rand);
      while (g_main_context_iteration (NULL, FALSE));
      assert_menu_equality (GTK_CONTAINER (menu), G_MENU_MODEL (model));
    }
  g_object_unref (model);
  g_object_unref (menu);
  g_rand_free (rand);
}
/* Epilogue {{{1 */
int
main (int argc, char **argv)
{
  g_test_init (&argc, &argv, NULL);

  g_test_add_func ("/gmenu/bind", test_bind_menu);

  return g_test_run ();
}
/* vim:set foldmethod=marker: */
