/*
 * $Id: XdgMenuReader.cpp 3516 2013-01-09 16:06:45Z karijes $
 *
 * Copyright (C) 2012 Sanel Zukan
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

#include <sys/types.h>
#include <dirent.h>
#include <string.h>
#include <stdlib.h>

#include <FL/Fl_Shared_Image.H>
#include <edelib/TiXml.h>
#include <edelib/Debug.h>
#include <edelib/StrUtil.h>
#include <edelib/Util.h>
#include <edelib/FileTest.h>
#include <edelib/Directory.h>
#include <edelib/DesktopFile.h>
#include <edelib/IconLoader.h>
#include <edelib/Nls.h>
#include <edelib/Run.h>

#include "DesktopEntry.h"
#include "MenuRules.h"
#include "XdgMenuReader.h"

EDELIB_NS_USING(DesktopFile)
EDELIB_NS_USING(IconLoader)
EDELIB_NS_USING(system_config_dirs)
EDELIB_NS_USING(system_data_dirs)
EDELIB_NS_USING(user_data_dir)
EDELIB_NS_USING(build_filename)
EDELIB_NS_USING(file_test)
EDELIB_NS_USING(str_ends)
EDELIB_NS_USING(run_async)
EDELIB_NS_USING(FILE_TEST_IS_DIR)
EDELIB_NS_USING(DESK_FILE_TYPE_DIRECTORY)
EDELIB_NS_USING(ICON_SIZE_SMALL)

#define DOT_OR_DOTDOT(base)    (base[0] == '.' && (base[1] == '\0' || (base[1] == '.' && base[2] == '\0')))
#define ELEMENT_IS(elem, val)  (strcmp(elem->Value(), val) == 0)
#define ELEMENT_GET_TEXT(elem) (elem->FirstChild() ? elem->FirstChild()->ToText() : NULL)

/* max. name size */
#define NAME_BUFSZ 128

/* do not allow empty menus to be shown */
#define NO_EMPTY_MENUS 1

/* in FLTK, a default size */
extern int FL_NORMAL_SIZE;

struct MenuParseContext;
struct MenuContext;

typedef list<MenuParseContext*> MenuParseList;
typedef list<MenuParseContext*>::iterator MenuParseListIt;

typedef list<MenuContext*> MenuContextList;
typedef list<MenuContext*>::iterator MenuContextListIt;

struct MenuParseContext {
	/* for <Deleted> <NotDeled> tags */
	bool deleted;

	/* for <OnlyUnallocated> and <NotOnlyUnallocated> (default) */
	bool only_unallocated;

	/* menu tag content */
	String *name;

	/* a stack of .directory names; the top one is used */
	StrList dir_files;

	/* directories where to find .directory file */
	StrList dir_dirs;

	/* a list of .desktop files */
	DesktopEntryList desk_files;

	/* include rules */
	MenuRulesList include_rules;

	/* exclude rules */
	MenuRulesList exclude_rules;

	/* nested menus */
	MenuParseList submenus;
};

struct MenuContext {
	/* menu label */
	String *name;

	/* should this entry be displayed */
	bool display_it;

	/* menu icon */
	String *icon;

	/* a list of .desktop files; at the same time also items in menu list */
	DesktopEntryList items;

	/* nested menus */
	MenuContextList submenus;
};

/* TODO: bug in edelib */
static bool menu_context_sorter(MenuContext* const& c1, MenuContext* const& c2) {
	return *(c1->name) < *(c2->name);
}

static MenuParseContext *menu_parse_context_new(void) {
	MenuParseContext *m = new MenuParseContext;
	m->name = NULL;
	m->deleted = false;
	m->only_unallocated = false;
	return m;
}

static void menu_parse_context_delete(MenuParseContext *m) {
	E_RETURN_IF_FAIL(m != NULL);

	delete m->name;

	/* delete rules */
	if(!m->include_rules.empty()) {
		MenuRulesListIt it = m->include_rules.begin(), it_end = m->include_rules.end();
		while(it != it_end) {
			menu_rules_delete(*it);
			it = m->include_rules.erase(it);
		}
	}

	if(!m->exclude_rules.empty()) {
		MenuRulesListIt it = m->exclude_rules.begin(), it_end = m->exclude_rules.end();
		while(it != it_end) {
			menu_rules_delete(*it);
			it = m->exclude_rules.erase(it);
		}
	}

	/* recurse for nested menus */
	if(!m->submenus.empty()) {
		MenuParseListIt it = m->submenus.begin(), it_end = m->submenus.end();

		while(it != it_end) {
			menu_parse_context_delete(*it);
			it = m->submenus.erase(it);
		}
	}

	delete m;
}

static void menu_parse_context_append_default_dir_dirs(MenuParseContext *ctx) {
	StrList lst;

	int ret = system_data_dirs(lst);
	if(ret < 1)
		return;

	StrListIt it = lst.begin(), it_end = lst.end();

	for(; it != it_end; ++it)
		ctx->dir_dirs.push_back(build_filename((*it).c_str(), "desktop-directories"));
}

static void menu_parse_context_append_desktop_files(MenuParseContext *ctx, const char *dir, const char *basedir) {
	DIR *ds = opendir(dir);
	if(!ds)
		return;

	dirent       *dp;
	DesktopEntry *entry;

	while((dp = readdir(ds)) != NULL) {
		/* skip dots and (possibly) hidden files */
		if(dp->d_name[0] == '.' || DOT_OR_DOTDOT(dp->d_name))
			continue;

		entry = new DesktopEntry;
		entry->assign_path(dir, dp->d_name, basedir);

		if(file_test(entry->get_path(), FILE_TEST_IS_DIR)) {
			/* recurse if needed; the spec said we must */
			menu_parse_context_append_desktop_files(ctx, entry->get_path(), basedir);

			/* delete it */
			delete entry;
			continue;
		}

		/* files must ends with this extension */
		if(str_ends(entry->get_path(), ".desktop")) {
			ctx->desk_files.push_back(entry);
		} else {
			/* clear non .desktop items */
			delete entry;
		}
	}

	closedir(ds);
}

static void menu_parse_context_append_desktop_files_from_xdg_data_dirs(MenuParseContext *ctx) {
	StrList lst;
	xdg_menu_applications_location(lst);

	StrListIt it = lst.begin(), it_end = lst.end();
	for(; it != it_end; ++it)
		menu_parse_context_append_desktop_files(ctx, it->c_str(), it->c_str());
}

static void scan_include_exclude_tag(TiXmlNode *elem, MenuRulesList &rules) {
	E_RETURN_IF_FAIL(elem != NULL);

	TiXmlNode *child;
	TiXmlText *txt;

	for(child = elem->FirstChildElement(); child; child = child->NextSibling()) {
		/* assure we do not include/exclude insinde include/exclude */
		if(ELEMENT_IS(child, "Include") || ELEMENT_IS(child, "Exclude")) {
			E_WARNING(E_STRLOC ": Nesting <Include> and <Exclude> tags are not supported\n");
			continue;
		}

		if(ELEMENT_IS(child, "Filename")) {
			txt = ELEMENT_GET_TEXT(child);
			menu_rules_append_rule(rules, MENU_RULES_OPERATOR_FILENAME, txt->Value());
			continue;
		}

		if(ELEMENT_IS(child, "Category")) {
			txt = ELEMENT_GET_TEXT(child);
			menu_rules_append_rule(rules, MENU_RULES_OPERATOR_CATEGORY, txt->Value());
			continue;
		}

		if(ELEMENT_IS(child, "All")) {
			menu_rules_append_rule(rules, MENU_RULES_OPERATOR_ALL, NULL);
			continue;
		}

		if(ELEMENT_IS(child, "And")) {
			MenuRules *and_rule = menu_rules_append_rule(rules, MENU_RULES_OPERATOR_AND, NULL);
			/* recurse */
			scan_include_exclude_tag(child, and_rule->subrules);
			continue;
		}

		if(ELEMENT_IS(child, "Or")) {
			MenuRules *or_rule = menu_rules_append_rule(rules, MENU_RULES_OPERATOR_OR, NULL);
			/* recurse */
			scan_include_exclude_tag(child, or_rule->subrules);
			continue;
		}

		if(ELEMENT_IS(child, "Not")) {
			MenuRules *not_rule = menu_rules_append_rule(rules, MENU_RULES_OPERATOR_NOT, NULL);
			/* recurse */
			scan_include_exclude_tag(child, not_rule->subrules);
			continue;
		}
	}
}

static void scan_menu_tag(TiXmlNode *elem, MenuParseList &parse_list) {
	E_RETURN_IF_FAIL(elem != NULL);

	TiXmlText *txt;
	bool got_default_app_dirs = false, got_default_dir_dirs = false;

	MenuParseContext *ctx = menu_parse_context_new();

	for(elem = elem->FirstChildElement(); elem; elem = elem->NextSibling()) {
		/* in case we got '<Menu>' as submenu, dive in it recursively and fill submenus */
		if(ELEMENT_IS(elem, "Menu"))
			scan_menu_tag(elem, ctx->submenus);

		if(ELEMENT_IS(elem, "Name")) {
			txt = ELEMENT_GET_TEXT(elem);
			if(txt && !ctx->name)
				ctx->name = new String(txt->Value());

			continue;
		}
		
		if(ELEMENT_IS(elem, "Directory")) {
			txt = ELEMENT_GET_TEXT(elem);
			/* entries must ends with .directory */
			if(txt && str_ends(txt->Value(), ".directory")) {
				/* push it at the top */
				ctx->dir_files.push_front(txt->Value());
			}

			continue;
		} 
		
		if(ELEMENT_IS(elem, "AppDir")) {
			txt = ELEMENT_GET_TEXT(elem);
			if(txt)
				menu_parse_context_append_desktop_files(ctx, txt->Value(), NULL);

			continue;
		} 
		
		if(ELEMENT_IS(elem, "DirectoryDir")) {
			txt = ELEMENT_GET_TEXT(elem);
			if(txt) {
				/* push it at the top */
				ctx->dir_dirs.push_front(txt->Value());
			}

			continue;
		}

		/* spec: '<DefaultAppDirs>' expands to $XDG_DATA_DIRS/applications */
		if(ELEMENT_IS(elem, "DefaultAppDirs")) {
			if(!got_default_app_dirs) {
				menu_parse_context_append_desktop_files_from_xdg_data_dirs(ctx);
				/* scan it only once */
				got_default_app_dirs = true;
			}

			continue;
		}

		if(ELEMENT_IS(elem, "DefaultDirectoryDirs")) {
			if(!got_default_dir_dirs) {
				menu_parse_context_append_default_dir_dirs(ctx);
				got_default_dir_dirs = true;
			}

			continue;
		}

		if(ELEMENT_IS(elem, "Include")) {
			scan_include_exclude_tag(elem, ctx->include_rules);
			continue;
		}

		if(ELEMENT_IS(elem, "Exclude")) {
			scan_include_exclude_tag(elem, ctx->exclude_rules);
			continue;
		}

		if(ELEMENT_IS(elem, "Deleted")) {
			ctx->deleted = true;
			continue;
		}

		if(ELEMENT_IS(elem, "NotDeleted")) {
			ctx->deleted = false;
			continue;
		}

		if(ELEMENT_IS(elem, "OnlyUnallocated")) {
			ctx->only_unallocated = true;
			continue;
		}

		if(ELEMENT_IS(elem, "NotOnlyUnallocated")) {
			ctx->only_unallocated = false;
			continue;
		}
	}

	parse_list.push_back(ctx);
}

static bool menu_context_construct_name_and_get_icon(MenuParseContext *m, 
													 MenuParseContext *top, 
													 String **ret_name, 
													 String **ret_icon,
													 bool *should_be_displayed)
{
	E_RETURN_VAL_IF_FAIL(m != NULL, false);

	*ret_name = *ret_icon = NULL;
	*should_be_displayed = true;

	if(!m->dir_files.empty()) {
		/*
		 * We have two locations where are keeping directory list: node specific and top node
		 * specific, where often is put <DefaultDirectoryDirs/> tag. Here, first we will look in
		 * node specific directory list, then will go in top node <DefaultDirectoryDirs/>.
		 */
		StrListIt dir_it = m->dir_dirs.begin(), dir_it_end = m->dir_dirs.end();

		/* this list has 'stack-ed' items; the last one is on top */
		StrListIt file_it, file_it_end = m->dir_files.end();

		DesktopFile df;
		String      tmp;

		/* first try specific directory list */
		for(; dir_it != dir_it_end; ++dir_it) {
			for(file_it = m->dir_files.begin(); file_it != file_it_end; ++file_it) {
				tmp = build_filename((*dir_it).c_str(), (*file_it).c_str());
				//E_DEBUG("==> %s\n", tmp.c_str());

				/* load it and see if it is real .desktop file */
				df.load(tmp.c_str());

				if(df && (df.type() == DESK_FILE_TYPE_DIRECTORY)) {
					/* check if it can be displayed */
					if(df.no_display())
						*should_be_displayed = false;

					char buf[NAME_BUFSZ];

					/* try icon first */
					if(!(*ret_icon) && df.icon(buf, NAME_BUFSZ))
						*ret_icon = new String(buf);

					/* then name, so we can quit nicely */
					if(!(*ret_name) && df.name(buf, NAME_BUFSZ)) {
						*ret_name = new String(buf);
						return true;
					}
				}
			}
		}

		/* now try default ones */
		dir_it = top->dir_dirs.begin(), dir_it_end = top->dir_dirs.end();

		for(; dir_it != dir_it_end; ++dir_it) {
			for(file_it = m->dir_files.begin(); file_it != file_it_end; ++file_it) {
				tmp = build_filename((*dir_it).c_str(), (*file_it).c_str());
				//E_DEBUG("++> %s\n", tmp.c_str());

				/* load it and see if it is real .desktop file */
				df.load(tmp.c_str());
				if(df && (df.type() == DESK_FILE_TYPE_DIRECTORY)) {
					/* check if it can be displayed */
					if(df.no_display())
						*should_be_displayed = false;

					char buf[NAME_BUFSZ];

					/* try icon first */
					if(!(*ret_icon) && df.icon(buf, NAME_BUFSZ))
						*ret_icon = new String(buf);

					/* then name, so we can quit nicely */
					if(!(*ret_name) && df.name(buf, NAME_BUFSZ)) {
						*ret_name = new String(buf);
						return true;
					}
				}
			}
		}
	}	

	E_RETURN_VAL_IF_FAIL(m->name != NULL, false);

	/* if there are no files and can be displayed, use context name; let icon name be NULL */
	*ret_name = new String(*(m->name));
	return true;
}

static void apply_include_rules(MenuContext *ctx, DesktopEntryList &items, MenuRulesList &rules) {
	if(items.empty() || rules.empty())
		return;

	MenuRulesListIt     rit, rit_end = rules.end();
	DesktopEntryListIt  it = items.begin(), it_end = items.end();

	DesktopEntry *entry;
	bool         eval_true;

	for(; it != it_end; ++it) {
		for(rit = rules.begin(); rit != rit_end; ++rit) {
			entry = *it;

			eval_true = menu_rules_eval(*rit, entry);

			/* append entry if matches to the rule, and mark it as allocated */
			if(eval_true) {
				entry->mark_as_allocated();
				ctx->items.push_back(entry);

				/* do not scan rules any more; go to the next item */
				break;
			}
		}
	}
}

static void apply_exclude_rules(DesktopEntryList& items, MenuRulesList &rules) {
	if(items.empty() || rules.empty())
		return;

	MenuRulesListIt    rit, rit_end = rules.end();
	DesktopEntryListIt it = items.begin(), it_end = items.end();
	bool eval_true;

	while(it != it_end) {
		eval_true = false;

		for(rit = rules.begin(); rit != rit_end; ++rit) {
			eval_true = menu_rules_eval(*rit, *it);

			if(eval_true) {
				/* pop entry if matches */
				it = items.erase(it);
				break;
			}
		}

		if(!eval_true) 
			++it;
	}
}

#if NO_EMPTY_MENUS
/* forward decl */
static void menu_context_delete(MenuContext *c);
#endif

static MenuContext *menu_parse_context_to_menu_context(MenuParseContext *m, 
													   MenuParseContext *top,
													   DesktopEntryList *all_unallocated) 
{
	E_RETURN_VAL_IF_FAIL(m != NULL, NULL);

	/* make sure we are not processing only_unallocated node when not get all_unallocated */
	if(m->only_unallocated && !all_unallocated)
		return NULL;

	/* 
	 * figure out the name first; if returns false, either menu should not be displayed, or something
	 * went wrong
	 */
	String *n = NULL, *ic = NULL;
	bool   should_be_displayed = false;

	if(!menu_context_construct_name_and_get_icon(m, top, &n, &ic, &should_be_displayed))
		return NULL;

	/* 
	 * nodes marked as 'NoDisplay' (from .directory file) or '<Deleted>' (from applications.menu) must
	 * be processed as ordinary nodes, since this operation will correctly setup allocated
	 * (<OnlyUnallocated> and <NotOnlyUnallocated>) entries
	 */
	if(m->deleted)
		should_be_displayed = false;

	/* assure we got name here; icon can be NULL */
	E_RETURN_VAL_IF_FAIL(n != NULL, NULL);

	MenuContext *ctx = new MenuContext;
	ctx->name = n;
	ctx->icon = ic;
	ctx->display_it = should_be_displayed;
	//E_DEBUG("+ Menu: %s %i\n", ctx->name->c_str(), m->include_rules.size());

	/* fill MenuContext items, depending on what list was passed */
	if(all_unallocated) {
		apply_include_rules(ctx, *all_unallocated, m->include_rules);
	} else {
		apply_include_rules(ctx, m->desk_files, m->include_rules);
		/* check the rules for top list, but make sure we are not applying them on the same node again */
		if(m != top)
			apply_include_rules(ctx, top->desk_files, m->include_rules);
	}

	/* pop filled MenuContext items if match the rule */
	apply_exclude_rules(ctx->items, m->exclude_rules);

	//E_DEBUG("- Menu: %s %i\n", ctx->name->c_str(), ctx->items.size());
	
	/* sort entries via their full names */
	desktop_entry_list_sort(ctx->items);
	
	/* process submenus */
	if(!m->submenus.empty()) {
		MenuParseListIt mit = m->submenus.begin(), mit_end = m->submenus.end();
		MenuContext *sub_ctx;

		for(; mit != mit_end; ++mit) {
			sub_ctx = menu_parse_context_to_menu_context(*mit, top, all_unallocated);

			if(sub_ctx)
				ctx->submenus.push_back(sub_ctx);
		}
	}

#if NO_EMPTY_MENUS
	/* do not allow empty menus */
	if(ctx->items.empty() && ctx->submenus.empty()) {
		menu_context_delete(ctx);
		ctx = NULL;
	}
#endif

	return ctx;
}

static void menu_context_delete(MenuContext *c) {
	E_RETURN_IF_FAIL(c != NULL);

	if(!c->submenus.empty()) {
		MenuContextListIt it = c->submenus.begin(), it_end = c->submenus.end();
		for(; it != it_end; ++it)
			menu_context_delete(*it);
	}

	c->items.clear();
	delete c->name;
	delete c->icon;
	delete c;
}

static void menu_parse_context_list_get_all_unallocated_desk_files(MenuParseList &parse_list, DesktopEntryList &ret) {
	if(parse_list.empty())
		return;

	MenuParseListIt     it = parse_list.begin(), it_end = parse_list.end();
	DesktopEntryListIt  dit, dit_end;
	MenuParseContext    *parse_ctx;

	for(; it != it_end; ++it) {
		parse_ctx = *it;

		dit = parse_ctx->desk_files.begin();
		dit_end = parse_ctx->desk_files.end();

		for(; dit != dit_end; ++dit) {
			if((*dit)->is_allocated() == false)
				ret.push_back(*dit);
		}

		/* recurse */
		menu_parse_context_list_get_all_unallocated_desk_files(parse_ctx->submenus, ret);
	}
}

static void menu_context_list_sort(MenuContextList &lst) {
	if(lst.empty())
		return;

	lst.sort(menu_context_sorter);

	MenuContextListIt it = lst.begin(), it_end = lst.end();
	for(; it != it_end; ++it)
		menu_context_list_sort((*it)->submenus);
}

static void menu_parse_context_list_to_menu_context_list(MenuParseList &parse_list, 
														 MenuContextList &ret)
{
	MenuParseListIt it = parse_list.begin(), it_end = parse_list.end();
	MenuParseContext *parse_ctx;
	MenuContext      *ctx;

	for(; it != it_end; ++it) {
		parse_ctx = *it;

		/* remove duplicate id's */
		desktop_entry_list_remove_duplicates(parse_ctx->desk_files);

		/* read all .desktop files from disk */
		desktop_entry_list_load_all(parse_ctx->desk_files);

		/* now convert it to usable menu node */
		ctx = menu_parse_context_to_menu_context(parse_ctx, parse_ctx, NULL);

		if(ctx)
			ret.push_back(ctx);
	}

	/* now, pickup all unallocated items */
	DesktopEntryList all_unallocated;
	menu_parse_context_list_get_all_unallocated_desk_files(parse_list, all_unallocated);

	/* 
	 * second pass; process unallocated items, but put them in second list that will later be
	 * merged; this is to preserve the order got in the first list
	 */
	MenuContextList unallocated_list;

	for(it = parse_list.begin(); it != it_end; ++it) {
		parse_ctx = *it;

		/* now convert it to usable menu node */
		ctx = menu_parse_context_to_menu_context(parse_ctx, parse_ctx, &all_unallocated);

		if(ctx)
			unallocated_list.push_back(ctx);
	}

	/* 
	 * both list have the same root node, so we skip the first node and merge below it; the first node is 
	 * top level <Menu> and is often only menu name and description
	 */
	E_RETURN_IF_FAIL(ret.size() == 1);
	E_RETURN_IF_FAIL(unallocated_list.size() == 1);

	MenuContext *head = ret.front(), *unallocated_head = unallocated_list.front();
	MenuContextListIt uit = unallocated_head->submenus.begin(), uit_end = unallocated_head->submenus.end();

	for(; uit != uit_end; ++uit)
		head->submenus.push_back(*uit);

	/* sort everthing at the end */
	menu_context_list_sort(ret);
}

/* 
 * Count the number of items in each submenu + submenu node itself; used to allocate 
 * array for edelib::MenuItem list.
 */
static unsigned int menu_context_list_count(MenuContextList &lst) {
	if(lst.empty())
		return 0;

	unsigned int ret = lst.size();

	MenuContextListIt it = lst.begin(), it_end = lst.end();
	MenuContext *cc;

	for(; it != it_end; ++it) {
		cc = *it;
		ret += cc->items.size();

		ret += menu_context_list_count(cc->submenus);

		/* 
		 * a room for NULL to deduce submenus in edelib::MenuItem, no matter if submenus are empty
		 * in case empty menus are going to be displayed
		 */
		ret += 1;
	}

	return ret;
}

static void menu_all_parse_lists_clear(MenuParseList &parse_list, MenuContextList &ctx_list) {
	MenuContextListIt cit = ctx_list.begin(), cit_end = ctx_list.end();
	MenuParseListIt   pit = parse_list.begin(), pit_end = parse_list.end();

	MenuParseContext *cc;

	while(cit != cit_end) {
		menu_context_delete(*cit);
		cit = ctx_list.erase(cit);
	}

	while(pit != pit_end) {
		cc = *pit;
		/* 
		 * Desktop entries are shared among MenuContext and MenuParseContext, so they
		 * must be explicitly deleted. This sharing depends on 'Include' rules, so some MenuParseContext
		 * entries are on MenuContext list and all MenuContext entries are in MenuParseContext.
		 */
		DesktopEntryListIt it = cc->desk_files.begin(), it_end = cc->desk_files.end();
		while(it != it_end) {
			delete *it;
			it = cc->desk_files.erase(it);
		}

		menu_parse_context_delete(cc);
		pit = parse_list.erase(pit);
	}
}

static TiXmlNode *load_menu_file(TiXmlDocument &doc) {
	char   *menu_prefix = getenv("XDG_MENU_PREFIX");
	String  menu_file;

	if(menu_prefix) {
		menu_file = menu_prefix;
		menu_file += "applications.menu";
	} else {
		menu_file = "applications.menu";
	}

	StrList paths;
	if(system_config_dirs(paths) < 1)
		return NULL;

	String    tmp;
	StrListIt it = paths.begin(), it_end = paths.end();

	for(; it != it_end; ++it) {
		tmp = build_filename((*it).c_str(), "menus", menu_file.c_str());

		if(doc.LoadFile(tmp.c_str()))
			goto done;
	}

	return NULL;

done:
	return doc.FirstChild("Menu");
}

static void menu_context_list_dump(MenuContextList &lst) {
	if(lst.empty())
		return;

	MenuContextListIt  it = lst.begin(), it_end = lst.end();
	DesktopEntryListIt ds, de;

	for(; it != it_end; ++it) {
		if((*it)->display_it == false)
			continue;

		ds = (*it)->items.begin();
		de = (*it)->items.end();

		/* print each desktop entry with menu name */
		for(; ds != de; ++ds) {
			printf("%s/\t%s\t%s\n", (*it)->name->c_str(),
								    (*ds)->get_id(),
								    (*ds)->get_path());
		}

		menu_context_list_dump((*it)->submenus);
	}
}

static void menu_all_parse_lists_load(MenuParseList &parse_list, MenuContextList &content) {
	/* 
	 * TiXmlDocument object must be used externaly, so as long as this object is
	 * alive, the whole XML tree is alive too (see DOM reference).
	 */
	TiXmlDocument doc;

	TiXmlNode *elem = load_menu_file(doc);
	if(!elem)
		return;

	/* parse XML file */
	scan_menu_tag(elem, parse_list);

	/* convert it to our list */
	menu_parse_context_list_to_menu_context_list(parse_list, content);
}

void xdg_menu_dump_for_test_suite(void) {
	MenuParseList   pl;
	MenuContextList cl;

	/* load everything */
	menu_all_parse_lists_load(pl, cl);

	menu_context_list_dump(cl);

	/* clear everything */
	menu_all_parse_lists_clear(pl, cl);
}

/* public API */

struct XdgMenuContent {
	MenuItem *fltk_menu;

	/*
	 * We are keeping them as fltk_menu references some objects, like text.
	 * This is since FLTK menu can't free label strings as MenuItem is plain struct.
	 */
	MenuParseList   parse_list;
	MenuContextList context_list;
};

static void item_cb(Fl_Widget*, void *en) {
	DesktopEntry *entry = (DesktopEntry*)en;
	run_async("ede-launch %s", entry->get_exec());

	E_DEBUG(E_STRLOC ": ede-launch %s\n", entry->get_exec());
}

static void logout_cb(Fl_Widget*, void*) {
	run_async("ede-shutdown");
}

static unsigned int construct_edelib_menu(MenuContextList &lst, MenuItem *mi, unsigned int pos) {
	if(lst.empty())
		return pos;

	MenuContextListIt it = lst.begin(), it_end = lst.end();
	MenuContext *cc;

	DesktopEntryListIt ds, de;

	unsigned long initial_pos = pos;

	for(; it != it_end; ++it) {
		cc = *it;

		if(!cc->display_it)
			continue;

		mi[pos].text = cc->name->c_str();

		/* every MenuContext is submenu for itself */
		mi[pos].flags = FL_SUBMENU;

		//E_DEBUG("{%s submenu}\n", mi[pos].text);

		/* some default values that must be filled */
		mi[pos].shortcut_ = 0;
		mi[pos].callback_ = 0;
		mi[pos].user_data_ = 0;
		mi[pos].labeltype_ = FL_NORMAL_LABEL;
		mi[pos].labelfont_ = FL_HELVETICA;
		mi[pos].labelsize_ = FL_NORMAL_SIZE;
		mi[pos].labelcolor_ = FL_BLACK;

		MenuItem::init_extensions(&mi[pos]);

		/* set image for menu */
		if(cc->icon && IconLoader::inited()) {
			Fl_Image *img = IconLoader::get(cc->icon->c_str(), ICON_SIZE_SMALL);
			mi[pos].image(img);
		}

		/* a room for an item */
		pos++;

		/* try with nested submenus first, so submenus be before desktop entries in current menu node */
		pos = construct_edelib_menu(cc->submenus, mi, pos);

		/* now, add the real items if they exists*/
		if(!cc->items.empty()) {
			ds = cc->items.begin();
			de = cc->items.end();

			for(; ds != de; ++ds, ++pos) {
				mi[pos].text = (*ds)->get_name();
				mi[pos].flags = 0;
				
				//E_DEBUG("  {%s item}\n", mi[pos].text);

				/* some default values that must be filled */
				mi[pos].shortcut_ = 0;

				/* set callback and callback data to be current entry */
				mi[pos].callback_ = item_cb;
				mi[pos].user_data_ = *ds;

				mi[pos].labeltype_ = FL_NORMAL_LABEL;
				mi[pos].labelfont_ = FL_HELVETICA;
				mi[pos].labelsize_ = FL_NORMAL_SIZE;
				mi[pos].labelcolor_ = FL_BLACK;
				MenuItem::init_extensions(&mi[pos]);

				/* set image for menu item*/
				if((*ds)->get_icon() && IconLoader::inited()) {
					Fl_Image *img = IconLoader::get((*ds)->get_icon(), ICON_SIZE_SMALL);
					mi[pos].image(img);
				}

				/* set tooltip if we have; it is actually a comment from .desktop file */
				mi[pos].tooltip((*ds)->get_comment());
			}
		}

		/* to inject Logout button */
		if(initial_pos == 0) {
			//E_DEBUG("  {Logout item}\n");

			mi[pos].text = _("Logout");

			if(pos) 
				mi[pos - 1].flags |= FL_MENU_DIVIDER;

			mi[pos].flags = 0;
			mi[pos].shortcut_ = 0;
			mi[pos].labeltype_ = FL_NORMAL_LABEL;
			mi[pos].labelfont_ = FL_HELVETICA;
			mi[pos].labelsize_ = FL_NORMAL_SIZE;
			mi[pos].labelcolor_ = FL_BLACK;
			MenuItem::init_extensions(&mi[pos]);

			/* set callback and callback data to be current entry */
			mi[pos].callback_ = logout_cb;
			mi[pos].user_data_ = 0;
	
			if(IconLoader::inited()) {
				Fl_Image *img = IconLoader::get("system-log-out", ICON_SIZE_SMALL);
				mi[pos].image(img);
			}

			pos++;
		}

		/* end this menu */
		mi[pos].text = NULL;
		MenuItem::init_extensions(&mi[pos]);

		//E_DEBUG("{0}\n");

		/* make a room for the next item */
		pos++;

	}

	/* return position to next item */
	return pos;
}

void xdg_menu_applications_location(StrList &lst) {
	lst.clear();

	if(system_data_dirs(lst) < 1)
		return;

	StrListIt it = lst.begin(), it_end = lst.end();

	for(; it != it_end; ++it)
		*it = build_filename(it->c_str(), "applications");

	/* 
	 * Add user directory too; the spec is unclear about this, but official menu spec tests
	 * requires it. Also, users will be able to add menu items without superuser permissions.
	 */
	String user_dir = user_data_dir();
	lst.push_back(build_filename(user_dir.c_str(), "applications"));
}

XdgMenuContent *xdg_menu_load(void) {
	XdgMenuContent *content = new XdgMenuContent();

	/* load everything */
	menu_all_parse_lists_load(content->parse_list, content->context_list);

	unsigned int sz = menu_context_list_count(content->context_list);
	E_RETURN_VAL_IF_FAIL(sz > 0, NULL);

	MenuItem *mi = new MenuItem[sz + 2]; /* plus logout + ending NULL */

	unsigned int pos = construct_edelib_menu(content->context_list, mi, 0);
	mi[pos].text = NULL;

	/* 
	 * MenuItem does not have constructor, so everywhere where we access MenuItem object, image
	 * member must be NULL-ed too
	 */
	MenuItem::init_extensions(&mi[pos]);

	E_ASSERT(pos <= sz + 2);

	content->fltk_menu = mi;
	return content;
}

void xdg_menu_delete(XdgMenuContent *m) {
	E_RETURN_IF_FAIL(m != NULL);

	delete [] m->fltk_menu;
	menu_all_parse_lists_clear(m->parse_list, m->context_list);
	delete m;
}

MenuItem *xdg_menu_to_fltk_menu(XdgMenuContent *m) {
	E_RETURN_VAL_IF_FAIL(m != NULL, NULL);
	return m->fltk_menu;
}
