/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

#include <string.h>
#include "egg-menu-merge.h"
#include "egg-toolbar.h"
#include "egg-separatortoolitem.h"
#include "gobject-utils.h"
#include "intl.h"

#define NODE_INFO(node) ((EggMenuMergeNode *)node->data)

typedef struct {
	guint merge_id;
	GQuark action_quark;
} NodeUIReference;

enum {
	ADD_WIDGET,
	REMOVE_WIDGET,
	LAST_SIGNAL
};

static void   egg_menu_merge_class_init    (EggMenuMergeClass *class);
static void   egg_menu_merge_init          (EggMenuMerge *merge);
static void   egg_menu_merge_dispose       (GObject *obj);

static void   egg_menu_merge_queue_update  (EggMenuMerge *self);
static void   egg_menu_merge_dirty_all     (EggMenuMerge *self);

static GNode *get_child_node               (EggMenuMerge *self, GNode *parent,
					    const gchar *childname,
					    gint childname_length,
					    EggMenuMergeNodeType node_type,
					    gboolean create, gboolean top);
static GNode *egg_menu_merge_get_node      (EggMenuMerge *self,
					    const gchar *path,
					    EggMenuMergeNodeType node_type,
					    gboolean create);
static guint egg_menu_merge_next_merge_id  (EggMenuMerge *self);

static void  egg_menu_merge_node_prepend_ui_reference (EggMenuMergeNode *node,
						       guint merge_id,
						       GQuark action_quark);
static void  egg_menu_merge_node_remove_ui_reference  (EggMenuMergeNode *node,
						       guint merge_id);

static guint merge_signals[LAST_SIGNAL] = { 0 };

static GMemChunk *merge_node_chunk = NULL;

KZ_OBJECT_GET_TYPE(egg_menu_merge, "EggMenuMerge", EggMenuMerge,
		   egg_menu_merge_class_init, egg_menu_merge_init,
		   G_TYPE_OBJECT)

static void
egg_menu_merge_class_init (EggMenuMergeClass *class)
{
	GObjectClass *object_class;

	object_class = G_OBJECT_CLASS(class);

	if (!merge_node_chunk)
		merge_node_chunk = g_mem_chunk_create(EggMenuMergeNode, 64,
						      G_ALLOC_AND_FREE);

	merge_signals[ADD_WIDGET] =
		g_signal_new ("add_widget",
			      G_OBJECT_CLASS_TYPE(class),
			      G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
			      G_STRUCT_OFFSET(EggMenuMergeClass, add_widget),
			      NULL, NULL,
			      g_cclosure_marshal_VOID__OBJECT,
			      G_TYPE_NONE, 1,
			      GTK_TYPE_WIDGET);
	merge_signals[REMOVE_WIDGET] =
		g_signal_new ("remove_widget",
			      G_OBJECT_CLASS_TYPE(class),
			      G_SIGNAL_RUN_FIRST | G_SIGNAL_NO_RECURSE,
			      G_STRUCT_OFFSET(EggMenuMergeClass, remove_widget),
			      NULL, NULL,
			      g_cclosure_marshal_VOID__OBJECT,
			      G_TYPE_NONE, 1,
			      GTK_TYPE_WIDGET);  
  
	object_class->dispose = egg_menu_merge_dispose;
}


static void
egg_menu_merge_init (EggMenuMerge *self)
{
	guint merge_id;
	GNode *node;

	self->accel_group = NULL;

	self->root_node = NULL;
	self->action_groups = NULL;

	self->last_merge_id = 0;


	merge_id = egg_menu_merge_next_merge_id(self);
	node = get_child_node(self, NULL, "Root", 4,
			      EGG_MENU_MERGE_ROOT, TRUE, FALSE);
	egg_menu_merge_node_prepend_ui_reference(NODE_INFO(node), merge_id, 0);
	node = get_child_node(self, self->root_node, "popups", 6,
			      EGG_MENU_MERGE_POPUPS, TRUE, FALSE);
	egg_menu_merge_node_prepend_ui_reference(NODE_INFO(node), merge_id, 0);
}

static gboolean
destroy_node (GNode *node, gpointer data)
{
	EggMenuMergeNode *mnode = NODE_INFO(node);

#warning FIXME
	g_free((gchar *) mnode->name);
	mnode->name = NULL;

	mnode->action_name = 0;

	if (mnode->action)
		g_object_unref(mnode->action);
	mnode->action = NULL;

	g_list_foreach(mnode->uifiles, (GFunc) g_free, NULL);
	g_list_free(mnode->uifiles);
	mnode->uifiles = NULL;

	g_free(mnode);

	return FALSE;
}

static void
egg_menu_merge_dispose (GObject *obj)
{
	EggMenuMerge *self = EGG_MENU_MERGE(obj);

	if (self->accel_group)
		g_object_unref(self->accel_group);
	self->accel_group = NULL;
	
	g_node_traverse(self->root_node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
			destroy_node, NULL);
	g_node_destroy(self->root_node);

	if (self->action_groups) {
		g_list_foreach(self->action_groups, (GFunc) g_object_unref, NULL);
		g_list_free(self->action_groups);
	}
	self->action_groups = NULL;
}

EggMenuMerge *
egg_menu_merge_new (void)
{
	return g_object_new(EGG_TYPE_MENU_MERGE, NULL);
}

void
egg_menu_merge_set_accel_group (EggMenuMerge *self,
				GtkAccelGroup *accel_group)
{
	g_return_if_fail(EGG_IS_MENU_MERGE(self));

	if (self->accel_group)
		g_object_unref(self->accel_group);
	self->accel_group = NULL;

	if (accel_group && GTK_IS_ACCEL_GROUP(accel_group))
		self->accel_group = g_object_ref(accel_group);
}

void
egg_menu_merge_insert_action_group (EggMenuMerge *self,
				    EggActionGroup *action_group, gint pos)
{
	g_return_if_fail(EGG_IS_MENU_MERGE(self));
	g_return_if_fail(EGG_IS_ACTION_GROUP(action_group));
	g_return_if_fail(g_list_find(self->action_groups, action_group) == NULL);

	g_object_ref(action_group);
	self->action_groups = g_list_insert(self->action_groups,
					    action_group, pos);

	/* dirty all nodes, as action bindings may change */
	egg_menu_merge_dirty_all(self);
}

void
egg_menu_merge_remove_action_group (EggMenuMerge *self,
				    EggActionGroup *action_group)
{
	g_return_if_fail(EGG_IS_MENU_MERGE(self));
	g_return_if_fail(EGG_IS_ACTION_GROUP(action_group));
	g_return_if_fail(g_list_find(self->action_groups, action_group) != NULL);

	self->action_groups = g_list_remove(self->action_groups, action_group);
	g_object_unref(action_group);

	/* dirty all nodes, as action bindings may change */
	egg_menu_merge_dirty_all(self);
}

GtkWidget *
egg_menu_merge_get_widget (EggMenuMerge *self, const gchar *path)
{
	GNode *node;

	/* ensure that there are no pending updates before we get the
	 * widget */
	egg_menu_merge_ensure_update(self);

	node = egg_menu_merge_get_node(self, path,
				       EGG_MENU_MERGE_UNDECIDED,
				       FALSE);
	if (!node) return NULL;

	return NODE_INFO(node)->proxy;
}

static GNode *
get_child_node(EggMenuMerge *self, GNode *parent,
	       const gchar *childname, gint childname_length,
	       EggMenuMergeNodeType node_type,
	       gboolean create, gboolean top)
{
	GNode *child = NULL;

	g_return_val_if_fail(parent == NULL ||
			     (NODE_INFO(parent)->type != EGG_MENU_MERGE_MENUITEM &&
			      NODE_INFO(parent)->type != EGG_MENU_MERGE_TOOLITEM), NULL);

	if (parent)
	{
		if (childname)
		{
			for (child = parent->children; child != NULL; child = child->next)
			{
				if (strlen(NODE_INFO(child)->name) == childname_length &&
				    !strncmp(NODE_INFO(child)->name, childname, childname_length))
				{
					/* if undecided about node type, set it */
					if (NODE_INFO(child)->type == EGG_MENU_MERGE_UNDECIDED)
						NODE_INFO(child)->type = node_type;
		  
					/* warn about type mismatch */
					if (node_type != EGG_MENU_MERGE_UNDECIDED &&
					    NODE_INFO(child)->type != node_type)
						g_warning("node type doesn't match %d (%s is type %d)",
							  node_type, NODE_INFO(child)->name,
							  NODE_INFO(child)->type);

					return child;
				}
			}
		}
		if (!child && create)
		{
			EggMenuMergeNode *mnode;

			mnode = g_chunk_new0(EggMenuMergeNode, merge_node_chunk);
			mnode->type = node_type;
			mnode->name = g_strndup(childname, childname_length);
			mnode->dirty = TRUE;

			if (top)
				child = g_node_prepend_data(parent, mnode);
			else
				child = g_node_append_data(parent, mnode);
		}
	}
	else
	{
		/* handle root node */
		if (self->root_node)
		{
			child = self->root_node;
			if (strncmp(NODE_INFO(child)->name, childname, childname_length) !=0)
				g_warning("root node name '%s' doesn't match '%s'",
					  childname, NODE_INFO(child)->name);
			if (NODE_INFO(child)->type != EGG_MENU_MERGE_ROOT)
				g_warning("base element must be of type ROOT");
		}
		else if (create)
		{
			EggMenuMergeNode *mnode;

			mnode = g_chunk_new0(EggMenuMergeNode, merge_node_chunk);
			mnode->type = node_type;
			mnode->name = g_strndup(childname, childname_length);
			mnode->dirty = TRUE;

			child = self->root_node = g_node_new(mnode);
		}
	}

	return child;
}

static GNode *
egg_menu_merge_get_node (EggMenuMerge *self, const gchar *path,
			 EggMenuMergeNodeType node_type, gboolean create)
{
	const gchar *pos, *end;
	GNode *parent, *node;

	end = path + strlen(path);
	pos = path;
	parent = node = NULL;
	while (pos < end)
	{
		const gchar *slash;
		gsize length;

		slash = strchr(pos, '/');
		if (slash)
			length = slash - pos;
		else
			length = strlen(pos);

		node = get_child_node(self, parent, pos, length,
				      EGG_MENU_MERGE_UNDECIDED,
				      create, FALSE);
		if (!node)
			return NULL;

		pos += length + 1;/* move past the node name and the slash too */
		parent = node;
	}

	if (NODE_INFO(node)->type == EGG_MENU_MERGE_UNDECIDED)
		NODE_INFO(node)->type = node_type;
	return node;
}

static guint
egg_menu_merge_next_merge_id (EggMenuMerge *self)
{
	self->last_merge_id++;

	return self->last_merge_id;
}

static void
egg_menu_merge_node_prepend_ui_reference (EggMenuMergeNode *node,
					  guint merge_id, GQuark action_quark)
{
	NodeUIReference *reference;

	reference = g_new(NodeUIReference, 1);
	reference->action_quark = action_quark;
	reference->merge_id = merge_id;

	/* Prepend the reference */
	node->uifiles = g_list_prepend(node->uifiles, reference);

	node->dirty = TRUE;
}

static void
egg_menu_merge_node_remove_ui_reference (EggMenuMergeNode *node,
					 guint merge_id)
{
	GList *p;

	for (p = node->uifiles; p != NULL; p = p->next)
	{
		NodeUIReference *reference = p->data;

		if (reference->merge_id == merge_id)
		{
			node->uifiles = g_list_remove_link(node->uifiles, p);
			node->dirty = TRUE;
			g_free(reference);

			break;
		}
	}
}

/* -------------------- The UI file parser -------------------- */

typedef enum {
	STATE_START,
	STATE_ROOT,
	STATE_MENU,
	STATE_TOOLBAR,
	STATE_POPUPS,
	STATE_MENUITEM,
	STATE_TOOLITEM,
	STATE_END
} ParseState;

typedef struct _ParseContext ParseContext;
struct _ParseContext
{
	ParseState state;
	ParseState prev_state;

	EggMenuMerge *self;

	GNode *current;

	guint merge_id;
};

static void
start_element_handler (GMarkupParseContext *context,
		       const gchar *element_name,
		       const gchar **attribute_names,
		       const gchar **attribute_values,
		       gpointer user_data,
		       GError **error)
{
	ParseContext *ctx = user_data;
	EggMenuMerge *self = ctx->self;

	gint i;
	const gchar *node_name;
	GQuark verb_quark;
	gboolean top;

	gboolean raise_error = TRUE;
	gchar *error_attr = NULL;

	/* g_message("starting element %s", element_name); */

	/* work out a name for this node.  Either the name attribute, or
	 * element name */
	node_name = element_name;
	verb_quark = 0;
	top = FALSE;
	for (i = 0; attribute_names[i] != NULL; i++)
	{
		if (!strcmp(attribute_names[i], "name"))
		{
			node_name = attribute_values[i];
		}
		else if (!strcmp(attribute_names[i], "verb"))
		{
			verb_quark = g_quark_from_string(attribute_values[i]);
		}
		else if (!strcmp(attribute_names[i], "pos"))
		{
			top = !strcmp(attribute_values[i], "top");
		}
	}
	/* if no verb, then set it to the node's name */
	if (verb_quark == 0)
		verb_quark = g_quark_from_string(node_name);

	switch (element_name[0])
	{
	case 'R':
		if (ctx->state == STATE_START && !strcmp(element_name, "Root"))
		{
			ctx->state = STATE_ROOT;
			ctx->current = self->root_node;
			raise_error = FALSE;

			egg_menu_merge_node_prepend_ui_reference(
				NODE_INFO(ctx->current),
				ctx->merge_id, verb_quark);
		}
		break;
	case 'm':
		if (ctx->state == STATE_ROOT && !strcmp(element_name, "menu"))
		{
			ctx->state = STATE_MENU;
			ctx->current = get_child_node(self, ctx->current,
						      node_name,
						      strlen(node_name),
						      EGG_MENU_MERGE_MENUBAR,
						      TRUE, FALSE);
			if (NODE_INFO(ctx->current)->action_name == 0)
				NODE_INFO(ctx->current)->action_name
					= verb_quark;

			egg_menu_merge_node_prepend_ui_reference(
				NODE_INFO(ctx->current),
				ctx->merge_id, verb_quark);
			NODE_INFO(ctx->current)->dirty = TRUE;

			raise_error = FALSE;
		}
		else if (ctx->state == STATE_MENU &&
			 !strcmp(element_name, "menuitem"))
		{
			GNode *node;

			ctx->state = STATE_MENUITEM;
			node = get_child_node(self, ctx->current,
					      node_name, strlen(node_name),
					      EGG_MENU_MERGE_MENUITEM,
					      TRUE, top);
			if (NODE_INFO(node)->action_name == 0)
				NODE_INFO(node)->action_name = verb_quark;

			egg_menu_merge_node_prepend_ui_reference(
				NODE_INFO(node),
				ctx->merge_id, verb_quark);
			NODE_INFO(node)->dirty = TRUE;

			raise_error = FALSE;
		}
		break;
	case 'd':
		if (ctx->state == STATE_ROOT &&
		    !strcmp(element_name, "dockitem"))
		{
			ctx->state = STATE_TOOLBAR;
			ctx->current = get_child_node(self, ctx->current,
						      node_name,
						      strlen(node_name),
						      EGG_MENU_MERGE_TOOLBAR,
						      TRUE, FALSE);
			if (NODE_INFO(ctx->current)->action_name == 0)
				NODE_INFO(ctx->current)->action_name
					= verb_quark;

			egg_menu_merge_node_prepend_ui_reference(
				NODE_INFO(ctx->current),
				ctx->merge_id, verb_quark);
			NODE_INFO(ctx->current)->dirty = TRUE;

			raise_error = FALSE;
		}
		break;
	case 'p':
		if (ctx->state == STATE_ROOT && !strcmp(element_name, "popups"))
		{
			ctx->state = STATE_POPUPS;
			ctx->current = get_child_node(self, ctx->current,
						      node_name,
						      strlen(node_name),
						      EGG_MENU_MERGE_POPUPS,
						      TRUE, FALSE);

			egg_menu_merge_node_prepend_ui_reference(
				NODE_INFO (ctx->current),
				ctx->merge_id, verb_quark);
			NODE_INFO(ctx->current)->dirty = TRUE;

			raise_error = FALSE;
		}
		else if (ctx->state == STATE_POPUPS &&
			 !strcmp(element_name, "popup"))
		{
			ctx->state = STATE_MENU;
			ctx->current = get_child_node(self, ctx->current,
						      node_name,
						      strlen(node_name),
						      EGG_MENU_MERGE_MENU,
						      TRUE, FALSE);
			if (NODE_INFO(ctx->current)->action_name == 0)
				NODE_INFO(ctx->current)->action_name
					= verb_quark;

			egg_menu_merge_node_prepend_ui_reference(
				NODE_INFO(ctx->current),
				ctx->merge_id, verb_quark);
			NODE_INFO(ctx->current)->dirty = TRUE;

			raise_error = FALSE;
		}
		else if ((ctx->state == STATE_MENU ||
			  ctx->state == STATE_TOOLBAR) &&
			 !strcmp(element_name, "placeholder"))
		{
			if (ctx->state == STATE_MENU)
				ctx->current = get_child_node(self, ctx->current,
							      node_name,
							      strlen(node_name),
							      EGG_MENU_MERGE_MENU_PLACEHOLDER,
							      TRUE, top);
			else
				ctx->current = get_child_node(self, ctx->current,
							      node_name,
							      strlen(node_name),
							      EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER,
							      TRUE, top);

			egg_menu_merge_node_prepend_ui_reference(
				NODE_INFO(ctx->current),
				ctx->merge_id, verb_quark);
			NODE_INFO(ctx->current)->dirty = TRUE;

			raise_error = FALSE;
		}
		break;
	case 's':
		if (ctx->state == STATE_MENU && !strcmp(element_name, "submenu"))
		{
			ctx->state = STATE_MENU;
			ctx->current = get_child_node(self, ctx->current,
						      node_name,
						      strlen(node_name),
						      EGG_MENU_MERGE_MENU,
						      TRUE, top);
			if (NODE_INFO(ctx->current)->action_name == 0)
				NODE_INFO(ctx->current)->action_name
					= verb_quark;

			egg_menu_merge_node_prepend_ui_reference(
				NODE_INFO(ctx->current),
				ctx->merge_id, verb_quark);
			NODE_INFO(ctx->current)->dirty = TRUE;

			raise_error = FALSE;
		}
		else if ((ctx->state == STATE_MENU ||
			  ctx->state == STATE_TOOLBAR) &&
			 !strcmp(element_name, "separator"))
		{
			GNode *node;

			if (ctx->state == STATE_MENU)
				ctx->state = STATE_MENUITEM;
			else
				ctx->state = STATE_TOOLITEM;
			node = get_child_node(self, ctx->current,
					      node_name, strlen(node_name),
					      EGG_MENU_MERGE_SEPARATOR,
					      TRUE, top);
			if (NODE_INFO(node)->action_name == 0)
				NODE_INFO(node)->action_name = verb_quark;

			egg_menu_merge_node_prepend_ui_reference(
				NODE_INFO (node),
				ctx->merge_id, verb_quark);
			NODE_INFO(node)->dirty = TRUE;

			raise_error = FALSE;
		}
		break;
	case 't':
		if (ctx->state == STATE_TOOLBAR &&
		    !strcmp(element_name, "toolitem"))
		{
			GNode *node;

			ctx->state = STATE_TOOLITEM;
			node = get_child_node(self, ctx->current,
					      node_name, strlen(node_name),
					      EGG_MENU_MERGE_TOOLITEM,
					      TRUE, top);
			if (NODE_INFO(node)->action_name == 0)
				NODE_INFO(node)->action_name = verb_quark;

			egg_menu_merge_node_prepend_ui_reference(
				NODE_INFO (node),
				ctx->merge_id, verb_quark);
			NODE_INFO(node)->dirty = TRUE;

			raise_error = FALSE;
		}
		break;
	default:
		break;
	}
	if (raise_error)
	{
		gint line_number, char_number;

		g_markup_parse_context_get_position(context,
						    &line_number, &char_number);
		if (error_attr)
			g_set_error(error,
				    G_MARKUP_ERROR,
				    G_MARKUP_ERROR_UNKNOWN_ATTRIBUTE,
				    _("Unknown attribute '%s' on line %d char %d"),
				    error_attr,
				    line_number, char_number);
		else
			g_set_error(error,
				    G_MARKUP_ERROR,
				    G_MARKUP_ERROR_UNKNOWN_ELEMENT,
				    _("Unknown tag '%s' on line %d char %d"),
				    element_name,
				    line_number, char_number);
	}
}

static void
end_element_handler (GMarkupParseContext *context,
		     const gchar *element_name,
		     gpointer user_data,
		     GError **error)
{
	ParseContext *ctx = user_data;
	EggMenuMerge *self = ctx->self;

	/*
	g_message("ending element %s (state=%d)", element_name, ctx->state);
	*/

	switch (ctx->state)
	{
	case STATE_START:
		g_warning("shouldn't get any end tags in start state");
		/* should we GError here? */
		break;
	case STATE_ROOT:
		if (ctx->current != self->root_node)
			g_warning("we are in STATE_ROOT, but the current node isn't the root");
		ctx->current = NULL;
		ctx->state = STATE_END;
		break;
	case STATE_MENU:
		ctx->current = ctx->current->parent;
		/* menubar */
		if (NODE_INFO(ctx->current)->type == EGG_MENU_MERGE_ROOT)
			ctx->state = STATE_ROOT;
		/* popup */
		else if (NODE_INFO(ctx->current)->type == EGG_MENU_MERGE_POPUPS)
			ctx->state = STATE_POPUPS;
		/* else, stay in STATE_MENU state */
		break;
	case STATE_TOOLBAR:
		ctx->current = ctx->current->parent;
		/* we conditionalise this test, in case we are closing off a
		 * placeholder */
		if (NODE_INFO(ctx->current)->type == EGG_MENU_MERGE_ROOT)
			ctx->state = STATE_ROOT;
		/* else, stay in STATE_TOOLBAR state */
		break;
	case STATE_POPUPS:
		ctx->current = ctx->current->parent;
		ctx->state = STATE_ROOT;
		break;
	case STATE_MENUITEM:
		ctx->state = STATE_MENU;
		break;
	case STATE_TOOLITEM:
	ctx->state = STATE_TOOLBAR;
		break;
	case STATE_END:
		g_warning("shouldn't get any end tags at this point");
		/* should do an error here */
		break;
	}
}

static void
cleanup (GMarkupParseContext *context,
	 GError *error,
	 gpointer user_data)
{
	ParseContext *ctx = user_data;
	EggMenuMerge *self = ctx->self;

	ctx->current = NULL;
	/* should also walk through the tree and get rid of nodes related to
	 * this UI file's tag */

	egg_menu_merge_remove_ui(self, ctx->merge_id);
}

static GMarkupParser ui_parser = {
	start_element_handler,
	end_element_handler,
	NULL,
	NULL,
	cleanup
};

guint
egg_menu_merge_add_ui_from_string (EggMenuMerge *self,
				   const gchar *buffer, guint length,
				   GError **error)
{
	ParseContext ctx = { 0 };
	GMarkupParseContext *context;
	gboolean res = TRUE;

	g_return_val_if_fail(EGG_IS_MENU_MERGE(self), FALSE);
	g_return_val_if_fail(buffer != NULL, FALSE);

	ctx.state = STATE_START;
	ctx.self = self;
	ctx.current = NULL;
	ctx.merge_id = egg_menu_merge_next_merge_id(self);

	context = g_markup_parse_context_new(&ui_parser, 0, &ctx, NULL);
	if (length < 0)
		length = strlen(buffer);

	if (g_markup_parse_context_parse(context, buffer, length, error))
	{
		if (!g_markup_parse_context_end_parse(context, error))
			res = FALSE;
	}
	else
		res = FALSE;

	g_markup_parse_context_free(context);

	egg_menu_merge_queue_update(self);

	if (res)
		return ctx.merge_id;
	return 0;
}

guint
egg_menu_merge_add_ui_from_file (EggMenuMerge *self,
				 const gchar *filename,
				 GError **error)
{
	gchar *buffer;
	gint length;
	guint res;

	if (!g_file_get_contents(filename, &buffer, &length, error))
		return 0;

	res = egg_menu_merge_add_ui_from_string(self, buffer, length, error);
	g_free(buffer);

	return res;
}

static gboolean
remove_ui (GNode *node, gpointer user_data)
{
	guint merge_id = GPOINTER_TO_UINT(user_data);

	egg_menu_merge_node_remove_ui_reference(NODE_INFO(node), merge_id);

	return FALSE; /* continue */
}

void
egg_menu_merge_remove_ui (EggMenuMerge *self, guint merge_id)
{
	g_node_traverse(self->root_node, G_POST_ORDER, G_TRAVERSE_ALL, -1,
			remove_ui, GUINT_TO_POINTER(merge_id));

	egg_menu_merge_queue_update(self);
}

/* -------------------- Updates -------------------- */


static EggAction *
get_action_by_name (EggMenuMerge *merge, const char *action_name)
{
	GList *tmp;

	if (!action_name)
		return NULL;
  
	/* lookup name */
	for (tmp = merge->action_groups; tmp != NULL; tmp = tmp->next)
	{
		EggActionGroup *action_group = tmp->data;
		EggAction *action;

		action = egg_action_group_get_action(action_group, action_name);

		if (action)
			return action;
	}

	return NULL;
}

static gboolean
find_menu_position (GNode *node, GtkWidget **menushell_p, gint *pos_p)
{
	GtkWidget *menushell;
	gint pos;

	g_return_val_if_fail(node != NULL, FALSE);
	g_return_val_if_fail(NODE_INFO(node)->type == EGG_MENU_MERGE_MENU ||
			     NODE_INFO(node)->type == EGG_MENU_MERGE_MENU_PLACEHOLDER ||
			     NODE_INFO(node)->type == EGG_MENU_MERGE_MENUITEM ||
			     NODE_INFO(node)->type == EGG_MENU_MERGE_SEPARATOR,
			     FALSE);

	/* first sibling -- look at parent */
	if (node->prev == NULL)
	{
		GNode *parent;

		parent = node->parent;
		switch (NODE_INFO(parent)->type)
		{
		case EGG_MENU_MERGE_MENUBAR:
			menushell = NODE_INFO(parent)->proxy;
			pos = 0;
			break;
		case EGG_MENU_MERGE_MENU:
			menushell = NODE_INFO(parent)->proxy;
			if (GTK_IS_MENU_ITEM(menushell))
				menushell = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menushell));
			pos = 0;
			break;
		case EGG_MENU_MERGE_MENU_PLACEHOLDER:
			menushell = gtk_widget_get_parent(NODE_INFO(parent)->proxy);
			g_return_val_if_fail(GTK_IS_MENU_SHELL(menushell), FALSE);
			pos = g_list_index(GTK_MENU_SHELL(menushell)->children,
					   NODE_INFO(parent)->proxy) + 1;
			break;
		default:
			g_warning("%s: bad parent node type %d", G_STRLOC,
				  NODE_INFO(parent)->type);
			return FALSE;
		}
	}
	else
	{
		GtkWidget *prev_child;
		GNode *sibling;

		sibling = node->prev;
		if (NODE_INFO(sibling)->type == EGG_MENU_MERGE_MENU_PLACEHOLDER)
			/* second Separator */
			prev_child = NODE_INFO(sibling)->extra;
		else
			prev_child = NODE_INFO(sibling)->proxy;

		g_return_val_if_fail(GTK_IS_WIDGET(prev_child), FALSE);
		menushell = gtk_widget_get_parent(prev_child);
		g_return_val_if_fail(GTK_IS_MENU_SHELL(menushell), FALSE);

		pos = g_list_index(GTK_MENU_SHELL(menushell)->children,
				   prev_child) + 1;
	}

	if (menushell_p)
		*menushell_p = menushell;
	if (pos_p)
		*pos_p = pos;

	return TRUE;
}

static gboolean
find_toolbar_position (GNode *node, GtkWidget **toolbar_p, gint *pos_p)
{
	GtkWidget *toolbar;
	gint pos;

	g_return_val_if_fail(node != NULL, FALSE);
	g_return_val_if_fail(NODE_INFO(node)->type == EGG_MENU_MERGE_TOOLBAR ||
			     NODE_INFO(node)->type == EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER ||
			     NODE_INFO(node)->type == EGG_MENU_MERGE_TOOLITEM ||
			     NODE_INFO(node)->type == EGG_MENU_MERGE_SEPARATOR,
			     FALSE);

	/* first sibling -- look at parent */
	if (node->prev == NULL)
	{
		GNode *parent;

		parent = node->parent;
		switch (NODE_INFO(parent)->type)
		{
		case EGG_MENU_MERGE_TOOLBAR:
			toolbar = NODE_INFO(parent)->proxy;
			pos = 0;
			break;
		case EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER:
			toolbar = gtk_widget_get_parent(NODE_INFO(parent)->proxy);
			g_return_val_if_fail(EGG_IS_TOOLBAR(toolbar), FALSE);
			pos = g_list_index(egg_toolbar_get_tool_items(EGG_TOOLBAR(toolbar)),
					   NODE_INFO(parent)->proxy) + 1;
			break;
		default:
			g_warning("%s: bad parent node type %d", G_STRLOC,
				  NODE_INFO(parent)->type);
			return FALSE;
		}
	}
	else
	{
		GtkWidget *prev_child;
		GNode *sibling;

		sibling = node->prev;
		if (NODE_INFO(sibling)->type == EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER)
			/* second Separator */
			prev_child = NODE_INFO(sibling)->extra;
		else
			prev_child = NODE_INFO(sibling)->proxy;

		g_return_val_if_fail(GTK_IS_WIDGET(prev_child), FALSE);
		toolbar = gtk_widget_get_parent(prev_child);
		g_return_val_if_fail(EGG_IS_TOOLBAR(toolbar), FALSE);

		pos = g_list_index(egg_toolbar_get_tool_items(EGG_TOOLBAR(toolbar)),
				   prev_child) + 1;
	}

	if (toolbar_p)
		*toolbar_p = toolbar;
	if (pos_p)
		*pos_p = pos;

	return TRUE;
}

static void
update_node (EggMenuMerge *self, GNode *node)
{
	EggMenuMergeNode *info;
	GNode *child;
	EggAction *action;

	g_return_if_fail (node != NULL);
	g_return_if_fail (NODE_INFO(node) != NULL);

	info = NODE_INFO(node);

	if (NODE_INFO(node)->dirty)
	{
		const gchar *action_name;
		NodeUIReference *ref;

		if (info->uifiles == NULL) {
			/* We may need to remove this node.
			 * This must be done in post order
			 */
			goto recurse_children;
		}

		ref = info->uifiles->data;
		action_name = g_quark_to_string(ref->action_quark);
		action = get_action_by_name(self, action_name);

		NODE_INFO(node)->dirty = FALSE;

		/* Check if the node doesn't have an action and must have an action */
		if (action == NULL &&
		    info->type != EGG_MENU_MERGE_MENUBAR &&
		    info->type != EGG_MENU_MERGE_TOOLBAR &&
		    info->type != EGG_MENU_MERGE_SEPARATOR &&
		    info->type != EGG_MENU_MERGE_MENU_PLACEHOLDER &&
		    info->type != EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER)
		{
			/* FIXME: Should we warn here? */
			goto recurse_children;
		}

		/* If the widget already has a proxy and the action hasn't changed, then
		 * we don't have to do anything.
		 */
		if (info->proxy != NULL &&
		    action == info->action)
		{
			goto recurse_children;
		}
      
		if (info->action)
			g_object_unref(info->action);
		info->action = action;
		if (info->action)
			g_object_ref(info->action);

		switch (info->type)
		{
		case EGG_MENU_MERGE_MENUBAR:
			if (info->proxy == NULL)
			{
				info->proxy = gtk_menu_bar_new();
				gtk_widget_show(info->proxy);
				g_signal_emit(self, merge_signals[ADD_WIDGET], 0,
					      info->proxy);
			}
			break;
		case EGG_MENU_MERGE_MENU:
		{
			GtkWidget *prev_submenu = NULL;
			/* remove the proxy if it is of the wrong type ... */
			if (info->proxy &&  G_OBJECT_TYPE(info->proxy) !=
			    EGG_ACTION_GET_CLASS(info->action)->menu_item_type)
			{
				prev_submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(info->proxy));
				if (prev_submenu)
				{
					g_object_ref (prev_submenu);
					gtk_menu_item_set_submenu(GTK_MENU_ITEM(info->proxy),NULL);
				}
				gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
						     info->proxy);
				info->proxy = NULL;
			}
			/* create proxy if needed ... */
			if (info->proxy == NULL)
			{
				GtkWidget *menushell;
				gint pos;
				GNode *parent;

				parent = node->parent;

				if (parent && NODE_INFO (parent)->type == EGG_MENU_MERGE_POPUPS)
				{
					info->proxy = gtk_menu_new();
				}
				else if (find_menu_position(node, &menushell, &pos))
				{
					GtkWidget *menu;
					info->proxy = egg_action_create_menu_item (info->action);
					menu = gtk_menu_new();
					gtk_menu_item_set_submenu(GTK_MENU_ITEM(info->proxy), menu);
					gtk_menu_set_accel_group (GTK_MENU (menu), self->accel_group);
					gtk_menu_shell_insert (GTK_MENU_SHELL (menushell),
							       info->proxy, pos);
				}
			}
			else
			{
				egg_action_connect_proxy (info->action, info->proxy);
			}
			if (prev_submenu)
			{
				gtk_menu_item_set_submenu (GTK_MENU_ITEM (info->proxy),
							   prev_submenu);
				g_object_unref (prev_submenu);
			}
		}
		break;
		case EGG_MENU_MERGE_UNDECIDED:
			g_warning("found 'undecided node!");
			break;
		case EGG_MENU_MERGE_ROOT:
			break;
		case EGG_MENU_MERGE_TOOLBAR:
			if (info->proxy == NULL)
			{
				info->proxy = egg_toolbar_new();
				gtk_widget_show(info->proxy);
				g_signal_emit(self, merge_signals[ADD_WIDGET], 0,
					      info->proxy);
			}
			break;
		case EGG_MENU_MERGE_MENU_PLACEHOLDER:
			/* create menu items for placeholders if necessary ... */
			if (!GTK_IS_SEPARATOR_MENU_ITEM(info->proxy) ||
			    !GTK_IS_SEPARATOR_MENU_ITEM(info->extra))
			{
				if (info->proxy)
					gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
							     info->proxy);
				if (info->extra)
					gtk_container_remove(GTK_CONTAINER(info->extra->parent),
							     info->extra);
				info->proxy = NULL;
				info->extra = NULL;
			}
			if (info->proxy == NULL)
			{
				GtkWidget *menushell;
				gint pos;

				if (find_menu_position(node, &menushell, &pos))
				{
					NODE_INFO(node)->proxy = gtk_separator_menu_item_new();
					gtk_menu_shell_insert(GTK_MENU_SHELL(menushell),
							      NODE_INFO(node)->proxy, pos);
					/* gtk_widget_show(NODE_INFO(node)->proxy); */

					NODE_INFO(node)->extra = gtk_separator_menu_item_new();
					gtk_menu_shell_insert(GTK_MENU_SHELL(menushell),
							      NODE_INFO(node)->extra, pos+1);
					/* gtk_widget_show(NODE_INFO(node)->extra); */
				}
			}
			break;
		case EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER:
			/* create toolbar items for placeholders if necessary ... */
			if (!EGG_IS_SEPARATOR_TOOL_ITEM (info->proxy) ||
			    !EGG_IS_SEPARATOR_TOOL_ITEM (info->extra))
			{
				if (info->proxy)
					gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
							     info->proxy);
				if (info->extra)
					gtk_container_remove(GTK_CONTAINER(info->extra->parent),
							     info->extra);
				info->proxy = NULL;
				info->extra = NULL;
			}
			if (info->proxy == NULL)
			{
				GtkWidget *toolbar;
				gint pos;

				if (find_toolbar_position(node, &toolbar, &pos))
				{
					EggToolItem *item;

					item = egg_separator_tool_item_new();
					egg_toolbar_insert_tool_item(EGG_TOOLBAR(toolbar), item, pos);
					NODE_INFO(node)->proxy = GTK_WIDGET(item);
					/* gtk_widget_show(NODE_INFO(node)->proxy); */

					item = egg_separator_tool_item_new();
					egg_toolbar_insert_tool_item(EGG_TOOLBAR(toolbar), item, pos+1);
					NODE_INFO(node)->extra = GTK_WIDGET (item);
					/* gtk_widget_show(NODE_INFO(node)->extra); */
				}
			}
			break;
		case EGG_MENU_MERGE_POPUPS:
			break;
		case EGG_MENU_MERGE_MENUITEM:
			/* remove the proxy if it is of the wrong type ... */
			if (info->proxy &&  G_OBJECT_TYPE(info->proxy) !=
			    EGG_ACTION_GET_CLASS(info->action)->menu_item_type)
			{
				gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
						     info->proxy);
				info->proxy = NULL;
			}
			/* create proxy if needed ... */
			if (info->proxy == NULL)
			{
				GtkWidget *menushell;
				gint pos;

				if (find_menu_position(node, &menushell, &pos))
				{
					info->proxy = egg_action_create_menu_item(info->action);

					gtk_menu_shell_insert(GTK_MENU_SHELL(menushell),
							       info->proxy, pos);
				}
			}
			else
			{
				gtk_menu_item_set_submenu(GTK_MENU_ITEM(info->proxy), NULL);
				egg_action_connect_proxy (info->action, info->proxy);
			}
			break;
		case EGG_MENU_MERGE_TOOLITEM:
			/* remove the proxy if it is of the wrong type ... */
			if (info->proxy &&  G_OBJECT_TYPE(info->proxy) !=
			    EGG_ACTION_GET_CLASS(info->action)->toolbar_item_type)
			{
				gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
						     info->proxy);
				info->proxy = NULL;
			}
			/* create proxy if needed ... */
			if (info->proxy == NULL)
			{
				GtkWidget *toolbar;
				gint pos;

				if (find_toolbar_position(node, &toolbar, &pos))
				{
					info->proxy = egg_action_create_tool_item(info->action);

					egg_toolbar_insert_tool_item(EGG_TOOLBAR(toolbar),
								      EGG_TOOL_ITEM (info->proxy), pos);
				}
			}
			else
			{
				egg_action_connect_proxy(info->action, info->proxy);
			}
			break;
		case EGG_MENU_MERGE_SEPARATOR:
			if (NODE_INFO(node->parent)->type == EGG_MENU_MERGE_TOOLBAR ||
			    NODE_INFO(node->parent)->type == EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER)
			{
				GtkWidget *toolbar;
				gint pos;

				if (EGG_IS_SEPARATOR_TOOL_ITEM(info->proxy))
				{
					gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
							     info->proxy);
					info->proxy = NULL;
				}

				if (find_toolbar_position(node, &toolbar, &pos))
				{
					EggToolItem *item = egg_separator_tool_item_new();
					egg_toolbar_insert_tool_item(EGG_TOOLBAR(toolbar), item, pos);
					info->proxy = GTK_WIDGET(item);
					gtk_widget_show(info->proxy);
				}
			}
			else
			{
				GtkWidget *menushell;
				gint pos;

				if (GTK_IS_SEPARATOR_MENU_ITEM(info->proxy))
				{
					gtk_container_remove(GTK_CONTAINER(info->proxy->parent),
							     info->proxy);
					info->proxy = NULL;
				}

				if (find_menu_position(node, &menushell, &pos))
				{
					info->proxy = gtk_separator_menu_item_new();
					gtk_menu_shell_insert(GTK_MENU_SHELL(menushell),
							       info->proxy, pos);
					gtk_widget_show(info->proxy);
				}
			}
			break;
		}

		/* if this node has a widget, but it is the wrong type, remove it */
	}

recurse_children:
	/* process children */
	child = node->children;
	while (child)
	{
		GNode *current;

		current = child;
		child = current->next;
		update_node (self, current);
	}

	/* handle cleanup of dead nodes */
	if (node->children == NULL && NODE_INFO(node)->uifiles == NULL)
	{
		if (NODE_INFO(node)->proxy)
			gtk_widget_destroy(NODE_INFO(node)->proxy);
		if ((NODE_INFO(node)->type == EGG_MENU_MERGE_MENU_PLACEHOLDER ||
		     NODE_INFO(node)->type == EGG_MENU_MERGE_TOOLBAR_PLACEHOLDER) &&
		    NODE_INFO(node)->extra)
			gtk_widget_destroy(NODE_INFO(node)->extra);
		g_chunk_free(NODE_INFO(node), merge_node_chunk);
		g_node_destroy(node);
	}
}

static gboolean
do_updates(EggMenuMerge *self)
{
	/* this function needs to check through the tree for dirty nodes.
	 * For such nodes, it needs to do the following:
	 *
	 * 1) check if they are referenced by any loaded UI files anymore.
	 *    In which case, the proxy widget should be destroyed, unless
	 *    there are any subnodes.
	 *
	 * 2) lookup the action for this node again.  If it is different to
	 *    the current one (or if no previous action has been looked up),
	 *    the proxy is reconnected to the new action (or a new proxy widget
	 *    is created and added to the parent container).
	 */

	/* g_message("do_updates"); */

	update_node(self, self->root_node);

	self->update_tag = 0;
	return FALSE;
}

static void
egg_menu_merge_queue_update (EggMenuMerge *self)
{
	if (self->update_tag != 0)
		return;

	self->update_tag = g_idle_add((GSourceFunc)do_updates, self);
}

void
egg_menu_merge_ensure_update (EggMenuMerge *self)
{
	if (self->update_tag != 0)
	{
		g_source_remove(self->update_tag);
		do_updates(self);
	}
}

static gboolean
dirty_traverse_func (GNode *node, gpointer data)
{
	NODE_INFO(node)->dirty = TRUE;
	return FALSE;
}

static void
egg_menu_merge_dirty_all (EggMenuMerge *self)
{
	g_node_traverse(self->root_node, G_PRE_ORDER, G_TRAVERSE_ALL, -1,
			dirty_traverse_func, NULL);
	egg_menu_merge_queue_update(self);
}
