# Copyright (C) 2009 Canonical Ltd
#
# 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 St, Fifth Floor, Boston, MA 02110-1301  USA

"""Tool management."""

import os, sys

from bzrlib import osutils, trace, debug
try:
    from xml.etree.ElementTree import ElementTree, Element
except ImportError:
    # We use bzrlib's version: xml.etree only arrived in Python 2.5
    from bzrlib.util.elementtree.ElementTree import ElementTree, Element

from bzrlib.plugins.explorer.lib import kinds


# Root folder name
TOOLS_ROOT_TITLE = "Tools"


class ToolEntry(object):
    """Any item that can appear in a tool collection."""


class Tool(ToolEntry):
    """A tool.

    Tools can be web sites, bzr commands or (local) applications.
    Alternatively, a predefined action can be specified by name.
    """

    def __init__(self, title, type, action, conditions=None, icon=None):
        """Create a tool.

        :param title: text to display
        :param type: either link, bzr, application, or shell
        :param action: url or command template
        :param conditions: list of bzr conditions that the tool is enabled for.
        :param icon: icon to use instead of the default one
        """
        self.title = title
        self.type = type
        self.action = action
        self.conditions = conditions
        self.icon = icon
        self.uses_selected = (action.find("%(wt_selected)s") != -1) 

    def __repr__(self):
        return "Tool(%s, %s, %s)" % (self.title, self.type, self.action)


class ToolAction(ToolEntry):
    """A predefined action."""

    def __init__(self, name):
        """Create an action.

        :param name: name of the action
        """
        self.name = name

    def __repr__(self):
        return "ToolAction(%s)" % (self.name,)


class ToolFolder(ToolEntry):
    """A directory of tool entries."""

    def __init__(self, title, icon=None, existing=None):
        self.title = title
        self.icon = icon
        self.existing = existing
        self._children = []

    def __repr__(self):
        return "ToolFolder(%s)" % (self.title,)

    def append(self, entry):
        self._children.append(entry)

    def __len__(self):
        return len(self._children)

    def __iter__(self):
        return iter(self._children)


class ToolSeparator(ToolEntry):
    """A separator between tools."""

    def __repr__(self):
        return "ToolSeparator()"


class ToolSet(ToolEntry):
    """An entry that gets expanded into other tool entries."""

    def __init__(self, name, project):
        """Create a toolset.

        :param name: name of the toolset to expand.
        :Param project: value for the project parameter
        """
        self.name = name
        self.project = project

    def __repr__(self):
        return "ToolSet(%s)" % (self.name,)


class ToolStore(object):
    """A persistent store of tools."""

    def __init__(self, path=None):
        self._path = path
        self.mapper = _ETreeToolMapper()
        self.load()

    def load(self):
        """Load the collection."""
        if self._path is None or not os.path.exists(self._path):
            self._root = ToolFolder(TOOLS_ROOT_TITLE)
        else:
            try:
                etree = ElementTree(file=self._path)
            except Exception:
                trace.mutter("failed to parse tools file %s" % (self._path,))
                if 'error' in debug.debug_flags:
                    trace.report_exception(sys.exc_info(), sys.stderr)
                self._root = ToolFolder(TOOLS_ROOT_TITLE)
            else:
                self._root = self.mapper.etree_to_folder(etree)
                if self._root is None:
                    self._root = ToolFolder(TOOLS_ROOT_TITLE)

    def save(self, path=None):
        """Save the collection.

        :param path: if non-None, the path to save to
          (instead of the default path set in the constructor)
        """
        if path is None:
            path = self._path

        # Backup old file if it exists
        if os.path.exists(path):
            backup_path = "%s.bak" % (path,)
            if os.path.exists(backup_path):
                os.unlink(backup_path)
            osutils.rename(path, backup_path)

        # Convert to xml and dump to a file
        etree = self.mapper.folder_to_etree(self._root)
        etree.write(path)

    def root(self):
        """Return the root folder of this collection."""
        return self._root


def _etree_indent(elem, level=0):
    # From http://effbot.org/zone/element-lib.htm
    i = "\n" + level*"  "
    if len(elem):
        if not elem.text or not elem.text.strip():
            elem.text = i + "  "
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
        for elem in elem:
            _etree_indent(elem, level+1)
        if not elem.tail or not elem.tail.strip():
            elem.tail = i
    else:
        if level and (not elem.tail or not elem.tail.strip()):
            elem.tail = i


class _ETreeToolMapper(object):

    def folder_to_etree(self, bm_folder):
        """Convert a ToolFolder to an ElementTree.

        :param bm_folder: the ToolFolder to convert
        :return: the ElementTree generated
        """
        root = self._entry_to_element(bm_folder)
        # Make the output editable (by xml standards at least)
        _etree_indent(root)
        return ElementTree(root)

    def _entry_to_element(self, entry):
        if isinstance(entry, Tool):
            element = Element('tool',
                title=entry.title,
                type=entry.type,
                action=entry.action)
            if entry.conditions:
                element.attrib['conditions'] = " ".join(entry.conditions)
            if entry.icon:
                element.attrib['icon'] = entry.icon
            return element
        elif isinstance(entry, ToolAction):
            return Element('action', name=entry.name)
        elif isinstance(entry, ToolFolder):
            folder_element = Element('folder', title=entry.title)
            if entry.icon:
                folder_element.attrib['icon'] = entry.icon
            if entry.existing:
                folder_element.attrib['existing'] = entry.existing
            for child in entry:
                child_element = self._entry_to_element(child)
                folder_element.append(child_element)
            return folder_element
        elif isinstance(entry, ToolSeparator):
            return Element('separator')
        elif isinstance(entry, ToolSet):
            return Element('toolset', name=entry.name, project=entry.project)
        else:
            raise AssertionError("unexpected entry %r" % (entry,))

    def etree_to_folder(self, etree):
        """Convert an ElementTree to a ToolFolder.

        :param etree: the ElementTree to convert
        :return: the ToolFolder generated
        """
        root = etree.getroot()
        base = root
        if root.tag != 'folder':
            base = Element('folder', title=TOOLS_ROOT_TITLE)
            base.append(root)
        return self._element_to_entry(base)

    def _element_to_entry(self, element):
        tag = element.tag
        if tag == 'tool':
            title = element.get('title')
            type = element.get('type')
            action = element.get('action')

            # Validate the attributes
            invalid_tool = False
            if (title == None or len(title) == 0):
                trace.mutter(
                    "invalid tool definition: <tool> '%s' has no title", title)
                invalid_tool = True
            if (type == None or len(type) == 0):
                trace.mutter(
                    "invalid tool definition: <tool> '%s' has no type", title)
                invalid_tool = True
            if (action == None or len(action) == 0):
                trace.mutter(
                    "invalid tool definition: <tool> '%s' has no action", title)
                invalid_tool = True
            if invalid_tool:
                return None
            valid_type = type in  [kinds.LINK_TOOL,
                kinds.BZR_TOOL,
                kinds.BZREXEC_TOOL,
                kinds.APPLICATION_TOOL,
                kinds.SHELL_TOOL,
                ]
            if not valid_type:
                trace.mutter(
                    "invalid tool definition: <tool> '%s' has unknown type %s",
                    title, type)
                return None

            conditions = element.get('conditions')
            if conditions:
                conditions = conditions.split()
            icon = element.get('icon')
            return Tool(title, type, action, conditions, icon)
        elif tag == 'action':
            name = element.get('name')
            return ToolAction(name)
        elif tag == 'folder':
            title = element.get('title')
            icon = element.get('icon')
            existing = element.get('existing')
            folder = ToolFolder(title, icon, existing)
            for child in element:
                child_entry = self._element_to_entry(child)
                folder.append(child_entry)
            return folder
        elif tag == 'separator':
            return ToolSeparator()
        elif tag == 'toolset':
            name = element.get('name')
            project = element.get('project')
            return ToolSet(name, project)
        else:
            trace.mutter("invalid tool definition: unexpected element tag %s",
                tag)


# testing
if __name__ == '__main__':
    store = ToolStore("tools-in.xml")
    store.save("tools-out.xml")
