"""
Implementation of the XDG Menu Specification
http://standards.freedesktop.org/menu-spec/

Example code:

from xdg.Menu import parse, Menu, MenuEntry

def print_menu(menu, tab=0):
  for submenu in menu.Entries:
    if isinstance(submenu, Menu):
      print ("\t" * tab) + unicode(submenu)
      print_menu(submenu, tab+1)
    elif isinstance(submenu, MenuEntry):
      print ("\t" * tab) + unicode(submenu.DesktopEntry)

print_menu(parse())
"""

import os
import locale
import subprocess
import ast
try:
    import xml.etree.cElementTree as etree
except ImportError:
    import xml.etree.ElementTree as etree

from .BaseDirectory import xdg_data_dirs, xdg_config_dirs
from . import DesktopEntry, Locale, Config
from .Exceptions import ParsingError
from .util import PY3


def _strxfrm(s):
    """Wrapper around locale.strxfrm that accepts unicode strings on Python 2.

    See Python bug #2481.
    """
    if (not PY3) and isinstance(s, unicode):
        s = s.encode('utf-8')
    return locale.strxfrm(s)


DELETED = "Deleted"
NO_DISPLAY = "NoDisplay"
HIDDEN = "Hidden"
EMPTY = "Empty"
NOT_SHOW_IN = "NotShowIn"
NO_EXEC = "NoExec"


class Menu:
    """Menu containing sub menus under menu.Entries

    Contains both Menu and MenuEntry items.
    """
    def __init__(self):
        # Public stuff
        self.Name = ""
        self.Directory = None
        self.Entries = []
        self.Doc = ""
        self.Filename = ""
        self.Depth = 0
        self.Parent = None
        self.NotInXml = False

        # Can be True, False, DELETED, NO_DISPLAY, HIDDEN, EMPTY or NOT_SHOW_IN
        self.Show = True
        self.Visible = 0

        # Private stuff, only needed for parsing
        self.AppDirs = []
        self.DefaultLayout = None
        self.Deleted = None
        self.Directories = []
        self.DirectoryDirs = []
        self.Layout = None
        self.MenuEntries = []
        self.Moves = []
        self.OnlyUnallocated = None
        self.Rules = []
        self.Submenus = []

    def __str__(self):
        return self.Name

    def __add__(self, other):
        for dir in other.AppDirs:
            self.AppDirs.append(dir)

        for dir in other.DirectoryDirs:
            self.DirectoryDirs.append(dir)

        for directory in other.Directories:
            self.Directories.append(directory)

        if other.Deleted is not None:
            self.Deleted = other.Deleted

        if other.OnlyUnallocated is not None:
            self.OnlyUnallocated = other.OnlyUnallocated

        if other.Layout:
            self.Layout = other.Layout

        if other.DefaultLayout:
            self.DefaultLayout = other.DefaultLayout

        for rule in other.Rules:
            self.Rules.append(rule)

        for move in other.Moves:
            self.Moves.append(move)

        for submenu in other.Submenus:
            self.addSubmenu(submenu)

        return self

    # FIXME: Performance: cache getName()
    def __cmp__(self, other):
        return locale.strcoll(self.getName(), other.getName())

    def _key(self):
        """Key function for locale-aware sorting."""
        return _strxfrm(self.getName())

    def __lt__(self, other):
        try:
            other = other._key()
        except AttributeError:
            pass
        return self._key() < other

    def __eq__(self, other):
        try:
            return self.Name == unicode(other)
        except NameError:  # unicode() becomes str() in Python 3
            return self.Name == str(other)

    """ PUBLIC STUFF """
    def getEntries(self, show_hidden=False):
        """Interator for a list of Entries visible to the user."""
        for entry in self.Entries:
            if show_hidden:
                yield entry
            elif entry.Show is True:
                yield entry

    # FIXME: Add searchEntry/seaqrchMenu function
    # search for name/comment/genericname/desktopfileid
    # return multiple items

    def getMenuEntry(self, desktopfileid, deep=False):
        """Searches for a MenuEntry with a given DesktopFileID."""
        for menuentry in self.MenuEntries:
            if menuentry.DesktopFileID == desktopfileid:
                return menuentry
        if deep:
            for submenu in self.Submenus:
                submenu.getMenuEntry(desktopfileid, deep)

    def getMenu(self, path):
        """Searches for a Menu with a given path."""
        array = path.split("/", 1)
        for submenu in self.Submenus:
            if submenu.Name == array[0]:
                if len(array) > 1:
                    return submenu.getMenu(array[1])
                else:
                    return submenu

    def getPath(self, org=False, toplevel=False):
        """Returns this menu's path in the menu structure."""
        parent = self
        names = []
        while 1:
            if org:
                names.append(parent.Name)
            else:
                names.append(parent.getName())
            if parent.Depth > 0:
                parent = parent.Parent
            else:
                break
        names.reverse()
        path = ""
        if not toplevel:
            names.pop(0)
        for name in names:
            path = os.path.join(path, name)
        return path

    def getName(self):
        """Returns the menu's localised name."""
        try:
            return self.Directory.DesktopEntry.getName()
        except AttributeError:
            return self.Name

    def getGenericName(self):
        """Returns the menu's generic name."""
        try:
            return self.Directory.DesktopEntry.getGenericName()
        except AttributeError:
            return ""

    def getComment(self):
        """Returns the menu's comment text."""
        try:
            return self.Directory.DesktopEntry.getComment()
        except AttributeError:
            return ""

    def getIcon(self):
        """Returns the menu's icon, filename or simple name"""
        try:
            return self.Directory.DesktopEntry.getIcon()
        except AttributeError:
            return ""

    def sort(self):
        self.Entries = []
        self.Visible = 0

        for submenu in self.Submenus:
            submenu.sort()

        _submenus = set()
        _entries = set()

        for order in self.Layout.order:
            if order[0] == "Filename":
                _entries.add(order[1])
            elif order[0] == "Menuname":
                _submenus.add(order[1])

        for order in self.Layout.order:
            if order[0] == "Separator":
                separator = Separator(self)
                if len(self.Entries) > 0 and isinstance(self.Entries[-1], Separator):
                    separator.Show = False
                self.Entries.append(separator)
            elif order[0] == "Filename":
                menuentry = self.getMenuEntry(order[1])
                if menuentry:
                    self.Entries.append(menuentry)
            elif order[0] == "Menuname":
                submenu = self.getMenu(order[1])
                if submenu:
                    if submenu.Layout.inline:
                        self.merge_inline(submenu)
                    else:
                        self.Entries.append(submenu)
            elif order[0] == "Merge":
                if order[1] == "files" or order[1] == "all":
                    self.MenuEntries.sort()
                    for menuentry in self.MenuEntries:
                        if menuentry.DesktopFileID not in _entries:
                            self.Entries.append(menuentry)
                elif order[1] == "menus" or order[1] == "all":
                    self.Submenus.sort()
                    for submenu in self.Submenus:
                        if submenu.Name not in _submenus:
                            if submenu.Layout.inline:
                                self.merge_inline(submenu)
                            else:
                                self.Entries.append(submenu)

        # getHidden / NoDisplay / OnlyShowIn / NotOnlyShowIn / Deleted / NoExec
        for entry in self.Entries:
            entry.Show = True
            self.Visible += 1
            if isinstance(entry, Menu):
                if entry.Deleted is True:
                    entry.Show = DELETED
                    self.Visible -= 1
                elif isinstance(entry.Directory, MenuEntry):
                    if entry.Directory.DesktopEntry.getNoDisplay():
                        entry.Show = NO_DISPLAY
                        self.Visible -= 1
                    elif entry.Directory.DesktopEntry.getHidden():
                        entry.Show = HIDDEN
                        self.Visible -= 1
            elif isinstance(entry, MenuEntry):
                if entry.DesktopEntry.getNoDisplay():
                    entry.Show = NO_DISPLAY
                    self.Visible -= 1
                elif entry.DesktopEntry.getHidden():
                    entry.Show = HIDDEN
                    self.Visible -= 1
                elif entry.DesktopEntry.getTryExec() and not entry.DesktopEntry.findTryExec():
                    entry.Show = NO_EXEC
                    self.Visible -= 1
                elif xdg.Config.windowmanager:
                    if (entry.DesktopEntry.OnlyShowIn != [] and (
                            xdg.Config.windowmanager not in entry.DesktopEntry.OnlyShowIn
                        )
                    ) or (
                        xdg.Config.windowmanager in entry.DesktopEntry.NotShowIn
                    ):
                        entry.Show = NOT_SHOW_IN
                        self.Visible -= 1
            elif isinstance(entry, Separator):
                self.Visible -= 1
        # remove separators at the beginning and at the end
        if len(self.Entries) > 0:
            if isinstance(self.Entries[0], Separator):
                self.Entries[0].Show = False
        if len(self.Entries) > 1:
            if isinstance(self.Entries[-1], Separator):
                self.Entries[-1].Show = False

        # show_empty tag
        for entry in self.Entries[:]:
            if isinstance(entry, Menu) and not entry.Layout.show_empty and entry.Visible == 0:
                entry.Show = EMPTY
                self.Visible -= 1
                if entry.NotInXml is True:
                    self.Entries.remove(entry)

    """ PRIVATE STUFF """
    def addSubmenu(self, newmenu):
        for submenu in self.Submenus:
            if submenu == newmenu:
                submenu += newmenu
                break
        else:
            self.Submenus.append(newmenu)
            newmenu.Parent = self
            newmenu.Depth = self.Depth + 1

    # inline tags
    def merge_inline(self, submenu):
        """Appends a submenu's entries to this menu
        See the <Menuname> section of the spec about the "inline" attribute
        """
        if len(submenu.Entries) == 1 and submenu.Layout.inline_alias:
            menuentry = submenu.Entries[0]
            menuentry.DesktopEntry.set("Name", submenu.getName(), locale=True)
            menuentry.DesktopEntry.set("GenericName", submenu.getGenericName(), locale=True)
            menuentry.DesktopEntry.set("Comment", submenu.getComment(), locale=True)
            self.Entries.append(menuentry)
        elif len(submenu.Entries) <= submenu.Layout.inline_limit or submenu.Layout.inline_limit == 0:
            if submenu.Layout.inline_header:
                header = Header(submenu.getName(), submenu.getGenericName(), submenu.getComment())
                self.Entries.append(header)
            for entry in submenu.Entries:
                self.Entries.append(entry)
        else:
            self.Entries.append(submenu)


class Move:
    "A move operation"
    def __init__(self, old="", new=""):
        self.Old = old
        self.New = new

    def __cmp__(self, other):
        return cmp(self.Old, other.Old)


class Layout:
    "Menu Layout class"
    def __init__(self, show_empty=False, inline=False, inline_limit=4,
                 inline_header=True, inline_alias=False):
        self.show_empty = show_empty
        self.inline = inline
        self.inline_limit = inline_limit
        self.inline_header = inline_header
        self.inline_alias = inline_alias
        self._order = []
        self._default_order = [
            ['Merge', 'menus'],
            ['Merge', 'files']
        ]

    @property
    def order(self):
        return self._order if self._order else self._default_order

    @order.setter
    def order(self, order):
        self._order = order


class Rule:
    """Include / Exclude Rules Class"""

    TYPE_INCLUDE, TYPE_EXCLUDE = 0, 1

    @classmethod
    def fromFilename(cls, type, filename):
        tree = ast.Expression(
            body=ast.Compare(
                left=ast.Str(filename),
                ops=[ast.Eq()],
                comparators=[ast.Attribute(
                    value=ast.Name(id='menuentry', ctx=ast.Load()),
                    attr='DesktopFileID',
                    ctx=ast.Load()
                )]
            ),
            lineno=1, col_offset=0
        )
        ast.fix_missing_locations(tree)
        rule = Rule(type, tree)
        return rule

    def __init__(self, type, expression):
        # Type is TYPE_INCLUDE or TYPE_EXCLUDE
        self.Type = type
        # expression is ast.Expression
        self.expression = expression
        self.code = compile(self.expression, '<compiled-menu-rule>', 'eval')

    def __str__(self):
        return ast.dump(self.expression)

    def apply(self, menuentries, run):
        for menuentry in menuentries:
            if run == 2 and (menuentry.MatchedInclude is True or
                             menuentry.Allocated is True):
                continue
            if eval(self.code):
                if self.Type is Rule.TYPE_INCLUDE:
                    menuentry.Add = True
                    menuentry.MatchedInclude = True
                else:
                    menuentry.Add = False
        return menuentries


class MenuEntry:
    "Wrapper for 'Menu Style' Desktop Entries"

    TYPE_USER = "User"
    TYPE_SYSTEM = "System"
    TYPE_BOTH = "Both"

    def __init__(self, filename, dir="", prefix=""):
        # Create entry
        self.DesktopEntry = DesktopEntry(os.path.join(dir, filename))
        self.setAttributes(filename, dir, prefix)

        # Can True, False DELETED, HIDDEN, EMPTY, NOT_SHOW_IN or NO_EXEC
        self.Show = True

        # Semi-Private
        self.Original = None
        self.Parents = []

        # Private Stuff
        self.Allocated = False
        self.Add = False
        self.MatchedInclude = False

        # Caching
        self.Categories = self.DesktopEntry.getCategories()

    def save(self):
        """Save any changes to the desktop entry."""
        if self.DesktopEntry.tainted:
            self.DesktopEntry.write()

    def getDir(self):
        """Return the directory containing the desktop entry file."""
        return self.DesktopEntry.filename.replace(self.Filename, '')

    def getType(self):
        """Return the type of MenuEntry, System/User/Both"""
        if not xdg.Config.root_mode:
            if self.Original:
                return self.TYPE_BOTH
            elif xdg_data_dirs[0] in self.DesktopEntry.filename:
                return self.TYPE_USER
            else:
                return self.TYPE_SYSTEM
        else:
            return self.TYPE_USER

    def setAttributes(self, filename, dir="", prefix=""):
        self.Filename = filename
        self.Prefix = prefix
        self.DesktopFileID = os.path.join(prefix, filename).replace("/", "-")

        if not os.path.isabs(self.DesktopEntry.filename):
            self.__setFilename()

    def updateAttributes(self):
        if self.getType() == self.TYPE_SYSTEM:
            self.Original = MenuEntry(self.Filename, self.getDir(), self.Prefix)
            self.__setFilename()

    def __setFilename(self):
        if not xdg.Config.root_mode:
            path = xdg_data_dirs[0]
        else:
            path = xdg_data_dirs[1]

        if self.DesktopEntry.getType() == "Application":
            dir_ = os.path.join(path, "applications")
        else:
            dir_ = os.path.join(path, "desktop-directories")

        self.DesktopEntry.filename = os.path.join(dir_, self.Filename)

    def __cmp__(self, other):
        return locale.strcoll(self.DesktopEntry.getName(), other.DesktopEntry.getName())

    def _key(self):
        """Key function for locale-aware sorting."""
        return _strxfrm(self.DesktopEntry.getName())

    def __lt__(self, other):
        try:
            other = other._key()
        except AttributeError:
            pass
        return self._key() < other

    def __eq__(self, other):
        if self.DesktopFileID == str(other):
            return True
        else:
            return False

    def __repr__(self):
        return self.DesktopFileID


class Separator:
    "Just a dummy class for Separators"
    def __init__(self, parent):
        self.Parent = parent
        self.Show = True


class Header:
    "Class for Inline Headers"
    def __init__(self, name, generic_name, comment):
        self.Name = name
        self.GenericName = generic_name
        self.Comment = comment

    def __str__(self):
        return self.Name


TYPE_DIR, TYPE_FILE = 0, 1


def _check_file_path(value, filename, type):
    path = os.path.dirname(filename)
    if not os.path.isabs(value):
        value = os.path.join(path, value)
    value = os.path.abspath(value)
    if not os.path.exists(value):
        return False
    if type == TYPE_DIR and os.path.isdir(value):
        return value
    if type == TYPE_FILE and os.path.isfile(value):
        return value
    return False


def _get_menu_file_path(filename):
    dirs = list(xdg_config_dirs)
    if xdg.Config.root_mode is True:
        dirs.pop(0)
    for d in dirs:
        menuname = os.path.join(d, "menus", filename)
        if os.path.isfile(menuname):
            return menuname


def _to_bool(value):
    if isinstance(value, bool):
        return value
    return value.lower() == "true"


# remove duplicate entries from a list
def _dedupe(_list):
    _set = {}
    _list.reverse()
    _list = [_set.setdefault(e, e) for e in _list if e not in _set]
    _list.reverse()
    return _list


class XMLMenuBuilder(object):

    def __init__(self, debug=False):
        self.debug = debug

    def parse(self, filename=None):
        """Load an applications.menu file.

        filename : str, optional
          The default is ``$XDG_CONFIG_DIRS/menus/${XDG_MENU_PREFIX}applications.menu``.
        """
        # convert to absolute path
        if filename and not os.path.isabs(filename):
            filename = _get_menu_file_path(filename)
        # use default if no filename given
        if not filename:
            candidate = os.environ.get('XDG_MENU_PREFIX', '') + "applications.menu"
            filename = _get_menu_file_path(candidate)
        if not filename:
            raise ParsingError('File not found', "/etc/xdg/menus/%s" % candidate)
        # check if it is a .menu file
        if not filename.endswith(".menu"):
            raise ParsingError('Not a .menu file', filename)
        # create xml parser
        try:
            tree = etree.parse(filename)
        except:
            raise ParsingError('Not a valid .menu file', filename)

        # parse menufile
        self._merged_files = set()
        self._directory_dirs = set()
        self.cache = MenuEntryCache()

        menu = self.parse_menu(tree.getroot(), filename)
        menu.tree = tree
        menu.filename = filename

        self.handle_moves(menu)
        self.post_parse(menu)

        # generate the menu
        self.generate_not_only_allocated(menu)
        self.generate_only_allocated(menu)

        # and finally sort
        menu.sort()

        return menu

    def parse_menu(self, node, filename):
        menu = Menu()
        self.parse_node(node, filename, menu)
        return menu

    def parse_node(self, node, filename, parent=None):
        num_children = len(node)
        for child in node:
            tag, text = child.tag, child.text
            text = text.strip() if text else None
            if tag == 'Menu':
                menu = self.parse_menu(child, filename)
                parent.addSubmenu(menu)
            elif tag == 'AppDir' and text:
                self.parse_app_dir(text, filename, parent)
            elif tag == 'DefaultAppDirs':
                self.parse_default_app_dir(filename, parent)
            elif tag == 'DirectoryDir' and text:
                self.parse_directory_dir(text, filename, parent)
            elif tag == 'DefaultDirectoryDirs':
                self.parse_default_directory_dir(filename, parent)
            elif tag == 'Name' and text:
                parent.Name = text
            elif tag == 'Directory' and text:
                parent.Directories.append(text)
            elif tag == 'OnlyUnallocated':
                parent.OnlyUnallocated = True
            elif tag == 'NotOnlyUnallocated':
                parent.OnlyUnallocated = False
            elif tag == 'Deleted':
                parent.Deleted = True
            elif tag == 'NotDeleted':
                parent.Deleted = False
            elif tag == 'Include' or tag == 'Exclude':
                parent.Rules.append(self.parse_rule(child))
            elif tag == 'MergeFile':
                if child.attrib.get("type", None) == "parent":
                    self.parse_merge_file("applications.menu", child, filename, parent)
                elif text:
                    self.parse_merge_file(text, child, filename, parent)
            elif tag == 'MergeDir' and text:
                self.parse_merge_dir(text, child, filename, parent)
            elif tag == 'DefaultMergeDirs':
                self.parse_default_merge_dirs(child, filename, parent)
            elif tag == 'Move':
                parent.Moves.append(self.parse_move(child))
            elif tag == 'Layout':
                if num_children > 1:
                    parent.Layout = self.parse_layout(child)
            elif tag == 'DefaultLayout':
                if num_children > 1:
                    parent.DefaultLayout = self.parse_layout(child)
            elif tag == 'LegacyDir' and text:
                self.parse_legacy_dir(text, child.attrib.get("prefix", ""), filename, parent)
            elif tag == 'KDELegacyDirs':
                self.parse_kde_legacy_dirs(filename, parent)

    def parse_layout(self, node):
        layout = Layout(
            show_empty=_to_bool(node.attrib.get("show_empty", False)),
            inline=_to_bool(node.attrib.get("inline", False)),
            inline_limit=int(node.attrib.get("inline_limit", 4)),
            inline_header=_to_bool(node.attrib.get("inline_header", True)),
            inline_alias=_to_bool(node.attrib.get("inline_alias", False))
        )
        for child in node:
            tag, text = child.tag, child.text
            text = text.strip() if text else None
            if tag == "Menuname" and text:
                layout.order.append([
                    "Menuname",
                    text,
                    _to_bool(child.attrib.get("show_empty", False)),
                    _to_bool(child.attrib.get("inline", False)),
                    int(child.attrib.get("inline_limit", 4)),
                    _to_bool(child.attrib.get("inline_header", True)),
                    _to_bool(child.attrib.get("inline_alias", False))
                ])
            elif tag == "Separator":
                layout.order.append(['Separator'])
            elif tag == "Filename" and text:
                layout.order.append(["Filename", text])
            elif tag == "Merge":
                layout.order.append([
                    "Merge",
                    child.attrib.get("type", "all")
                ])
        return layout

    def parse_move(self, node):
        old, new = "", ""
        for child in node:
            tag, text = child.tag, child.text
            text = text.strip() if text else None
            if tag == "Old" and text:
                old = text
            elif tag == "New" and text:
                new = text
        return Move(old, new)

    # ---------- <Rule> parsing

    def parse_rule(self, node):
        type = Rule.TYPE_INCLUDE if node.tag == 'Include' else Rule.TYPE_EXCLUDE
        tree = ast.Expression(lineno=1, col_offset=0)
        expr = self.parse_bool_op(node, ast.Or())
        if expr:
            tree.body = expr
        else:
            tree.body = ast.Name('False', ast.Load())
        ast.fix_missing_locations(tree)
        return Rule(type, tree)

    def parse_bool_op(self, node, operator):
        values = []
        for child in node:
            rule = self.parse_rule_node(child)
            if rule:
                values.append(rule)
        num_values = len(values)
        if num_values > 1:
            return ast.BoolOp(operator, values)
        elif num_values == 1:
            return values[0]
        return None

    def parse_rule_node(self, node):
        tag = node.tag
        if tag == 'Or':
            return self.parse_bool_op(node, ast.Or())
        elif tag == 'And':
            return self.parse_bool_op(node, ast.And())
        elif tag == 'Not':
            expr = self.parse_bool_op(node, ast.Or())
            return ast.UnaryOp(ast.Not(), expr) if expr else None
        elif tag == 'All':
            return ast.Name('True', ast.Load())
        elif tag == 'Category':
            category = node.text
            return ast.Compare(
                left=ast.Str(category),
                ops=[ast.In()],
                comparators=[ast.Attribute(
                    value=ast.Name(id='menuentry', ctx=ast.Load()),
                    attr='Categories',
                    ctx=ast.Load()
                )]
            )
        elif tag == 'Filename':
            filename = node.text
            return ast.Compare(
                left=ast.Str(filename),
                ops=[ast.Eq()],
                comparators=[ast.Attribute(
                    value=ast.Name(id='menuentry', ctx=ast.Load()),
                    attr='DesktopFileID',
                    ctx=ast.Load()
                )]
            )

    # ---------- App/Directory Dir Stuff

    def parse_app_dir(self, value, filename, parent):
        value = _check_file_path(value, filename, TYPE_DIR)
        if value:
            parent.AppDirs.append(value)

    def parse_default_app_dir(self, filename, parent):
        for d in reversed(xdg_data_dirs):
            self.parse_app_dir(os.path.join(d, "applications"), filename, parent)

    def parse_directory_dir(self, value, filename, parent):
        value = _check_file_path(value, filename, TYPE_DIR)
        if value:
            parent.DirectoryDirs.append(value)

    def parse_default_directory_dir(self, filename, parent):
        for d in reversed(xdg_data_dirs):
            self.parse_directory_dir(os.path.join(d, "desktop-directories"), filename, parent)

    # ---------- Merge Stuff

    def parse_merge_file(self, value, child, filename, parent):
        if child.attrib.get("type", None) == "parent":
            for d in xdg_config_dirs:
                rel_file = filename.replace(d, "").strip("/")
                if rel_file != filename:
                    for p in xdg_config_dirs:
                        if d == p:
                            continue
                        if os.path.isfile(os.path.join(p, rel_file)):
                            self.merge_file(os.path.join(p, rel_file), child, parent)
                            break
        else:
            value = _check_file_path(value, filename, TYPE_FILE)
            if value:
                self.merge_file(value, child, parent)

    def parse_merge_dir(self, value, child, filename, parent):
        value = _check_file_path(value, filename, TYPE_DIR)
        if value:
            for item in os.listdir(value):
                try:
                    if item.endswith(".menu"):
                        self.merge_file(os.path.join(value, item), child, parent)
                except UnicodeDecodeError:
                    continue

    def parse_default_merge_dirs(self, child, filename, parent):
        basename = os.path.splitext(os.path.basename(filename))[0]
        for d in reversed(xdg_config_dirs):
            self.parse_merge_dir(os.path.join(d, "menus", basename + "-merged"), child, filename, parent)

    def merge_file(self, filename, child, parent):
        # check for infinite loops
        if filename in self._merged_files:
            if self.debug:
                raise ParsingError('Infinite MergeFile loop detected', filename)
            else:
                return
        self._merged_files.add(filename)
        # load file
        try:
            tree = etree.parse(filename)
        except IOError:
            if self.debug:
                raise ParsingError('File not found', filename)
            else:
                return
        except:
            if self.debug:
                raise ParsingError('Not a valid .menu file', filename)
            else:
                return
        root = tree.getroot()
        self.parse_node(root, filename, parent)

    # ---------- Legacy Dir Stuff

    def parse_legacy_dir(self, dir_, prefix, filename, parent):
        m = self.merge_legacy_dir(dir_, prefix, filename, parent)
        if m:
            parent += m

    def merge_legacy_dir(self, dir_, prefix, filename, parent):
        dir_ = _check_file_path(dir_, filename, TYPE_DIR)
        if dir_ and dir_ not in self._directory_dirs:
            self._directory_dirs.add(dir_)
            m = Menu()
            m.AppDirs.append(dir_)
            m.DirectoryDirs.append(dir_)
            m.Name = os.path.basename(dir_)
            m.NotInXml = True

            for item in os.listdir(dir_):
                try:
                    if item == ".directory":
                        m.Directories.append(item)
                    elif os.path.isdir(os.path.join(dir_, item)):
                        m.addSubmenu(self.merge_legacy_dir(
                            os.path.join(dir_, item),
                            prefix,
                            filename,
                            parent
                        ))
                except UnicodeDecodeError:
                    continue

            self.cache.add_menu_entries([dir_], prefix, True)
            menuentries = self.cache.get_menu_entries([dir_], False)

            for menuentry in menuentries:
                categories = menuentry.Categories
                if len(categories) == 0:
                    r = Rule.fromFilename(Rule.TYPE_INCLUDE, menuentry.DesktopFileID)
                    m.Rules.append(r)
                if not dir_ in parent.AppDirs:
                    categories.append("Legacy")
                    menuentry.Categories = categories

            return m

    def parse_kde_legacy_dirs(self, filename, parent):
        try:
            proc = subprocess.Popen(
                ['kde-config', '--path', 'apps'],
                stdout=subprocess.PIPE,
                universal_newlines=True
            )
            output = proc.communicate()[0].splitlines()
        except OSError:
            # If kde-config doesn't exist, ignore this.
            return
        try:
            for dir_ in output[0].split(":"):
                self.parse_legacy_dir(dir_, "kde", filename, parent)
        except IndexError:
            pass

    def post_parse(self, menu):
        # unallocated / deleted
        if menu.Deleted is None:
            menu.Deleted = False
        if menu.OnlyUnallocated is None:
            menu.OnlyUnallocated = False

        # Layout Tags
        if not menu.Layout or not menu.DefaultLayout:
            if menu.DefaultLayout:
                menu.Layout = menu.DefaultLayout
            elif menu.Layout:
                if menu.Depth > 0:
                    menu.DefaultLayout = menu.Parent.DefaultLayout
                else:
                    menu.DefaultLayout = Layout()
            else:
                if menu.Depth > 0:
                    menu.Layout = menu.Parent.DefaultLayout
                    menu.DefaultLayout = menu.Parent.DefaultLayout
                else:
                    menu.Layout = Layout()
                    menu.DefaultLayout = Layout()

        # add parent's app/directory dirs
        if menu.Depth > 0:
            menu.AppDirs = menu.Parent.AppDirs + menu.AppDirs
            menu.DirectoryDirs = menu.Parent.DirectoryDirs + menu.DirectoryDirs

        # remove duplicates
        menu.Directories = _dedupe(menu.Directories)
        menu.DirectoryDirs = _dedupe(menu.DirectoryDirs)
        menu.AppDirs = _dedupe(menu.AppDirs)

        # go recursive through all menus
        for submenu in menu.Submenus:
            self.post_parse(submenu)

        # reverse so handling is easier
        menu.Directories.reverse()
        menu.DirectoryDirs.reverse()
        menu.AppDirs.reverse()

        # get the valid .directory file out of the list
        for directory in menu.Directories:
            for dir in menu.DirectoryDirs:
                if os.path.isfile(os.path.join(dir, directory)):
                    menuentry = MenuEntry(directory, dir)
                    if not menu.Directory:
                        menu.Directory = menuentry
                    elif menuentry.Type == MenuEntry.TYPE_SYSTEM:
                        if menu.Directory.Type == MenuEntry.TYPE_USER:
                            menu.Directory.Original = menuentry
            if menu.Directory:
                break

    # Finally generate the menu
    def generate_not_only_allocated(self, menu):
        for submenu in menu.Submenus:
            self.generate_not_only_allocated(submenu)

        if menu.OnlyUnallocated is False:
            self.cache.add_menu_entries(menu.AppDirs)
            menuentries = []
            for rule in menu.Rules:
                menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 1)

            for menuentry in menuentries:
                if menuentry.Add is True:
                    menuentry.Parents.append(menu)
                    menuentry.Add = False
                    menuentry.Allocated = True
                    menu.MenuEntries.append(menuentry)

    def generate_only_allocated(self, menu):
        for submenu in menu.Submenus:
            self.generate_only_allocated(submenu)

        if menu.OnlyUnallocated is True:
            self.cache.add_menu_entries(menu.AppDirs)
            menuentries = []
            for rule in menu.Rules:
                menuentries = rule.apply(self.cache.get_menu_entries(menu.AppDirs), 2)
            for menuentry in menuentries:
                if menuentry.Add is True:
                    menuentry.Parents.append(menu)
                #   menuentry.Add = False
                #   menuentry.Allocated = True
                    menu.MenuEntries.append(menuentry)

    def handle_moves(self, menu):
        for submenu in menu.Submenus:
            self.handle_moves(submenu)
        # parse move operations
        for move in menu.Moves:
            move_from_menu = menu.getMenu(move.Old)
            if move_from_menu:
                # FIXME: this is assigned, but never used...
                move_to_menu = menu.getMenu(move.New)

                menus = move.New.split("/")
                oldparent = None
                while len(menus) > 0:
                    if not oldparent:
                        oldparent = menu
                    newmenu = oldparent.getMenu(menus[0])
                    if not newmenu:
                        newmenu = Menu()
                        newmenu.Name = menus[0]
                        if len(menus) > 1:
                            newmenu.NotInXml = True
                        oldparent.addSubmenu(newmenu)
                    oldparent = newmenu
                    menus.pop(0)

                newmenu += move_from_menu
                move_from_menu.Parent.Submenus.remove(move_from_menu)


class MenuEntryCache:
    "Class to cache Desktop Entries"
    def __init__(self):
        self.cacheEntries = {}
        self.cacheEntries['legacy'] = []
        self.cache = {}

    def add_menu_entries(self, dirs, prefix="", legacy=False):
        for dir_ in dirs:
            if not dir_ in self.cacheEntries:
                self.cacheEntries[dir_] = []
                self.__addFiles(dir_, "", prefix, legacy)

    def __addFiles(self, dir_, subdir, prefix, legacy):
        for item in os.listdir(os.path.join(dir_, subdir)):
            if item.endswith(".desktop"):
                try:
                    menuentry = MenuEntry(os.path.join(subdir, item), dir_, prefix)
                except ParsingError:
                    continue

                self.cacheEntries[dir_].append(menuentry)
                if legacy:
                    self.cacheEntries['legacy'].append(menuentry)
            elif os.path.isdir(os.path.join(dir_, subdir, item)) and not legacy:
                self.__addFiles(dir_, os.path.join(subdir, item), prefix, legacy)

    def get_menu_entries(self, dirs, legacy=True):
        entries = []
        ids = set()
        # handle legacy items
        appdirs = dirs[:]
        if legacy:
            appdirs.append("legacy")
        # cache the results again
        key = "".join(appdirs)
        try:
            return self.cache[key]
        except KeyError:
            pass
        for dir_ in appdirs:
            for menuentry in self.cacheEntries[dir_]:
                try:
                    if menuentry.DesktopFileID not in ids:
                        ids.add(menuentry.DesktopFileID)
                        entries.append(menuentry)
                    elif menuentry.getType() == MenuEntry.TYPE_SYSTEM:
                        # FIXME: This is only 99% correct, but still...
                        idx = entries.index(menuentry)
                        entry = entries[idx]
                        if entry.getType() == MenuEntry.TYPE_USER:
                            entry.Original = menuentry
                except UnicodeDecodeError:
                    continue
        self.cache[key] = entries
        return entries


def parse(filename=None, debug=False):
    """Helper function.
    Equivalent to calling xdg.Menu.XMLMenuBuilder().parse(filename)
    """
    return XMLMenuBuilder(debug).parse(filename)
