/* Menu layout in-memory data structure (a custom "DOM tree") */

/*
 * Copyright (C) 2002 - 2004 Red Hat, Inc.
 *
 * 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 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 library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include <config.h>

#include "menu-layout.h"

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#include "canonicalize.h"
#include "entry-directories.h"
#include "menu-util.h"

typedef struct MenuLayoutNodeMenu          MenuLayoutNodeMenu;
typedef struct MenuLayoutNodeRoot          MenuLayoutNodeRoot;
typedef struct MenuLayoutNodeLegacyDir     MenuLayoutNodeLegacyDir;
typedef struct MenuLayoutNodeMergeFile     MenuLayoutNodeMergeFile;
typedef struct MenuLayoutNodeDefaultLayout MenuLayoutNodeDefaultLayout;
typedef struct MenuLayoutNodeMenuname      MenuLayoutNodeMenuname;
typedef struct MenuLayoutNodeMerge         MenuLayoutNodeMerge;

struct MenuLayoutNode
{
  /* Node lists are circular, for length-one lists
   * prev/next point back to the node itself.
   */
  MenuLayoutNode *prev;
  MenuLayoutNode *next;
  MenuLayoutNode *parent;
  MenuLayoutNode *children;

  char *content;

  guint refcount : 20;
  guint type : 7;
};

struct MenuLayoutNodeRoot
{
  MenuLayoutNode node;

  char *basedir;
  char *name;

  GMainContext *main_context;

  GSList *monitors;
  GSource *monitors_idle_handler;
};

struct MenuLayoutNodeMenu
{
  MenuLayoutNode node;

  MenuLayoutNode *name_node; /* cache of the <Name> node */

  EntryDirectoryList *app_dirs;
  EntryDirectoryList *dir_dirs;
};

struct MenuLayoutNodeLegacyDir
{
  MenuLayoutNode node;

  char *prefix;
};

struct MenuLayoutNodeMergeFile
{
  MenuLayoutNode node;

  MenuMergeFileType type;
};

struct MenuLayoutNodeDefaultLayout
{
  MenuLayoutNode node;

  MenuLayoutValues layout_values;
};

struct MenuLayoutNodeMenuname
{
  MenuLayoutNode node;

  MenuLayoutValues layout_values;
};

struct MenuLayoutNodeMerge
{
  MenuLayoutNode node;

  MenuLayoutMergeType merge_type;
};

typedef struct
{
  MenuLayoutNodeEntriesChangedFunc callback;
  gpointer                         user_data;
} MenuLayoutNodeEntriesMonitor;


static inline MenuLayoutNode *
node_next (MenuLayoutNode *node)
{
  /* root nodes (no parent) never have siblings */
  if (node->parent == NULL)
    return NULL;

  /* circular list */
  if (node->next == node->parent->children)
    return NULL;

  return node->next;
}

static gboolean
menu_layout_invoke_monitors (MenuLayoutNodeRoot *nr)
{
  GSList *tmp;

  g_assert (nr->node.type == MENU_LAYOUT_NODE_ROOT);

  nr->monitors_idle_handler = NULL;

  tmp = nr->monitors;
  while (tmp != NULL)
    {
      MenuLayoutNodeEntriesMonitor *monitor = tmp->data;
      GSList                       *next    = tmp->next;

      monitor->callback ((MenuLayoutNode *) nr, monitor->user_data);

      tmp = next;
    }

  return FALSE;
}

static void
handle_entry_directory_changed (EntryDirectory *dir,
                                MenuLayoutNode *node)
{
  MenuLayoutNodeRoot *nr;

  g_assert (node->type == MENU_LAYOUT_NODE_MENU);

  nr = (MenuLayoutNodeRoot *) menu_layout_node_get_root (node);

  if (nr->monitors_idle_handler == NULL)
    {
      nr->monitors_idle_handler = g_idle_source_new ();
      g_source_set_callback (nr->monitors_idle_handler,
                             (GSourceFunc) menu_layout_invoke_monitors, nr, NULL);
      g_source_attach (nr->monitors_idle_handler, nr->main_context);
      g_source_unref (nr->monitors_idle_handler);
    }
}

static void
remove_entry_directory_list (MenuLayoutNodeMenu  *nm,
                             EntryDirectoryList **dirs)
{
  if (*dirs)
    {
      entry_directory_list_remove_monitors (*dirs,
                                            (EntryDirectoryChangedFunc) handle_entry_directory_changed,
                                            nm);
      entry_directory_list_unref (*dirs);
      *dirs = NULL;
    }
}

MenuLayoutNode *
menu_layout_node_ref (MenuLayoutNode *node)
{
  g_return_val_if_fail (node != NULL, NULL);

  node->refcount += 1;

  return node;
}

void
menu_layout_node_unref (MenuLayoutNode *node)
{
  g_return_if_fail (node != NULL);
  g_return_if_fail (node->refcount > 0);

  node->refcount -= 1;
  if (node->refcount == 0)
    {
      MenuLayoutNode *iter;

      iter = node->children;
      while (iter != NULL)
        {
          MenuLayoutNode *next = node_next (iter);

          menu_layout_node_unref (iter);

          iter = next;
        }

      if (node->type == MENU_LAYOUT_NODE_MENU)
        {
          MenuLayoutNodeMenu *nm = (MenuLayoutNodeMenu *) node;

          if (nm->name_node)
            menu_layout_node_unref (nm->name_node);

          remove_entry_directory_list (nm, &nm->app_dirs);
          remove_entry_directory_list (nm, &nm->dir_dirs);
        }
      else if (node->type == MENU_LAYOUT_NODE_LEGACY_DIR)
        {
          MenuLayoutNodeLegacyDir *legacy = (MenuLayoutNodeLegacyDir *) node;

          g_free (legacy->prefix);
        }
      else if (node->type == MENU_LAYOUT_NODE_ROOT)
        {
          MenuLayoutNodeRoot *nr = (MenuLayoutNodeRoot*) node;

          g_slist_foreach (nr->monitors, (GFunc) g_free, NULL);
          g_slist_free (nr->monitors);

          if (nr->monitors_idle_handler != NULL)
            g_source_destroy (nr->monitors_idle_handler);
          nr->monitors_idle_handler = NULL;

          if (nr->main_context != NULL)
            g_main_context_unref (nr->main_context);
          nr->main_context = NULL;

          g_free (nr->basedir);
          g_free (nr->name);
        }

      g_free (node->content);
      g_free (node);
    }
}

MenuLayoutNode *
menu_layout_node_new (MenuLayoutNodeType type)
{
  MenuLayoutNode *node;

  switch (type)
    {
    case MENU_LAYOUT_NODE_MENU:
      node = (MenuLayoutNode *) g_new0 (MenuLayoutNodeMenu, 1);
      break;

    case MENU_LAYOUT_NODE_LEGACY_DIR:
      node = (MenuLayoutNode *) g_new0 (MenuLayoutNodeLegacyDir, 1);
      break;

    case MENU_LAYOUT_NODE_ROOT:
      node = (MenuLayoutNode*) g_new0 (MenuLayoutNodeRoot, 1);
      break;

    case MENU_LAYOUT_NODE_MERGE_FILE:
      node = (MenuLayoutNode *) g_new0 (MenuLayoutNodeMergeFile, 1);
      break;

    case MENU_LAYOUT_NODE_DEFAULT_LAYOUT:
      node = (MenuLayoutNode *) g_new0 (MenuLayoutNodeDefaultLayout, 1);
      break;

    case MENU_LAYOUT_NODE_MENUNAME:
      node = (MenuLayoutNode *) g_new0 (MenuLayoutNodeMenuname, 1);
      break;

    case MENU_LAYOUT_NODE_MERGE:
      node = (MenuLayoutNode *) g_new0 (MenuLayoutNodeMerge, 1);
      break;

    default:
      node = g_new0 (MenuLayoutNode, 1);
      break;
    }

  node->type = type;

  node->refcount = 1;

  /* we're in a list of one node */
  node->next = node;
  node->prev = node;

  return node;
}

MenuLayoutNode *
menu_layout_node_get_next (MenuLayoutNode *node)
{
  return node_next (node);
}

MenuLayoutNode *
menu_layout_node_get_parent (MenuLayoutNode *node)
{
  return node->parent;
}

MenuLayoutNode *
menu_layout_node_get_children (MenuLayoutNode *node)
{
  return node->children;
}

MenuLayoutNode *
menu_layout_node_get_root (MenuLayoutNode *node)
{
  MenuLayoutNode *parent;

  parent = node;
  while (parent->parent != NULL)
    parent = parent->parent;

  g_assert (parent->type == MENU_LAYOUT_NODE_ROOT);

  return parent;
}

char *
menu_layout_node_get_content_as_path (MenuLayoutNode *node)
{
  if (node->content == NULL)
    {
      menu_verbose ("  (node has no content to get as a path)\n");
      return NULL;
    }

  if (g_path_is_absolute (node->content))
    {
      return g_strdup (node->content);
    }
  else
    {
      MenuLayoutNodeRoot *root;

      root = (MenuLayoutNodeRoot *) menu_layout_node_get_root (node);

      if (root->basedir == NULL)
        {
          menu_verbose ("No basedir available, using \"%s\" as-is\n",
                        node->content);
          return g_strdup (node->content);
        }
      else
        {
          menu_verbose ("Using basedir \"%s\" filename \"%s\"\n",
                        root->basedir, node->content);
          return g_build_filename (root->basedir, node->content, NULL);
        }
    }
}

#define RETURN_IF_NO_PARENT(node) G_STMT_START {                  \
    if ((node)->parent == NULL)                                   \
      {                                                           \
        g_warning ("To add siblings to a menu node, "             \
                   "it must not be the root node, "               \
                   "and must be linked in below some root node\n" \
                   "node parent = %p and type = %d",              \
                   (node)->parent, (node)->type);                 \
        return;                                                   \
      }                                                           \
  } G_STMT_END

#define RETURN_IF_HAS_ENTRY_DIRS(node) G_STMT_START {             \
    if ((node)->type == MENU_LAYOUT_NODE_MENU &&                  \
        (((MenuLayoutNodeMenu*)(node))->app_dirs != NULL ||       \
         ((MenuLayoutNodeMenu*)(node))->dir_dirs != NULL))        \
      {                                                           \
        g_warning ("node acquired ->app_dirs or ->dir_dirs "      \
                   "while not rooted in a tree\n");               \
        return;                                                   \
      }                                                           \
  } G_STMT_END                                                    \

void
menu_layout_node_insert_before (MenuLayoutNode *node,
                                MenuLayoutNode *new_sibling)
{
  g_return_if_fail (new_sibling != NULL);
  g_return_if_fail (new_sibling->parent == NULL);

  RETURN_IF_NO_PARENT (node);
  RETURN_IF_HAS_ENTRY_DIRS (new_sibling);

  new_sibling->next = node;
  new_sibling->prev = node->prev;

  node->prev = new_sibling;
  new_sibling->prev->next = new_sibling;

  new_sibling->parent = node->parent;

  if (node == node->parent->children)
    node->parent->children = new_sibling;

  menu_layout_node_ref (new_sibling);
}

void
menu_layout_node_insert_after (MenuLayoutNode *node,
                               MenuLayoutNode *new_sibling)
{
  g_return_if_fail (new_sibling != NULL);
  g_return_if_fail (new_sibling->parent == NULL);

  RETURN_IF_NO_PARENT (node);
  RETURN_IF_HAS_ENTRY_DIRS (new_sibling);

  new_sibling->prev = node;
  new_sibling->next = node->next;

  node->next = new_sibling;
  new_sibling->next->prev = new_sibling;

  new_sibling->parent = node->parent;

  menu_layout_node_ref (new_sibling);
}

void
menu_layout_node_prepend_child (MenuLayoutNode *parent,
                                MenuLayoutNode *new_child)
{
  RETURN_IF_HAS_ENTRY_DIRS (new_child);

  if (parent->children)
    {
      menu_layout_node_insert_before (parent->children, new_child);
    }
  else
    {
      parent->children = menu_layout_node_ref (new_child);
      new_child->parent = parent;
    }
}

void
menu_layout_node_append_child (MenuLayoutNode *parent,
                               MenuLayoutNode *new_child)
{
  RETURN_IF_HAS_ENTRY_DIRS (new_child);

  if (parent->children)
    {
      menu_layout_node_insert_after (parent->children->prev, new_child);
    }
  else
    {
      parent->children = menu_layout_node_ref (new_child);
      new_child->parent = parent;
    }
}

void
menu_layout_node_unlink (MenuLayoutNode *node)
{
  g_return_if_fail (node != NULL);
  g_return_if_fail (node->parent != NULL);

  menu_layout_node_steal (node);
  menu_layout_node_unref (node);
}

static void
recursive_clean_entry_directory_lists (MenuLayoutNode *node,
                                       gboolean        apps)
{
  EntryDirectoryList **dirs;
  MenuLayoutNodeMenu  *nm;
  MenuLayoutNode      *iter;

  if (node->type != MENU_LAYOUT_NODE_MENU)
    return;

  nm = (MenuLayoutNodeMenu *) node;

  dirs = apps ? &nm->app_dirs : &nm->dir_dirs;

  if (*dirs == NULL || entry_directory_list_get_length (*dirs) == 0)
    return; /* child menus continue to have valid lists */

  remove_entry_directory_list (nm, dirs);

  iter = node->children;
  while (iter != NULL)
    {
      if (iter->type == MENU_LAYOUT_NODE_MENU)
        recursive_clean_entry_directory_lists (iter, apps);

      iter = node_next (iter);
    }
}

void
menu_layout_node_steal (MenuLayoutNode *node)
{
  g_return_if_fail (node != NULL);
  g_return_if_fail (node->parent != NULL);

  switch (node->type)
    {
    case MENU_LAYOUT_NODE_NAME:
      {
        MenuLayoutNodeMenu *nm = (MenuLayoutNodeMenu *) node->parent;

        if (nm->name_node == node)
          {
            menu_layout_node_unref (nm->name_node);
            nm->name_node = NULL;
          }
      }
      break;

    case MENU_LAYOUT_NODE_APP_DIR:
      recursive_clean_entry_directory_lists (node->parent, TRUE);
      break;

    case MENU_LAYOUT_NODE_DIRECTORY_DIR:
      recursive_clean_entry_directory_lists (node->parent, FALSE);
      break;

    default:
      break;
    }

  if (node->parent && node->parent->children == node)
    {
      if (node->next != node)
        node->parent->children = node->next;
      else
        node->parent->children = NULL;
    }

  /* these are no-ops for length-one node lists */
  node->prev->next = node->next;
  node->next->prev = node->prev;

  node->parent = NULL;

  /* point to ourselves, now we're length one */
  node->next = node;
  node->prev = node;
}

MenuLayoutNodeType
menu_layout_node_get_type (MenuLayoutNode *node)
{
  return node->type;
}

const char *
menu_layout_node_get_content (MenuLayoutNode *node)
{
  return node->content;
}

void
menu_layout_node_set_content (MenuLayoutNode *node,
                              const char     *content)
{
  if (node->content == content)
    return;

  g_free (node->content);
  node->content = g_strdup (content);
}

const char *
menu_layout_node_root_get_name (MenuLayoutNode *node)
{
  MenuLayoutNodeRoot *nr;

  g_return_val_if_fail (node->type == MENU_LAYOUT_NODE_ROOT, NULL);

  nr = (MenuLayoutNodeRoot*) node;

  return nr->name;
}

const char *
menu_layout_node_root_get_basedir (MenuLayoutNode *node)
{
  MenuLayoutNodeRoot *nr;

  g_return_val_if_fail (node->type == MENU_LAYOUT_NODE_ROOT, NULL);

  nr = (MenuLayoutNodeRoot*) node;

  return nr->basedir;
}

const char *
menu_layout_node_menu_get_name (MenuLayoutNode *node)
{
  MenuLayoutNodeMenu *nm;

  g_return_val_if_fail (node->type == MENU_LAYOUT_NODE_MENU, NULL);

  nm = (MenuLayoutNodeMenu*) node;

  if (nm->name_node == NULL)
    {
      MenuLayoutNode *iter;

      iter = node->children;
      while (iter != NULL)
        {
          if (iter->type == MENU_LAYOUT_NODE_NAME)
            {
              nm->name_node = menu_layout_node_ref (iter);
              break;
            }

          iter = node_next (iter);
        }
    }

  if (nm->name_node == NULL)
    return NULL;

  return menu_layout_node_get_content (nm->name_node);
}

static void
ensure_dir_lists (MenuLayoutNodeMenu *nm)
{
  MenuLayoutNode     *node;
  MenuLayoutNode     *iter;
  EntryDirectoryList *app_dirs;
  EntryDirectoryList *dir_dirs;

  node = (MenuLayoutNode *) nm;

  if (nm->app_dirs && nm->dir_dirs)
    return;

  app_dirs = NULL;
  dir_dirs = NULL;

  if (nm->app_dirs == NULL)
    {
      app_dirs = entry_directory_list_new ();

      if (node->parent && node->parent->type == MENU_LAYOUT_NODE_MENU)
        {
          EntryDirectoryList *dirs;

          if ((dirs = menu_layout_node_menu_get_app_dirs (node->parent)))
            entry_directory_list_append_list (app_dirs, dirs);
        }
    }

  if (nm->dir_dirs == NULL)
    {
      dir_dirs = entry_directory_list_new ();

      if (node->parent && node->parent->type == MENU_LAYOUT_NODE_MENU)
        {
          EntryDirectoryList *dirs;

          if ((dirs = menu_layout_node_menu_get_directory_dirs (node->parent)))
            entry_directory_list_append_list (dir_dirs, dirs);
        }
    }

  iter = node->children;
  while (iter != NULL)
    {
      EntryDirectory *ed;

      if (app_dirs != NULL && iter->type == MENU_LAYOUT_NODE_APP_DIR)
        {
          char *path;

          path = menu_layout_node_get_content_as_path (iter);

          ed = entry_directory_new (DESKTOP_ENTRY_DESKTOP, path);
          if (ed != NULL)
            {
              entry_directory_list_prepend (app_dirs, ed);
              entry_directory_unref (ed);
            }

          g_free (path);
        }

      if (dir_dirs != NULL && iter->type == MENU_LAYOUT_NODE_DIRECTORY_DIR)
        {
          char *path;

          path = menu_layout_node_get_content_as_path (iter);

          ed = entry_directory_new (DESKTOP_ENTRY_DIRECTORY, path);
          if (ed != NULL)
            {
              entry_directory_list_prepend (dir_dirs, ed);
              entry_directory_unref (ed);
            }

          g_free (path);
        }

      if (iter->type == MENU_LAYOUT_NODE_LEGACY_DIR)
        {
          MenuLayoutNodeLegacyDir *legacy = (MenuLayoutNodeLegacyDir *) iter;
          char                    *path;

          path = menu_layout_node_get_content_as_path (iter);

          if (app_dirs != NULL) /* we're loading app dirs */
            {
              ed = entry_directory_new_legacy (DESKTOP_ENTRY_DESKTOP,
                                               path,
                                               legacy->prefix);
              if (ed != NULL)
                {
                  entry_directory_list_prepend (app_dirs, ed);
                  entry_directory_unref (ed);
                }
            }

          if (dir_dirs != NULL) /* we're loading dir dirs */
            {
              ed = entry_directory_new_legacy (DESKTOP_ENTRY_DIRECTORY,
                                               path,
                                               legacy->prefix);
              if (ed != NULL)
                {
                  entry_directory_list_prepend (dir_dirs, ed);
                  entry_directory_unref (ed);
                }
            }

          g_free (path);
        }

      iter = node_next (iter);
    }

  if (app_dirs)
    {
      g_assert (nm->app_dirs == NULL);

      nm->app_dirs = app_dirs;
      entry_directory_list_add_monitors (nm->app_dirs,
                                         (EntryDirectoryChangedFunc) handle_entry_directory_changed,
                                         nm);
    }

  if (dir_dirs)
    {
      g_assert (nm->dir_dirs == NULL);

      nm->dir_dirs = dir_dirs;
      entry_directory_list_add_monitors (nm->dir_dirs,
                                         (EntryDirectoryChangedFunc) handle_entry_directory_changed,
                                         nm);
    }
}

EntryDirectoryList *
menu_layout_node_menu_get_app_dirs (MenuLayoutNode *node)
{
  MenuLayoutNodeMenu *nm;

  g_return_val_if_fail (node->type == MENU_LAYOUT_NODE_MENU, NULL);

  nm = (MenuLayoutNodeMenu *) node;

  ensure_dir_lists (nm);

  return nm->app_dirs;
}

EntryDirectoryList *
menu_layout_node_menu_get_directory_dirs (MenuLayoutNode *node)
{
  MenuLayoutNodeMenu *nm;

  g_return_val_if_fail (node->type == MENU_LAYOUT_NODE_MENU, NULL);

  nm = (MenuLayoutNodeMenu *) node;

  ensure_dir_lists (nm);

  return nm->dir_dirs;
}

const char *
menu_layout_node_move_get_old (MenuLayoutNode *node)
{
  MenuLayoutNode *iter;

  iter = node->children;
  while (iter != NULL)
    {
      if (iter->type == MENU_LAYOUT_NODE_OLD)
        return iter->content;

      iter = node_next (iter);
    }

  return NULL;
}

const char *
menu_layout_node_move_get_new (MenuLayoutNode *node)
{
  MenuLayoutNode *iter;

  iter = node->children;
  while (iter != NULL)
    {
      if (iter->type == MENU_LAYOUT_NODE_NEW)
        return iter->content;

      iter = node_next (iter);
    }

  return NULL;
}

const char *
menu_layout_node_legacy_dir_get_prefix (MenuLayoutNode *node)
{
  MenuLayoutNodeLegacyDir *legacy;

  g_return_val_if_fail (node->type == MENU_LAYOUT_NODE_LEGACY_DIR, NULL);

  legacy = (MenuLayoutNodeLegacyDir *) node;

  return legacy->prefix;
}

void
menu_layout_node_legacy_dir_set_prefix (MenuLayoutNode *node,
                                        const char     *prefix)
{
  MenuLayoutNodeLegacyDir *legacy;

  g_return_if_fail (node->type == MENU_LAYOUT_NODE_LEGACY_DIR);

  legacy = (MenuLayoutNodeLegacyDir *) node;

  g_free (legacy->prefix);
  legacy->prefix = g_strdup (prefix);
}

MenuMergeFileType
menu_layout_node_merge_file_get_type (MenuLayoutNode *node)
{
  MenuLayoutNodeMergeFile *merge_file;

  g_return_val_if_fail (node->type == MENU_LAYOUT_NODE_MERGE_FILE, FALSE);

  merge_file = (MenuLayoutNodeMergeFile *) node;

  return merge_file->type;
}

void
menu_layout_node_merge_file_set_type (MenuLayoutNode    *node,
				      MenuMergeFileType  type)
{
  MenuLayoutNodeMergeFile *merge_file;

  g_return_if_fail (node->type == MENU_LAYOUT_NODE_MERGE_FILE);

  merge_file = (MenuLayoutNodeMergeFile *) node;

  merge_file->type = type;
}

MenuLayoutMergeType
menu_layout_node_merge_get_type (MenuLayoutNode *node)
{
  MenuLayoutNodeMerge *merge;

  g_return_val_if_fail (node->type == MENU_LAYOUT_NODE_MERGE, 0);

  merge = (MenuLayoutNodeMerge *) node;

  return merge->merge_type;
}

static void
menu_layout_node_merge_set_type (MenuLayoutNode *node,
                                 const char     *merge_type)
{
  MenuLayoutNodeMerge *merge;

  g_return_if_fail (node->type == MENU_LAYOUT_NODE_MERGE);

  merge = (MenuLayoutNodeMerge *) node;

  merge->merge_type = MENU_LAYOUT_MERGE_NONE;

  if (strcmp (merge_type, "menus") == 0)
    {
      merge->merge_type = MENU_LAYOUT_MERGE_MENUS;
    }
  else if (strcmp (merge_type, "files") == 0)
    {
      merge->merge_type = MENU_LAYOUT_MERGE_FILES;
    }
  else if (strcmp (merge_type, "all") == 0)
    {
      merge->merge_type = MENU_LAYOUT_MERGE_ALL;
    }
}

void
menu_layout_node_default_layout_get_values (MenuLayoutNode   *node,
					    MenuLayoutValues *values)
{
  MenuLayoutNodeDefaultLayout *default_layout;

  g_return_if_fail (node->type == MENU_LAYOUT_NODE_DEFAULT_LAYOUT);
  g_return_if_fail (values != NULL);

  default_layout = (MenuLayoutNodeDefaultLayout *) node;

  *values = default_layout->layout_values;
}

void
menu_layout_node_menuname_get_values (MenuLayoutNode   *node,
				      MenuLayoutValues *values)
{
  MenuLayoutNodeMenuname *menuname;

  g_return_if_fail (node->type == MENU_LAYOUT_NODE_MENUNAME);
  g_return_if_fail (values != NULL);

  menuname = (MenuLayoutNodeMenuname *) node;

  *values = menuname->layout_values;
}

static void
menu_layout_values_set (MenuLayoutValues *values,
			const char       *show_empty,
			const char       *inline_menus,
			const char       *inline_limit,
			const char       *inline_header,
			const char       *inline_alias)
{
  values->mask          = MENU_LAYOUT_VALUES_NONE;
  values->show_empty    = FALSE;
  values->inline_menus  = FALSE;
  values->inline_limit  = 4;
  values->inline_header = FALSE;
  values->inline_alias  = FALSE;

  if (show_empty != NULL)
    {
      values->show_empty = strcmp (show_empty, "true") == 0;
      values->mask |= MENU_LAYOUT_VALUES_SHOW_EMPTY;
    }

  if (inline_menus != NULL)
    {
      values->inline_menus = strcmp (inline_menus, "true") == 0;
      values->mask |= MENU_LAYOUT_VALUES_INLINE_MENUS;
    }
  
  if (inline_limit != NULL)
    {
      char *end;
      int   limit;

      limit = strtol (inline_limit, &end, 10);
      if (*end == '\0')
	{
	  values->inline_limit = limit;
	  values->mask |= MENU_LAYOUT_VALUES_INLINE_LIMIT;
	}
    }

  if (inline_header != NULL)
    {
      values->inline_header = strcmp (inline_header, "true") == 0;
      values->mask |= MENU_LAYOUT_VALUES_INLINE_HEADER;
    }

  if (inline_alias != NULL)
    {
      values->inline_alias = strcmp (inline_alias, "true") == 0;
      values->mask |= MENU_LAYOUT_VALUES_INLINE_ALIAS;
    }
}

static void
menu_layout_node_default_layout_set_values (MenuLayoutNode *node,
					    const char     *show_empty,
					    const char     *inline_menus,
					    const char     *inline_limit,
					    const char     *inline_header,
					    const char     *inline_alias)
{
  MenuLayoutNodeDefaultLayout *default_layout;

  g_return_if_fail (node->type == MENU_LAYOUT_NODE_DEFAULT_LAYOUT);

  default_layout = (MenuLayoutNodeDefaultLayout *) node;

  menu_layout_values_set (&default_layout->layout_values,
			  show_empty,
			  inline_menus,
			  inline_limit,
			  inline_header,
			  inline_alias);
}

static void
menu_layout_node_menuname_set_values (MenuLayoutNode *node,
				      const char     *show_empty,
				      const char     *inline_menus,
				      const char     *inline_limit,
				      const char     *inline_header,
				      const char     *inline_alias)
{
  MenuLayoutNodeMenuname *menuname;

  g_return_if_fail (node->type == MENU_LAYOUT_NODE_MENUNAME);

  menuname = (MenuLayoutNodeMenuname *) node;

  menu_layout_values_set (&menuname->layout_values,
			  show_empty,
			  inline_menus,
			  inline_limit,
			  inline_header,
			  inline_alias);
}

void
menu_layout_node_root_add_entries_monitor (MenuLayoutNode                   *node,
                                           MenuLayoutNodeEntriesChangedFunc  callback,
                                           gpointer                          user_data)
{
  MenuLayoutNodeEntriesMonitor *monitor;
  MenuLayoutNodeRoot           *nr;
  GSList                       *tmp;

  g_return_if_fail (node->type == MENU_LAYOUT_NODE_ROOT);

  nr = (MenuLayoutNodeRoot *) node;

  tmp = nr->monitors;
  while (tmp != NULL)
    {
      monitor = tmp->data;

      if (monitor->callback  == callback &&
          monitor->user_data == user_data)
        break;

      tmp = tmp->next;
    }

  if (tmp == NULL)
    {
      monitor            = g_new0 (MenuLayoutNodeEntriesMonitor, 1);
      monitor->callback  = callback;
      monitor->user_data = user_data;

      nr->monitors = g_slist_append (nr->monitors, monitor);
    }
}

void
menu_layout_node_root_remove_entries_monitor (MenuLayoutNode                   *node,
                                              MenuLayoutNodeEntriesChangedFunc  callback,
                                              gpointer                          user_data)
{
  MenuLayoutNodeRoot *nr;
  GSList             *tmp;

  g_return_if_fail (node->type == MENU_LAYOUT_NODE_ROOT);

  nr = (MenuLayoutNodeRoot *) node;

  tmp = nr->monitors;
  while (tmp != NULL)
    {
      MenuLayoutNodeEntriesMonitor *monitor = tmp->data;
      GSList                       *next = tmp->next;

      if (monitor->callback == callback &&
          monitor->user_data == user_data)
        {
          nr->monitors = g_slist_delete_link (nr->monitors, tmp);
          g_free (monitor);
        }

      tmp = next;
    }
}


/*
 * Menu file parsing
 */

typedef struct
{
  MenuLayoutNode *root;
  MenuLayoutNode *stack_top;
} MenuParser;

static void set_error (GError             **err,
                       GMarkupParseContext *context,
                       int                  error_domain,
                       int                  error_code,
                       const char          *format,
                       ...) G_GNUC_PRINTF (5, 6);

static void add_context_to_error (GError             **err,
                                  GMarkupParseContext *context);

static void start_element_handler (GMarkupParseContext  *context,
                                   const char           *element_name,
                                   const char          **attribute_names,
                                   const char          **attribute_values,
                                   gpointer              user_data,
                                   GError              **error);
static void end_element_handler   (GMarkupParseContext  *context,
                                   const char           *element_name,
                                   gpointer              user_data,
                                   GError              **error);
static void text_handler          (GMarkupParseContext  *context,
                                   const char           *text,
                                   gsize                 text_len,
                                   gpointer              user_data,
                                   GError              **error);
static void passthrough_handler   (GMarkupParseContext  *context,
                                   const char           *passthrough_text,
                                   gsize                 text_len,
                                   gpointer              user_data,
                                   GError              **error);


static GMarkupParser menu_funcs = {
  start_element_handler,
  end_element_handler,
  text_handler,
  passthrough_handler,
  NULL
};

static void
set_error (GError              **err,
           GMarkupParseContext  *context,
           int                   error_domain,
           int                   error_code,
           const char           *format,
           ...)
{
  int      line, ch;
  va_list  args;
  char    *str;

  g_markup_parse_context_get_position (context, &line, &ch);

  va_start (args, format);
  str = g_strdup_vprintf (format, args);
  va_end (args);

  g_set_error (err, error_domain, error_code,
               "Line %d character %d: %s",
               line, ch, str);

  g_free (str);
}

static void
add_context_to_error (GError             **err,
                      GMarkupParseContext *context)
{
  int   line, ch;
  char *str;

  if (err == NULL || *err == NULL)
    return;

  g_markup_parse_context_get_position (context, &line, &ch);

  str = g_strdup_printf ("Line %d character %d: %s",
                         line, ch, (*err)->message);
  g_free ((*err)->message);
  (*err)->message = str;
}

#define ELEMENT_IS(name) (strcmp (element_name, (name)) == 0)

typedef struct
{
  const char  *name;
  const char **retloc;
} LocateAttr;

static gboolean
locate_attributes (GMarkupParseContext  *context,
                   const char           *element_name,
                   const char          **attribute_names,
                   const char          **attribute_values,
                   GError              **error,
                   const char           *first_attribute_name,
                   const char          **first_attribute_retloc,
                   ...)
{
#define MAX_ATTRS 24
  LocateAttr   attrs[MAX_ATTRS];
  int          n_attrs;
  va_list      args;
  const char  *name;
  const char **retloc;
  gboolean     retval;
  int          i;

  g_return_val_if_fail (first_attribute_name != NULL, FALSE);
  g_return_val_if_fail (first_attribute_retloc != NULL, FALSE);

  retval = TRUE;

  n_attrs = 1;
  attrs[0].name = first_attribute_name;
  attrs[0].retloc = first_attribute_retloc;
  *first_attribute_retloc = NULL;

  va_start (args, first_attribute_retloc);

  name = va_arg (args, const char *);
  retloc = va_arg (args, const char **);

  while (name != NULL)
    {
      g_return_val_if_fail (retloc != NULL, FALSE);

      g_assert (n_attrs < MAX_ATTRS);

      attrs[n_attrs].name = name;
      attrs[n_attrs].retloc = retloc;
      n_attrs += 1;
      *retloc = NULL;

      name = va_arg (args, const char *);
      retloc = va_arg (args, const char **);
    }

  va_end (args);

  i = 0;
  while (attribute_names[i])
    {
      int j;

      j = 0;
      while (j < n_attrs)
        {
          if (strcmp (attrs[j].name, attribute_names[i]) == 0)
            {
              retloc = attrs[j].retloc;

              if (*retloc != NULL)
                {
                  set_error (error, context,
                             G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                             "Attribute \"%s\" repeated twice on the same <%s> element",
                             attrs[j].name, element_name);
                  retval = FALSE;
                  goto out;
                }

              *retloc = attribute_values[i];
              break;
            }

          ++j;
        }

      if (j == n_attrs)
        {
          set_error (error, context,
                     G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                     "Attribute \"%s\" is invalid on <%s> element in this context",
                     attribute_names[i], element_name);
          retval = FALSE;
          goto out;
        }

      ++i;
    }

 out:
  return retval;

#undef MAX_ATTRS
}

static gboolean
check_no_attributes (GMarkupParseContext  *context,
                     const char           *element_name,
                     const char          **attribute_names,
                     const char          **attribute_values,
                     GError              **error)
{
  if (attribute_names[0] != NULL)
    {
      set_error (error, context,
                 G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                 "Attribute \"%s\" is invalid on <%s> element in this context",
                 attribute_names[0], element_name);
      return FALSE;
    }

  return TRUE;
}

static int
has_child_of_type (MenuLayoutNode     *node,
                   MenuLayoutNodeType  type)
{
  MenuLayoutNode *iter;

  iter = node->children;
  while (iter)
    {
      if (iter->type == type)
        return TRUE;

      iter = node_next (iter);
    }

  return FALSE;
}

static void
push_node (MenuParser         *parser,
           MenuLayoutNodeType  type)
{
  MenuLayoutNode *node;

  node = menu_layout_node_new (type);
  menu_layout_node_append_child (parser->stack_top, node);
  menu_layout_node_unref (node);

  parser->stack_top = node;
}

static void
start_menu_element (MenuParser           *parser,
                    GMarkupParseContext  *context,
                    const char           *element_name,
                    const char          **attribute_names,
                    const char          **attribute_values,
                    GError              **error)
{
  if (!check_no_attributes (context, element_name,
                            attribute_names, attribute_values,
                            error))
    return;

  if (!(parser->stack_top->type == MENU_LAYOUT_NODE_ROOT ||
        parser->stack_top->type == MENU_LAYOUT_NODE_MENU))
    {
      set_error (error, context,
                 G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                 "<Menu> element can only appear below other <Menu> elements or at toplevel\n");
    }
  else
    {
      push_node (parser, MENU_LAYOUT_NODE_MENU);
    }
}

static void
start_menu_child_element (MenuParser           *parser,
                          GMarkupParseContext  *context,
                          const char           *element_name,
                          const char          **attribute_names,
                          const char          **attribute_values,
                          GError              **error)
{
  if (ELEMENT_IS ("LegacyDir"))
    {
      const char *prefix;

      push_node (parser, MENU_LAYOUT_NODE_LEGACY_DIR);

      if (!locate_attributes (context, element_name,
                              attribute_names, attribute_values,
                              error,
                              "prefix", &prefix,
                              NULL))
        return;

      menu_layout_node_legacy_dir_set_prefix (parser->stack_top, prefix);
    }
  else if (ELEMENT_IS ("MergeFile"))
    {
      const char *type;

      push_node (parser, MENU_LAYOUT_NODE_MERGE_FILE);

      if (!locate_attributes (context, element_name,
                              attribute_names, attribute_values,
                              error,
                              "type", &type,
                              NULL))
        return;

      if (type != NULL && strcmp (type, "parent") == 0)
	{
	  menu_layout_node_merge_file_set_type (parser->stack_top,
						MENU_MERGE_FILE_TYPE_PARENT);
	}
    }
  else if (ELEMENT_IS ("DefaultLayout"))
    {
      const char *show_empty;
      const char *inline_menus;
      const char *inline_limit;
      const char *inline_header;
      const char *inline_alias;

      push_node (parser, MENU_LAYOUT_NODE_DEFAULT_LAYOUT);

      locate_attributes (context, element_name,
                         attribute_names, attribute_values,
                         error,
                         "show_empty",    &show_empty,
                         "inline",        &inline_menus,
                         "inline_limit",  &inline_limit,
                         "inline_header", &inline_header,
                         "inline_alias",  &inline_alias,
                         NULL);

      menu_layout_node_default_layout_set_values (parser->stack_top,
						  show_empty,
						  inline_menus,
						  inline_limit,
						  inline_header,
						  inline_alias);
    }
  else
    {
      if (!check_no_attributes (context, element_name,
                                attribute_names, attribute_values,
                                error))
        return;

      if (ELEMENT_IS ("AppDir"))
        {
          push_node (parser, MENU_LAYOUT_NODE_APP_DIR);
        }
      else if (ELEMENT_IS ("DefaultAppDirs"))
        {
          push_node (parser, MENU_LAYOUT_NODE_DEFAULT_APP_DIRS);
        }
      else if (ELEMENT_IS ("DirectoryDir"))
        {
          push_node (parser, MENU_LAYOUT_NODE_DIRECTORY_DIR);
        }
      else if (ELEMENT_IS ("DefaultDirectoryDirs"))
        {
          push_node (parser, MENU_LAYOUT_NODE_DEFAULT_DIRECTORY_DIRS);
        }
      else if (ELEMENT_IS ("DefaultMergeDirs"))
        {
          push_node (parser, MENU_LAYOUT_NODE_DEFAULT_MERGE_DIRS);
        }
      else if (ELEMENT_IS ("Name"))
        {
          if (has_child_of_type (parser->stack_top, MENU_LAYOUT_NODE_NAME))
            {
              set_error (error, context,
                         G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                         "Multiple <Name> elements in a <Menu> element is not allowed\n");
              return;
            }

          push_node (parser, MENU_LAYOUT_NODE_NAME);
        }
      else if (ELEMENT_IS ("Directory"))
        {
          push_node (parser, MENU_LAYOUT_NODE_DIRECTORY);
        }
      else if (ELEMENT_IS ("OnlyUnallocated"))
        {
          push_node (parser, MENU_LAYOUT_NODE_ONLY_UNALLOCATED);
        }
      else if (ELEMENT_IS ("NotOnlyUnallocated"))
        {
          push_node (parser, MENU_LAYOUT_NODE_NOT_ONLY_UNALLOCATED);
        }
      else if (ELEMENT_IS ("Include"))
        {
          push_node (parser, MENU_LAYOUT_NODE_INCLUDE);
        }
      else if (ELEMENT_IS ("Exclude"))
        {
          push_node (parser, MENU_LAYOUT_NODE_EXCLUDE);
        }
      else if (ELEMENT_IS ("MergeDir"))
        {
          push_node (parser, MENU_LAYOUT_NODE_MERGE_DIR);
        }
      else if (ELEMENT_IS ("KDELegacyDirs"))
        {
          push_node (parser, MENU_LAYOUT_NODE_KDE_LEGACY_DIRS);
        }
      else if (ELEMENT_IS ("Move"))
        {
          push_node (parser, MENU_LAYOUT_NODE_MOVE);
        }
      else if (ELEMENT_IS ("Deleted"))
        {
          push_node (parser, MENU_LAYOUT_NODE_DELETED);

        }
      else if (ELEMENT_IS ("NotDeleted"))
        {
          push_node (parser, MENU_LAYOUT_NODE_NOT_DELETED);
        }
      else if (ELEMENT_IS ("Layout"))
        {
          push_node (parser, MENU_LAYOUT_NODE_LAYOUT);
        }
      else
        {
          set_error (error, context,
                     G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
                     "Element <%s> may not appear below <%s>\n",
                     element_name, "Menu");
        }
    }
}

static void
start_matching_rule_element (MenuParser           *parser,
                             GMarkupParseContext  *context,
                             const char           *element_name,
                             const char          **attribute_names,
                             const char          **attribute_values,
                             GError              **error)
{
  if (!check_no_attributes (context, element_name,
                            attribute_names, attribute_values,
                            error))
    return;


  if (ELEMENT_IS ("Filename"))
    {
      push_node (parser, MENU_LAYOUT_NODE_FILENAME);
    }
  else if (ELEMENT_IS ("Category"))
    {
      push_node (parser, MENU_LAYOUT_NODE_CATEGORY);
    }
  else if (ELEMENT_IS ("All"))
    {
      push_node (parser, MENU_LAYOUT_NODE_ALL);
    }
  else if (ELEMENT_IS ("And"))
    {
      push_node (parser, MENU_LAYOUT_NODE_AND);
    }
  else if (ELEMENT_IS ("Or"))
    {
      push_node (parser, MENU_LAYOUT_NODE_OR);
    }
  else if (ELEMENT_IS ("Not"))
    {
      push_node (parser, MENU_LAYOUT_NODE_NOT);
    }
  else
    {
      set_error (error, context,
                 G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
                 "Element <%s> may not appear in this context\n",
                 element_name);
    }
}

static void
start_move_child_element (MenuParser           *parser,
                          GMarkupParseContext  *context,
                          const char           *element_name,
                          const char          **attribute_names,
                          const char          **attribute_values,
                          GError              **error)
{
  if (!check_no_attributes (context, element_name,
                            attribute_names, attribute_values,
                            error))
    return;

  if (ELEMENT_IS ("Old"))
    {
      push_node (parser, MENU_LAYOUT_NODE_OLD);
    }
  else if (ELEMENT_IS ("New"))
    {
      push_node (parser, MENU_LAYOUT_NODE_NEW);
    }
  else
    {
      set_error (error, context,
                 G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
                 "Element <%s> may not appear below <%s>\n",
                 element_name, "Move");
    }
}

static void
start_layout_child_element (MenuParser           *parser,
                            GMarkupParseContext  *context,
                            const char           *element_name,
                            const char          **attribute_names,
                            const char          **attribute_values,
                            GError              **error)
{
  if (ELEMENT_IS ("Menuname"))
    {
      const char *show_empty;
      const char *inline_menus;
      const char *inline_limit;
      const char *inline_header;
      const char *inline_alias;

      push_node (parser, MENU_LAYOUT_NODE_MENUNAME);

      locate_attributes (context, element_name,
                         attribute_names, attribute_values,
                         error,
                         "show_empty",    &show_empty,
                         "inline",        &inline_menus,
                         "inline_limit",  &inline_limit,
                         "inline_header", &inline_header,
                         "inline_alias",  &inline_alias,
                         NULL);

       menu_layout_node_menuname_set_values (parser->stack_top,
					     show_empty,
					     inline_menus,
					     inline_limit,
					     inline_header,
					     inline_alias);
    }
  else if (ELEMENT_IS ("Merge"))
    {
        const char *type;

        push_node (parser, MENU_LAYOUT_NODE_MERGE);

        locate_attributes (context, element_name,
                           attribute_names, attribute_values,
                           error,
                           "type", &type,
                           NULL);

	menu_layout_node_merge_set_type (parser->stack_top, type);
    }
  else
    {
      if (!check_no_attributes (context, element_name,
                                attribute_names, attribute_values,
                                error))
        return;

      if (ELEMENT_IS ("Filename"))
        {
          push_node (parser, MENU_LAYOUT_NODE_FILENAME);
        }
      else if (ELEMENT_IS ("Separator"))
        {
          push_node (parser, MENU_LAYOUT_NODE_SEPARATOR);
        }
      else
        {
          set_error (error, context,
                     G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
                     "Element <%s> may not appear below <%s>\n",
                     element_name, "Move");
        }
    }
}

static void
start_element_handler (GMarkupParseContext   *context,
                       const char            *element_name,
                       const char           **attribute_names,
                       const char           **attribute_values,
                       gpointer               user_data,
                       GError               **error)
{
  MenuParser *parser = user_data;

  if (ELEMENT_IS ("Menu"))
    {
      if (parser->stack_top == parser->root &&
          has_child_of_type (parser->root, MENU_LAYOUT_NODE_MENU))
        {
          set_error (error, context,
                     G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                     "Multiple root elements in menu file, only one toplevel <Menu> is allowed\n");
          return;
        }

      start_menu_element (parser, context, element_name,
                          attribute_names, attribute_values,
                          error);
    }
  else if (parser->stack_top == parser->root)
    {
      set_error (error, context,
                 G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                 "Root element in a menu file must be <Menu>, not <%s>\n",
                 element_name);
    }
  else if (parser->stack_top->type == MENU_LAYOUT_NODE_MENU)
    {
      start_menu_child_element (parser, context, element_name,
                                attribute_names, attribute_values,
                                error);
    }
  else if (parser->stack_top->type == MENU_LAYOUT_NODE_INCLUDE ||
           parser->stack_top->type == MENU_LAYOUT_NODE_EXCLUDE ||
           parser->stack_top->type == MENU_LAYOUT_NODE_AND     ||
           parser->stack_top->type == MENU_LAYOUT_NODE_OR      ||
           parser->stack_top->type == MENU_LAYOUT_NODE_NOT)
    {
      start_matching_rule_element (parser, context, element_name,
                                   attribute_names, attribute_values,
                                   error);
    }
  else if (parser->stack_top->type == MENU_LAYOUT_NODE_MOVE)
    {
      start_move_child_element (parser, context, element_name,
                                attribute_names, attribute_values,
                                error);
    }
  else if (parser->stack_top->type == MENU_LAYOUT_NODE_LAYOUT ||
           parser->stack_top->type == MENU_LAYOUT_NODE_DEFAULT_LAYOUT)
    {
      start_layout_child_element (parser, context, element_name,
                                  attribute_names, attribute_values,
                                  error);
    }
  else
    {
      set_error (error, context,
                 G_MARKUP_ERROR, G_MARKUP_ERROR_UNKNOWN_ELEMENT,
                 "Element <%s> may not appear in this context\n",
                 element_name);
    }

  add_context_to_error (error, context);
}

/* we want to make sure that the <Layout> or <DefaultLayout> is either empty,
 * or contain one <Merge> of type "all", or contain one <Merge> of type "menus"
 * and one <Merge> of type "files". If this is not the case, we try to clean up
 * things:
 *  + if there is at least one <Merge> of type "all", then we only keep the
 *    last <Merge> of type "all" and remove all others <Merge>
 *  + if there is no <Merge> with type "all", we keep only the last <Merge> of
 *    type "menus" and the last <Merge> of type "files". If there's no <Merge>
 *    of type "menus" we append one, and then if there's no <Merge> of type
 *    "files", we append one. (So menus are before files)
 */
static gboolean
fixup_layout_node (GMarkupParseContext   *context,
                   MenuParser            *parser,
                   MenuLayoutNode        *node,
                   GError              **error)
{
  MenuLayoutNode *child;
  MenuLayoutNode *last_all;
  MenuLayoutNode *last_menus;
  MenuLayoutNode *last_files;
  int             n_all;
  int             n_menus;
  int             n_files;

  if (!node->children)
    {
      return TRUE;
    }

  last_all   = NULL;
  last_menus = NULL;
  last_files = NULL;
  n_all   = 0;
  n_menus = 0;
  n_files = 0;

  child = node->children;
  while (child != NULL)
    {
      switch (child->type)
        {
        case MENU_LAYOUT_NODE_MERGE:
	  switch (menu_layout_node_merge_get_type (child))
	    {
	    case MENU_LAYOUT_MERGE_NONE:
	      break;

	    case MENU_LAYOUT_MERGE_MENUS:
	      last_menus = child;
	      n_menus++;
	      break;

	    case MENU_LAYOUT_MERGE_FILES:
	      last_files = child;
	      n_files++;
	      break;

	    case MENU_LAYOUT_MERGE_ALL:
	      last_all = child;
	      n_all++;
	      break;

	    default:
	      g_assert_not_reached ();
	      break;
	    }
          break;

	  default:
	    break;
	}

      child = node_next (child);
    }

  if ((n_all == 1 && n_menus == 0 && n_files == 0) ||
      (n_all == 0 && n_menus == 1 && n_files == 1))
    {
      return TRUE;
    }
  else if (n_all > 1 || n_menus > 1 || n_files > 1 ||
           (n_all == 1 && (n_menus != 0 || n_files != 0)))
    {
      child = node->children;
      while (child != NULL)
	{
	  MenuLayoutNode *next;

	  next = node_next (child);

	  switch (child->type)
	    {
	    case MENU_LAYOUT_NODE_MERGE:
	      switch (menu_layout_node_merge_get_type (child))
		{
		case MENU_LAYOUT_MERGE_NONE:
		  break;

		case MENU_LAYOUT_MERGE_MENUS:
		  if (n_all || last_menus != child)
		    {
		      menu_verbose ("removing duplicated merge menus element\n");
		      menu_layout_node_unlink (child);
		    }
		  break;

		case MENU_LAYOUT_MERGE_FILES:
		  if (n_all || last_files != child)
		    {
		      menu_verbose ("removing duplicated merge files element\n");
		      menu_layout_node_unlink (child);
		    }
		  break;

		case MENU_LAYOUT_MERGE_ALL:
		  if (last_all != child)
		    {
		      menu_verbose ("removing duplicated merge all element\n");
		      menu_layout_node_unlink (child);
		    }
		  break;

		default:
		  g_assert_not_reached ();
		  break;
		}
	      break;

	      default:
		break;
	    }

	  child = next;
	}
    }

  if (n_all == 0 && n_menus == 0)
    {
      last_menus = menu_layout_node_new (MENU_LAYOUT_NODE_MERGE);
      menu_layout_node_merge_set_type (last_menus, "menus");
      menu_verbose ("appending missing merge menus element\n");
      menu_layout_node_append_child (node, last_menus);
    }

  if (n_all == 0 && n_files == 0)
    {
      last_files = menu_layout_node_new (MENU_LAYOUT_NODE_MERGE);
      menu_layout_node_merge_set_type (last_files, "files");
      menu_verbose ("appending missing merge files element\n");
      menu_layout_node_append_child (node, last_files);
    }

  return TRUE;
}

/* we want to a) check that we have old-new pairs and b) canonicalize
 * such that each <Move> has exactly one old-new pair
 */
static gboolean
fixup_move_node (GMarkupParseContext   *context,
                 MenuParser            *parser,
                 MenuLayoutNode        *node,
                 GError              **error)
{
  MenuLayoutNode *child;
  int             n_old;
  int             n_new;

  n_old = 0;
  n_new = 0;

  child = node->children;
  while (child != NULL)
    {
      switch (child->type)
        {
        case MENU_LAYOUT_NODE_OLD:
          if (n_new != n_old)
            {
              set_error (error, context,
                         G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                         "<Old>/<New> elements not paired properly\n");
              return FALSE;
            }

          n_old += 1;

          break;

        case MENU_LAYOUT_NODE_NEW:
          n_new += 1;

          if (n_new != n_old)
            {
              set_error (error, context,
                         G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                         "<Old>/<New> elements not paired properly\n");
              return FALSE;
            }

          break;

        default:
          g_assert_not_reached ();
          break;
        }

      child = node_next (child);
    }

  if (n_new == 0 || n_old == 0)
    {
      set_error (error, context,
                 G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                 "<Old>/<New> elements missing under <Move>\n");
      return FALSE;
    }

  g_assert (n_new == n_old);
  g_assert ((n_new + n_old) % 2 == 0);

  if (n_new > 1)
    {
      MenuLayoutNode *prev;
      MenuLayoutNode *append_after;

      /* Need to split the <Move> into multiple <Move> */

      n_old = 0;
      n_new = 0;
      prev = NULL;
      append_after = node;

      child = node->children;
      while (child != NULL)
        {
          MenuLayoutNode *next;

          next = node_next (child);

          switch (child->type)
            {
            case MENU_LAYOUT_NODE_OLD:
              n_old += 1;
              break;

            case MENU_LAYOUT_NODE_NEW:
              n_new += 1;
              break;

            default:
              g_assert_not_reached ();
              break;
            }

          if (n_old == n_new &&
              n_old > 1)
            {
              /* Move the just-completed pair */
              MenuLayoutNode *new_move;

              g_assert (prev != NULL);

              new_move = menu_layout_node_new (MENU_LAYOUT_NODE_MOVE);
              menu_verbose ("inserting new_move after append_after\n");
              menu_layout_node_insert_after (append_after, new_move);
              append_after = new_move;

              menu_layout_node_steal (prev);
              menu_layout_node_steal (child);

              menu_verbose ("appending prev to new_move\n");
              menu_layout_node_append_child (new_move, prev);
              menu_verbose ("appending child to new_move\n");
              menu_layout_node_append_child (new_move, child);

              menu_verbose ("Created new move element old = %s new = %s\n",
                            menu_layout_node_move_get_old (new_move),
                            menu_layout_node_move_get_new (new_move));

              menu_layout_node_unref (new_move);
              menu_layout_node_unref (prev);
              menu_layout_node_unref (child);

              prev = NULL;
            }
          else
            {
              prev = child;
            }

          prev = child;
          child = next;
        }
    }

  return TRUE;
}

static void
end_element_handler (GMarkupParseContext  *context,
                     const char           *element_name,
                     gpointer              user_data,
                     GError              **error)
{
  MenuParser *parser = user_data;

  g_assert (parser->stack_top != NULL);

  switch (parser->stack_top->type)
    {
    case MENU_LAYOUT_NODE_APP_DIR:
    case MENU_LAYOUT_NODE_DIRECTORY_DIR:
    case MENU_LAYOUT_NODE_NAME:
    case MENU_LAYOUT_NODE_DIRECTORY:
    case MENU_LAYOUT_NODE_FILENAME:
    case MENU_LAYOUT_NODE_CATEGORY:
    case MENU_LAYOUT_NODE_MERGE_DIR:
    case MENU_LAYOUT_NODE_LEGACY_DIR:
    case MENU_LAYOUT_NODE_OLD:
    case MENU_LAYOUT_NODE_NEW:
    case MENU_LAYOUT_NODE_MENUNAME:
      if (menu_layout_node_get_content (parser->stack_top) == NULL)
        {
          set_error (error, context,
                     G_MARKUP_ERROR, G_MARKUP_ERROR_INVALID_CONTENT,
                     "Element <%s> is required to contain text and was empty\n",
                     element_name);
          goto out;
        }
      break;

    case MENU_LAYOUT_NODE_MENU:
      if (!has_child_of_type (parser->stack_top, MENU_LAYOUT_NODE_NAME))
        {
          set_error (error, context,
                     G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                     "<Menu> elements are required to contain a <Name> element\n");
          goto out;
        }
      break;

    case MENU_LAYOUT_NODE_ROOT:
    case MENU_LAYOUT_NODE_PASSTHROUGH:
    case MENU_LAYOUT_NODE_DEFAULT_APP_DIRS:
    case MENU_LAYOUT_NODE_DEFAULT_DIRECTORY_DIRS:
    case MENU_LAYOUT_NODE_DEFAULT_MERGE_DIRS:
    case MENU_LAYOUT_NODE_ONLY_UNALLOCATED:
    case MENU_LAYOUT_NODE_NOT_ONLY_UNALLOCATED:
    case MENU_LAYOUT_NODE_INCLUDE:
    case MENU_LAYOUT_NODE_EXCLUDE:
    case MENU_LAYOUT_NODE_ALL:
    case MENU_LAYOUT_NODE_AND:
    case MENU_LAYOUT_NODE_OR:
    case MENU_LAYOUT_NODE_NOT:
    case MENU_LAYOUT_NODE_KDE_LEGACY_DIRS:
    case MENU_LAYOUT_NODE_DELETED:
    case MENU_LAYOUT_NODE_NOT_DELETED:
    case MENU_LAYOUT_NODE_SEPARATOR:
    case MENU_LAYOUT_NODE_MERGE:
    case MENU_LAYOUT_NODE_MERGE_FILE:
      break;

    case MENU_LAYOUT_NODE_LAYOUT:
    case MENU_LAYOUT_NODE_DEFAULT_LAYOUT:
      if (!fixup_layout_node (context, parser, parser->stack_top, error))
        goto out;
      break;

    case MENU_LAYOUT_NODE_MOVE:
      if (!fixup_move_node (context, parser, parser->stack_top, error))
        goto out;
      break;
    }

 out:
  parser->stack_top = parser->stack_top->parent;
}

static gboolean
all_whitespace (const char *text,
                int         text_len)
{
  const char *p;
  const char *end;

  p = text;
  end = text + text_len;

  while (p != end)
    {
      if (!g_ascii_isspace (*p))
        return FALSE;

      p = g_utf8_next_char (p);
    }

  return TRUE;
}

static void
text_handler (GMarkupParseContext  *context,
              const char           *text,
              gsize                 text_len,
              gpointer              user_data,
              GError              **error)
{
  MenuParser *parser = user_data;

  switch (parser->stack_top->type)
    {
    case MENU_LAYOUT_NODE_APP_DIR:
    case MENU_LAYOUT_NODE_DIRECTORY_DIR:
    case MENU_LAYOUT_NODE_NAME:
    case MENU_LAYOUT_NODE_DIRECTORY:
    case MENU_LAYOUT_NODE_FILENAME:
    case MENU_LAYOUT_NODE_CATEGORY:
    case MENU_LAYOUT_NODE_MERGE_FILE:
    case MENU_LAYOUT_NODE_MERGE_DIR:
    case MENU_LAYOUT_NODE_LEGACY_DIR:
    case MENU_LAYOUT_NODE_OLD:
    case MENU_LAYOUT_NODE_NEW:
    case MENU_LAYOUT_NODE_MENUNAME:
      g_assert (menu_layout_node_get_content (parser->stack_top) == NULL);

      menu_layout_node_set_content (parser->stack_top, text);
      break;

    case MENU_LAYOUT_NODE_ROOT:
    case MENU_LAYOUT_NODE_PASSTHROUGH:
    case MENU_LAYOUT_NODE_MENU:
    case MENU_LAYOUT_NODE_DEFAULT_APP_DIRS:
    case MENU_LAYOUT_NODE_DEFAULT_DIRECTORY_DIRS:
    case MENU_LAYOUT_NODE_DEFAULT_MERGE_DIRS:
    case MENU_LAYOUT_NODE_ONLY_UNALLOCATED:
    case MENU_LAYOUT_NODE_NOT_ONLY_UNALLOCATED:
    case MENU_LAYOUT_NODE_INCLUDE:
    case MENU_LAYOUT_NODE_EXCLUDE:
    case MENU_LAYOUT_NODE_ALL:
    case MENU_LAYOUT_NODE_AND:
    case MENU_LAYOUT_NODE_OR:
    case MENU_LAYOUT_NODE_NOT:
    case MENU_LAYOUT_NODE_KDE_LEGACY_DIRS:
    case MENU_LAYOUT_NODE_MOVE:
    case MENU_LAYOUT_NODE_DELETED:
    case MENU_LAYOUT_NODE_NOT_DELETED:
    case MENU_LAYOUT_NODE_LAYOUT:
    case MENU_LAYOUT_NODE_DEFAULT_LAYOUT:
    case MENU_LAYOUT_NODE_SEPARATOR:
    case MENU_LAYOUT_NODE_MERGE:
      if (!all_whitespace (text, text_len))
        {
          set_error (error, context,
                     G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                     "No text is allowed inside element <%s>",
                     g_markup_parse_context_get_element (context));
        }
      break;
    }

  add_context_to_error (error, context);
}

static void
passthrough_handler (GMarkupParseContext  *context,
                     const char           *passthrough_text,
                     gsize                 text_len,
                     gpointer              user_data,
                     GError              **error)
{
  MenuParser *parser = user_data;
  MenuLayoutNode *node;

  /* don't push passthrough on the stack, it's not an element */

  node = menu_layout_node_new (MENU_LAYOUT_NODE_PASSTHROUGH);
  menu_layout_node_set_content (node, passthrough_text);

  menu_layout_node_append_child (parser->stack_top, node);
  menu_layout_node_unref (node);

  add_context_to_error (error, context);
}

static void
menu_parser_init (MenuParser *parser)
{
  parser->root = menu_layout_node_new (MENU_LAYOUT_NODE_ROOT);
  parser->stack_top = parser->root;
}

static void
menu_parser_free (MenuParser *parser)
{
  if (parser->root)
    menu_layout_node_unref (parser->root);
}

MenuLayoutNode *
menu_layout_load (const char  *filename,
                  const char  *non_prefixed_basename,
                  GError     **err)
{
  GMainContext        *main_context;
  GMarkupParseContext *context;
  MenuLayoutNodeRoot  *root;
  MenuLayoutNode      *retval;
  MenuParser           parser;
  GError              *error;
  GString             *str;
  char                *text;
  char                *s;
  gsize                length;

  text = NULL;
  length = 0;
  retval = NULL;
  context = NULL;

  main_context = g_main_context_get_thread_default ();

  menu_verbose ("Loading \"%s\" from disk\n", filename);

  if (!g_file_get_contents (filename,
                            &text,
                            &length,
                            err))
    {
      menu_verbose ("Failed to load \"%s\"\n",
                    filename);
      return NULL;
    }

  g_assert (text != NULL);

  menu_parser_init (&parser);

  root = (MenuLayoutNodeRoot *) parser.root;

  root->basedir = g_path_get_dirname (filename);
  menu_verbose ("Set basedir \"%s\"\n", root->basedir);

  if (non_prefixed_basename)
    s = g_strdup (non_prefixed_basename);
  else
    s = g_path_get_basename (filename);
  str = g_string_new (s);
  if (g_str_has_suffix (str->str, ".menu"))
    g_string_truncate (str, str->len - strlen (".menu"));

  root->name = str->str;
  menu_verbose ("Set menu name \"%s\"\n", root->name);

  g_string_free (str, FALSE);
  g_free (s);

  context = g_markup_parse_context_new (&menu_funcs, 0, &parser, NULL);

  error = NULL;
  if (!g_markup_parse_context_parse (context,
                                     text,
                                     length,
                                     &error))
    goto out;

  error = NULL;
  g_markup_parse_context_end_parse (context, &error);

  root->main_context = main_context ? g_main_context_ref (main_context) : NULL;

 out:
  if (context)
    g_markup_parse_context_free (context);
  g_free (text);

  if (error)
    {
      menu_verbose ("Error \"%s\" loading \"%s\"\n",
                    error->message, filename);
      g_propagate_error (err, error);
    }
  else if (has_child_of_type (parser.root, MENU_LAYOUT_NODE_MENU))
    {
      menu_verbose ("File loaded OK\n");
      retval = parser.root;
      parser.root = NULL;
    }
  else
    {
      menu_verbose ("Did not have a root element in file\n");
      g_set_error (err, G_MARKUP_ERROR, G_MARKUP_ERROR_PARSE,
                   "Menu file %s did not contain a root <Menu> element",
                   filename);
    }

  menu_parser_free (&parser);

  return retval;
}
