# Copyright (C) 2010 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

"""Toolset construction helpers.

These turn toolsets into widgets like toolbars, trees, etc.
"""

from PyQt4 import QtCore, QtGui

from bzrlib.trace import mutter

from bzrlib.plugins.explorer.lib import kinds
from bzrlib.plugins.explorer.lib.extensions import (
    accessories,
    tools,
    )
from bzrlib.plugins.explorer.lib.i18n import gettext, N_


# Map of logical toolbar styles to matching Qt constants
TOOLBAR_STYLES = {
    "text-only":         QtCore.Qt.ToolButtonTextOnly,
    "icon-only":         QtCore.Qt.ToolButtonIconOnly,
    "text-beside-icon":  QtCore.Qt.ToolButtonTextBesideIcon,
    "text-under-icon":   QtCore.Qt.ToolButtonTextUnderIcon,
    }


def _folders_as_lists(stores):
    """Find the top level logical folders in stores.

    :return: a list of (title, items) tuples.
    """
    folders = {}
    result = []
    for store in stores:
        for entry in store.root():
            if isinstance(entry, tools.ToolFolder):
                title = entry.title
                existing = entry.existing
                if title in folders:
                    items = folders[title]
                    if existing == 'replace':
                        del items[:]
                else:
                    items = []
                    folders[title] = items
                    result.append((title, items))
                for child in entry:
                    items.append(child)
    return result


class ToolbarBuilder(object):

    def __init__(self, accessories, tool_runner, action_provider, 
        menu_actions=None, style="text-under-icon"):
        """An object that builds toolbar objects.
        
        :param accesories: the Accessories manager
        :param tool_runner: callback to open a user-defined tool
        :param actions_provider: maps an action name to an action object
        :param menu_actions: list of action names which are menus
        :param style: style for toolbar. See the keys of TOOLBAR_STYLES for
          the list of permitted values.
        """
        self._accessories = accessories
        self._action_provider = action_provider
        self._tool_runner = tool_runner
        self._style = style
        self._menu_actions = menu_actions or []
        self._popup_mode = QtGui.QToolButton.InstantPopup
        self._toolbars = self._load()

    def keys(self):
        """Return the list of available toolbar names."""
        return self._toolbars.keys()

    def get(self, name):
        """Get a toolbar with the given name.

        If the name is unknown, an empty toolbar is returned.
        """
        try:
            return self._toolbars[name]
        except KeyError:
            return self._toolbars["empty:"]
        
    def set_style(self, style):
        actual_tb_style = TOOLBAR_STYLES.get(style)
        for tb in self._toolbars.values():
            if (actual_tb_style is not None and
                actual_tb_style is not tb.toolButtonStyle()):
                tb.setToolButtonStyle(actual_tb_style)

    def _load(self):
        """Load the toolbar objects.
        
        :return: a dictionary of toolbar names to toolbar objects.
          A special entry called "empty:" will hold just the prefix
          and suffix items.
        """
        stores = [acc.toolbars() for acc in self._accessories.items()]
        folders = dict(_folders_as_lists(stores))
        return self._build_toolbars(stores, folders)

    def _build_toolbars(self, stores, folders):
        toolbars = {}
        for store in stores:
            for entry in store.root():
                if isinstance(entry, tools.ToolFolder):
                    title = entry.title
                    if title[0] == '_' and title[-1] == '_':
                        # skip special folders
                        continue
                    toolbar = self._get_toolbar(title, toolbars, folders)
                    for child in entry:
                        self._add_tool_entry(child, toolbar, toolbars, folders)
                else:
                    # Ignore entries outside of folders
                    mutter("skipping entry %s building toolbar" % (entry,))

        # Add the empty toolbar
        self._get_toolbar("empty:", toolbars, folders)

        # Add suffixes
        suffix_items = folders.get("toolset:suffix", [])
        for toolbar in toolbars.values():
            for item in suffix_items:
                self._add_tool_entry(item, toolbar, toolbars, folders)
        return toolbars

    def _add_tool_entry(self, entry, toolbar, toolbars, folders):
        if isinstance(entry, tools.ToolAction):
            name = entry.name
            try:
                action = self._action_provider(name)
            except KeyError:
                # TODO: add a disabled action with a ? on it say?
                mutter("skipping unknown action %s building toolbar" % name)
                return
            toolbar.addAction(action)
            if name in self._menu_actions:
                button = toolbar.widgetForAction(action)
                if button is not None:
                    button.setPopupMode(self._popup_mode)
                else:
                    # This shouldn't happen ...
                    mutter("no toolbar button found for %s" % (name,))
        elif isinstance(entry, tools.ToolSeparator):
            toolbar.addSeparator()
        elif isinstance(entry, tools.Tool):
            def open_callback():
                self._tool_runner(entry)
            label = entry.title
            action = toolbar.addAction(entry.title, open_callback)
            tip = "(%s) %s" % (entry.type, entry.action)
            action.setStatusTip(tip)
            if entry.icon:
                icon = kinds.icon_by_resource_path(entry.icon)
            else:
                icon = kinds.icon_for_kind(entry.type)
            action.setIcon(icon)
        elif isinstance(entry, tools.ToolFolder):
            # create menu
            item = QtGui.QMenu(entry.title,toolbar)
            menu_action = QtGui.QAction(entry.title, toolbar)
            menu_action.setMenu(item)
            if entry.icon:
                icon = kinds.icon_by_resource_path(entry.icon)
                menu_action.setIcon(icon)
            toolbar.addAction(menu_action)
            # change to button behaviour if top of menu
            if isinstance(toolbar, QtGui.QToolBar):
                button = toolbar.widgetForAction(menu_action)
                button.setPopupMode(QtGui.QToolButton.InstantPopup)
            # add menu options
            for child in entry:
                self._add_tool_entry(child, item, toolbars, folders)
        elif isinstance(entry, tools.ToolSet):
            # Lookup the folder named "toolset:...".
            # Note: project substitution is not supported here
            name = entry.name
            folder = folders.get("toolset:%s" % name, [])
            for child in folder:
                self._add_tool_entry(child, toolbar, toolbars, folders)

    def _get_toolbar(self, title, toolbars, folders):
        """Return the existing toolbar for a title or create a new one."""
        if title in toolbars:
            return toolbars[title]
        tb = QtGui.QToolBar()
        tb.setToolButtonStyle(TOOLBAR_STYLES[self._style])
        tb.setContextMenuPolicy(QtCore.Qt.PreventContextMenu)
        toolbars[title] = tb
        # Start with the prefix items
        prefix_items = folders.get("toolset:prefix", [])
        for item in prefix_items:
            self._add_tool_entry(item, tb, toolbars, folders)
        return tb


class ToolTreeBuilder(object):

    def __init__(self, accessories, tool_runner, action_provider, 
        menu_actions=None):
        """An object that builds toolbar objects.
        
        :param accesories: the Accessories manager
        :param tool_runner: callback to open a user-defined tool
        :param actions_provider: maps an action name to an action object
        :param menu_actions: list of action names which are menus
        """
        self._accessories = accessories
        self._action_provider = action_provider
        self._tool_runner = tool_runner
        self._menu_actions = menu_actions or []
        # Use build_ui() to construct this
        self._tree = None

    def build_ui(self):
        """Return a Tree containing the tools.
        
        :return: a TreeView object.
        """
        self._tree = QtGui.QTreeView()
        # Hide the header, make the tree read-only and hook up callbacks
        self._tree.setHeaderHidden(True)
        self._tree.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
        self._tree.connect(self._tree,
            QtCore.SIGNAL("clicked(QModelIndex)"), self._do_selected)
        # Give it some data ...
        self.refresh()
        return self._tree

    def hat_starting_row(self):
        """Return the row number the hat starts on or -1 if no hat."""
        if self._accessories.hat:
            return self._base_entry_count
        else:
            return -1

    def refresh(self):
        """Refesh the view to use the latest tool definitions."""
        # Build the data model and update the view to use it.
        self.model = QtGui.QStandardItemModel()
        groups = {}
        stores = [acc.tools() for acc in self._accessories.base_items()]
        for store in stores:
            for entry in store.root():
                self._add_tool_entry(entry, self.model, groups)
        # Remember how many base entries were created
        self._base_entry_count = self.model.rowCount()
        if self._accessories.hat:
            store = self._accessories.hat.tools()
            for entry in store.root():
                self._add_tool_entry(entry, self.model, groups)
        self._tree.setModel(self.model)

    def _add_tool_entry(self, entry, group, groups):
        if isinstance(entry, tools.Tool):
            if entry.icon:
                icon = kinds.icon_by_resource_path(entry.icon)
            else:
                icon = kinds.icon_for_kind(entry.type)
            item = QtGui.QStandardItem(icon, entry.title)
            item.setData(QtCore.QVariant(entry))
            group.appendRow(item)
        elif isinstance(entry, tools.ToolFolder):
            clear = entry.existing == "replace"
            item, created = self._get_group(entry.title, groups, clear)
            if entry.icon:
                icon = kinds.icon_by_resource_path(entry.icon)
                if icon is not None:
                    item.setIcon(icon)
            if created:
                group.appendRow(item)
            for child in entry:
                self._add_tool_entry(child, item, groups)
        elif isinstance(entry, tools.ToolSeparator):
            # Ignore separators
            pass
        elif isinstance(entry, tools.ToolAction):
            name = entry.name
            try:
                action = self._action_provider(name)
            except KeyError:
                mutter("skipping unknown action %s building tool-tree" % name)
                return
            if name in self._menu_actions:
                # TODO: handle menu actions. Maybe make them folders?
                mutter("skipping menu action %s building tool-tree" % name)
            else:
                # Sanitize the action names as they may contain unwanted text
                label = _action_text_as_label(unicode(action.text()))
                item = QtGui.QStandardItem(action.icon(), label)
                item.setData(QtCore.QVariant(action))
                group.appendRow(item)
        elif isinstance(entry, tools.ToolSet):
            name = entry.name
            project = entry.project
            toolset = accessories.find_toolset(name)
            folder = toolset.as_tool_folder({'project': project})
            for child in folder:
                self._add_tool_entry(child, group, groups)

    def _get_group(self, title, groups, clear=False):
        """Return the existing group for a title or create a new one."""
        if title in groups:
            group = groups[title]
            if clear:
                group.removeRows(0, group.rowCount())
            return group, False
        group = QtGui.QStandardItem(title)
        groups[title] = group
        return group, True

    def _do_selected(self, index):
        # The user selected an item
        model = index.model()
        item = model.itemFromIndex(index)
        base_object = item.data()
        if base_object is None:
            return
        base_object = base_object.toPyObject()
        if isinstance(base_object, tools.Tool):
            self._tool_runner(base_object)
        elif isinstance(base_object, QtGui.QAction):
            base_object.trigger()
        else:
            # Probably a folder - expand or collapse it
            expanded = self._tree.isExpanded(index)
            self._tree.setExpanded(index, not expanded)


def _action_text_as_label(s):
    """Strip out accelerator markers and ellipsis."""
    if s.endswith("..."):
        s = s[:-3]
    return s.replace('&', '', 1)


class ToolMenuBuilder(object):

    def __init__(self, accessories, tool_runner, action_provider, 
        menu_actions=None):
        """An object that builds a menu of tools.
        
        :param accesories: the Accessories manager
        :param tool_runner: callback to open a user-defined tool
        :param actions_provider: maps an action name to an action object
        :param menu_actions: list of action names which are menus
        """
        self._accessories = accessories
        self._action_provider = action_provider
        self._tool_runner = tool_runner
        self._menu_actions = menu_actions or []
        # Use build_ui() to construct this
        self._ui = None

    def build_ui(self):
        """Return a Menu containing the tools.
        
        :return: a QMenu object.
        """
        self._ui = QtGui.QMenu()
        self.refresh()
        return self._ui

    def hat_starting_row(self):
        """Return the row number the hat starts on or -1 if no hat."""
        return -1

    def refresh(self):
        """Refesh the view to use the latest tool definitions."""
        menu = self._ui
        menu.clear()
        groups = {}
        stores = [acc.tools() for acc in self._accessories.base_items()]
        for store in stores:
            for entry in store.root():
                self._add_tool_entry(entry, menu, groups)
        if self._accessories.hat:
            store = self._accessories.hat.tools()
            for entry in store.root():
                self._add_tool_entry(entry, menu, groups)

    def _add_tool_entry(self, entry, group, groups):
        if isinstance(entry, tools.Tool):
            def open_callback():
                self._tool_runner(entry)
            label = entry.title
            action = group.addAction(entry.title, open_callback)
            tip = "(%s) %s" % (entry.type, entry.action)
            action.setStatusTip(tip)
            if entry.icon:
                icon = kinds.icon_by_resource_path(entry.icon)
            else:
                icon = kinds.icon_for_kind(entry.type)
            action.setIcon(icon)
        elif isinstance(entry, tools.ToolFolder):
            clear = entry.existing == "replace"
            item, created = self._get_group(entry.title, groups, clear, group)
            if entry.icon:
                icon = kinds.icon_by_resource_path(entry.icon)
                if icon is not None:
                    item.setIcon(icon)
            if created:
                group.addMenu(item)
            for child in entry:
                self._add_tool_entry(child, item, groups)
        elif isinstance(entry, tools.ToolSeparator):
            group.addSeparator()
        elif isinstance(entry, tools.ToolAction):
            name = entry.name
            try:
                action = self._action_provider(name)
            except KeyError:
                mutter("skipping unknown action %s building tool-menu" % name)
                return
            if name in self._menu_actions:
                # TODO: handle menu actions. Maybe make them folders?
                mutter("skipping menu action %s building tool-menu" % name)
            else:
                group.addAction(action)
        elif isinstance(entry, tools.ToolSet):
            name = entry.name
            project = entry.project
            toolset = accessories.find_toolset(name)
            folder = toolset.as_tool_folder({'project': project})
            for child in folder:
                self._add_tool_entry(child, group, groups)

    def _get_group(self, title, groups, clear=False, parent=None):
        """Return the existing group for a title or create a new one."""
        if title in groups:
            group = groups[title]
            if clear:
                group.clear()
            return group, False
        group = QtGui.QMenu(title, parent)
        groups[title] = group
        return group, True


class ToolBoxBuilder(object):

    def __init__(self, accessories, tool_runner, action_provider, 
        menu_actions=None):
        """An object that builds a toolbox of tools.
        
        :param accesories: the Accessories manager
        :param tool_runner: callback to open a user-defined tool
        :param actions_provider: maps an action name to an action object
        :param menu_actions: list of action names which are menus
        """
        self._accessories = accessories
        self._action_provider = action_provider
        self._tool_runner = tool_runner
        self._menu_actions = menu_actions or []
        # Use build_ui() to construct this
        self._ui = None

    def build_ui(self):
        """Return a toolbox containing the tools.
        
        :return: a QToolBox object.
        """
        self._ui = QtGui.QToolBox()
        self._top_level_items = {}
        self._top_level_count = 0
        self.refresh()
        return self._ui

    def hat_starting_row(self):
        """Return the row number the hat starts on or -1 if no hat."""
        return -1

    def refresh(self):
        """Refesh the view to use the latest tool definitions."""
        toolbox = self._ui
        _empty_toolbox(toolbox)
        if self._top_level_items:
            self._top_level_items.clear()
        default_group_icon = kinds.icon_for_kind(kinds.TOOLS_EXT)
        groups = {}
        stores = [acc.tools() for acc in self._accessories.base_items()]
        for store in stores:
            for entry in store.root():
                self._add_tool_entry(entry, toolbox, groups, default_group_icon)
        if self._top_level_count > 0:
            index = toolbox.addItem(self._top_level_items, gettext("My Tools"))
            toolbox.setItemIcon(index, default_group_icon)
        if self._accessories.hat:
            store = self._accessories.hat.tools()
            hat_icon = kinds.icon_by_resource_path(
                self._accessories.hat.icon_path(),
                kinds.path_for_icon(kinds.HAT_ACCESSORY))
            for entry in store.root():
                self._add_tool_entry(entry, toolbox, groups, hat_icon)

    def _add_tool_entry(self, entry, group, groups, default_group_icon):
        if isinstance(entry, tools.Tool):
            if isinstance(group, QtGui.QToolBox):
                if not self._top_level_items:
                    self._top_level_items = self._create_group("",self._ui)
                group = self._top_level_items
                self._top_level_count += 1
            def open_callback():
                self._tool_runner(entry)
            label = entry.title
            action = group.addAction(entry.title, open_callback)
            tip = "(%s) %s" % (entry.type, entry.action)
            action.setStatusTip(tip)
            if entry.icon:
                icon = kinds.icon_by_resource_path(entry.icon)
            else:
                icon = kinds.icon_for_kind(entry.type)
            action.setIcon(icon)
        elif isinstance(entry, tools.ToolFolder):
            clear = entry.existing == "replace"
            item, created = self._get_group(entry.title, groups, clear, group)
            if entry.icon:
                icon = kinds.icon_by_resource_path(entry.icon)
            else:
                icon = default_group_icon
            if created:
                if isinstance(group, QtGui.QToolBox):
                    index = group.addItem(item, entry.title)
                    if icon is not None:
                        group.setItemIcon(index, icon)
                else:
                    menu_action = QtGui.QAction(entry.title, group)
                    menu_action.setMenu(item)
                    if icon is not None:
                        menu_action.setIcon(icon)
                    group.addAction(menu_action)
                    if isinstance(group, QtGui.QToolBar):
                        button = group.widgetForAction(menu_action)
                        button.setPopupMode(QtGui.QToolButton.InstantPopup)
            for child in entry:
                self._add_tool_entry(child, item, groups, default_group_icon)
        elif isinstance(entry, tools.ToolSeparator):
            # We explicitly ignore separators at the top level
            if not isinstance(group, QtGui.QToolBox):
                group.addSeparator()
        elif isinstance(entry, tools.ToolAction):
            name = entry.name
            try:
                action = self._action_provider(name)
            except KeyError:
                mutter("skipping unknown action %s building tool-menu" % name)
                return
            if isinstance(group, QtGui.QToolBox):
                group = self._top_level_items
                self._top_level_count += 1
            if name in self._menu_actions:
                group.addAction(action)
                button = group.widgetForAction(action)
                button.setPopupMode(QtGui.QToolButton.InstantPopup)
            else:
                group.addAction(action)
        elif isinstance(entry, tools.ToolSet):
            name = entry.name
            project = entry.project
            toolset = accessories.find_toolset(name)
            folder = toolset.as_tool_folder({'project': project})
            for child in folder:
                self._add_tool_entry(child, group, groups, default_group_icon)

    def _get_group(self, title, groups, clear=False, parent=None):
        """Return the existing group for a title or create a new one."""
        if title in groups:
            group = groups[title]
            if clear:
                group.clear()
            return group, False
        group = self._create_group(title, parent)
        groups[title] = group
        return group, True

    def _create_group(self, title, parent):
        if isinstance(parent, QtGui.QToolBox):
            tb = QtGui.QToolBar(parent)
            self.set_toolbar_style(tb)
            return tb
        else:
            return QtGui.QMenu(title, parent)

    def set_toolbar_style(self, tb):
        tb.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)


class VerboseToolBoxBuilder(ToolBoxBuilder):

    def set_toolbar_style(self, tb):
        tb.setOrientation(QtCore.Qt.Vertical)
        tb.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
        tb.setSizePolicy(
            QtGui.QSizePolicy.Ignored,
            QtGui.QSizePolicy.Ignored)


class IconsOnlyToolBoxBuilder(ToolBoxBuilder):

    def set_toolbar_style(self, tb):
        tb.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)


def _empty_toolbox(toolbox):
    """Helper method for clearing a toolbox."""
    for i in range(toolbox.count()):
        # Note the documentation says this does *not* delete the
        # widgets but it doesn't provide a method for doing that.
        # Hopefully Python garbage-collects them.
        toolbox.removeItem(0)
