# 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


from PyQt4 import QtCore, QtGui

from bzrlib import urlutils, registry
from bzrlib.plugins.explorer.lib import (
    kinds,
    location,
    )
from bzrlib.plugins.explorer.lib.i18n import gettext, N_


class LocationSelector(QtCore.QObject):
    """A collection of widgets used to select between a set of views.

    A selector can provide up to 4 widgets, one for each side.

    Each view has a key and a model. The key uniquely identifies
    a view. The model provides display details such as the name,
    icon and tooltip.

    The currentChanged signal is raised when the selection changes.
    The key and model are passed as parameters.
    """

    def __init__(self, action_map, *args):
        """Create the selector.

        :param action_map: a map of logical names to useful actions.
          At least the following actions are expected:
           * close - close the current page
           * open-local - open a local location
           * open-location - open a location
        """
        QtGui.QWidget.__init__(self, *args)
        self._action_map = action_map
        self._init_data()

    def ui_decorators(self):
        """Returns the widgets for each border (N.E.W.S).

        :return: north_ui, east_ui, west_ui, south_ui where each item is
          a QWidget or None.
        """
        return None, None, None, None
    
    def ui_resizable(self):
        """Returns whether each decorator widget should be resizable.
        
        :return: north_resize, east_resize, west_resize, south_resize where
          each item is a boolean.
        """
        return False, False, False, False

    def _init_data(self):
        """Initialise subclass specific attributes."""

    def add_location(self, key, model):
        """Add an entry for a location."""

    def close_current(self):
        """Close the current location."""

    def update_current_label(self):
        """Update the text/icon/tooltip for the current location."""

    def select_key(self, key):
        """Select the given key. Raises KeyError if not found."""


class TabsLocationSelector(LocationSelector):
    """Select between locations using a set of tabs."""

    def _init_data(self):
        self._model_by_key = {}

    def ui_decorators(self):
        """Returns a tuple of widgets for each border (N.E.W.S)."""
        ui = QtGui.QWidget()
        ui.setLayout(self._layout_ui())
        return None, None, None, ui

    def _layout_ui(self):
        self._tabs = QtGui.QTabBar()
        self._tabs.setShape(QtGui.QTabBar.RoundedSouth)
        # TODO: only enable close icons if Qt 4.5 or later is being used
        #self._tabs.setTabsClosable(True)
        self._tabs.addActions([self._action_map["close"]])
        self._tabs.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
        layout = QtGui.QHBoxLayout()
        layout.addWidget(self._tabs)
        layout.setContentsMargins(0, 0, 0, 0)
        self.connect(self._tabs, QtCore.SIGNAL("currentChanged(int)"),
            self._changed_current)
        return layout

    def _changed_current(self, index):
        key = unicode(self._tabs.tabData(index).toString())
        if key:
            model = self._model_by_key[key]
        else:
            model = None
        self.emit(QtCore.SIGNAL("currentChanged"), key, model)

    def add_location(self, key, model):
        """Add an entry for a location."""
        self._model_by_key[key] = model
        index = self._tabs.addTab(model.display_name())
        self._tabs.setTabIcon(index, model.display_icon())
        self._tabs.setTabToolTip(index, model.display_tooltip())
        self._tabs.setTabData(index, QtCore.QVariant(key))
        self._tabs.setCurrentIndex(index)

    def close_current(self):
        """Close the current location."""
        index = self._tabs.currentIndex()
        key = unicode(self._tabs.tabData(index).toString())
        self._tabs.removeTab(index)
        del self._model_by_key[key]
        self.emit(QtCore.SIGNAL("locationDeleted"), key)

    def update_current_label(self):
        """Update the text/icon/tooltip for the current location."""
        index = self._tabs.currentIndex()
        key = unicode(self._tabs.tabData(index).toString())
        model = self._model_by_key[key]
        self._tabs.setTabText(index, model.display_name())
        self._tabs.setTabIcon(index, model.display_icon())
        self._tabs.setTabToolTip(index, model.display_tooltip())

    def select_key(self, key):
        """Select the given key. Raises KeyError if not found."""
        for i in range(0, self._tabs.count()):
            this_key = unicode(self._tabs.tabData(i).toString())
            if key == this_key:
                self._tabs.setCurrentIndex(i)
                return
        else:
            raise KeyError(key)


class _ComboBoxLocationSelector(LocationSelector):
    """Select between locations using a combo box.
    
    This isn't particularly useful but it validates the API design.
    It may evolve into a list-box selector one day.
    """

    def _init_data(self):
        self._model_by_key = {}

    def ui_decorators(self):
        """Returns a tuple of widgets for each border (N.E.W.S)."""
        ui = QtGui.QWidget()
        ui.setLayout(self._layout_ui())
        return None, None, None, ui

    def _layout_ui(self):
        self._combo = self._build_combo()
        layout = QtGui.QHBoxLayout()
        layout.addWidget(self._combo)
        layout.setContentsMargins(0, 0, 0, 0)
        return layout

    def _build_combo(self):
        combo = QtGui.QComboBox()
        self.connect(combo, QtCore.SIGNAL("currentIndexChanged(int)"),
            self._changed_current)
        return combo

    def _changed_current(self, index):
        key = unicode(self._combo.itemData(index).toString())
        if key:
            model = self._model_by_key[key]
        else:
            model = None
        self.emit(QtCore.SIGNAL("currentChanged"), key, model)

    def add_location(self, key, model):
        """Add an entry for a location."""
        self._model_by_key[key] = model
        self._combo.addItem(model.display_icon(),
            model.display_name(), key)
        index = self._combo.count() - 1
        self._combo.setItemData(index, QtCore.QVariant(model.display_tooltip()),
            QtCore.Qt.ToolTipRole)
        self._combo.setCurrentIndex(index)

    def close_current(self):
        """Close the current location."""
        index = self._combo.currentIndex()
        key = unicode(self._combo.itemData(index).toString())
        self._combo.removeItem(index)
        del self._model_by_key[key]
        self.emit(QtCore.SIGNAL("locationDeleted"), key)

    def update_current_label(self):
        """Update the text/icon/tooltip for the current location."""
        index, model = self._get_current_index_and_model()
        self._combo.setItemText(index, model.display_name())
        self._combo.setItemIcon(index, model.display_icon())
        self._combo.setItemData(index, QtCore.QVariant(model.display_tooltip()),
            QtCore.Qt.ToolTipRole)

    def _get_current_index_and_model(self):
        index = self._combo.currentIndex()
        key = unicode(self._combo.itemData(index).toString())
        model = self._model_by_key[key]
        return index, model

    def select_key(self, key):
        """Select the given key. Raises KeyError if not found."""
        for i in range(0, self._combo.count()):
            this_key = unicode(self._combo.itemData(i).toString())
            if key == this_key:
                self._combo.setCurrentIndex(i)
                return
        else:
            raise KeyError(key)


class TabGroupsLocationSelector(LocationSelector):
    """Select between locations, grouped by repository.
    
    A combo-box provides the primary selection (repository or welcome).
    When an item is selected, tabs are displayed for the
    repository itself, nested branches and associated checkouts
    that are opened.
    """

    def _init_data(self):
        self._model_by_key = {}
        self._tabs_for_group = {}
        self._adding_or_closing = False

    def ui_decorators(self):
        """Returns a tuple of widgets for each border (N.E.W.S)."""
        ui = QtGui.QWidget()
        ui.setLayout(self._layout_ui())
        return None, None, None, ui

    def _layout_ui(self):
        # Internally, we have a combo-box controlling a StackedWidget
        # that hold the tabs for the currently selected "group".
        self._stack = QtGui.QStackedWidget()
        self._combo = self._build_combo()
        layout = QtGui.QHBoxLayout()
        layout.addWidget(self._combo)
        layout.addWidget(self._stack)
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSizeConstraint(QtGui.QLayout.SetMinimumSize)
        return layout

    def _build_combo(self):
        combo = QtGui.QComboBox()
        self.connect(combo, QtCore.SIGNAL("currentIndexChanged(int)"),
            self._changed_combo)
        return combo

    def _changed_combo(self, index):
        # Show the matching tabs, if any
        if index < self._stack.count():
            self._stack.setCurrentIndex(index)
        self._emit_current()

    def _emit_current(self):
        if self._adding_or_closing:
            return
        # Get key for the current child, if any
        combo_index, combo_key, tabs, tab_index, tab_key = self._get_current()
        key = tab_key or combo_key
        #print "emitting key %s" % (key,)
        if key:
            model = self._model_by_key[key]
        else:
            model = None
        self.emit(QtCore.SIGNAL("currentChanged"), key, model)

    def _get_current(self):
        """Return combo_index, combo_key, tab_index, tab_key."""
        combo_index = self._combo.currentIndex()
        combo_key = unicode(self._combo.itemData(combo_index).toString())
        tab_key = None
        try:
            tabs = self._tabs_for_group[combo_key]
        except KeyError:
            tabs = None
            tab_index = -1
        else:
            tab_index = tabs.currentIndex()
            if tab_index >= 0:
                tab_key = unicode(tabs.tabData(tab_index).toString())
        return combo_index, combo_key, tabs, tab_index, tab_key

    def add_location(self, key, model):
        """Add an entry for a location."""
        self._adding_or_closing = True
        try:
            self._add_location(key, model)
        finally:
            self._adding_or_closing = False
            self._emit_current()

    def _add_location(self, key, model):
        self._model_by_key[key] = model
        group_key = self._get_group(key, model)
        #print "group(%s) -> %s" % (key, group_key)
        if group_key is None:
            self._add_group(key, model)
        else:
            try:
                group_model = self._model_by_key[group_key]
            except KeyError:
                # Repository is not yet opened so open it.
                group_model = self._build_group_model(group_key, model)
            tabs = self._add_group(group_key, group_model)
            if model.kind != kinds.REPOSITORY_KIND:
                # Implicitly open a repository tab
                self._add_tab(tabs, group_key, group_model)
            self._add_tab(tabs, key, model)

    def _get_group(self, key, model):
        """Return the group key for an item or None if none."""
        group = model.group()
        if group is not None and group.startswith("file://"):
            group = urlutils.local_path_from_url(group)
        return group

    def _build_group_model(self, group_key, item_model):
        action_callback = item_model.action_callback
        dry_run = item_model.dry_run
        model = location.LocationModel(group_key, action_callback, dry_run)
        self._model_by_key[group_key] = model
        return model

    def _add_group(self, key, model):
        try:
            tabs = self._tabs_for_group[key]
            index = self._find_combo_index(key)
        except KeyError:
            tabs = self._build_tabs()
            self._tabs_for_group[key] = tabs
            self._stack.addWidget(tabs)
            self._combo.addItem(model.display_icon(),
                model.display_name(), QtCore.QVariant(key))
            index = self._combo.count() - 1
            tooltip = QtCore.QVariant(model.display_tooltip())
            self._combo.setItemData(index, tooltip,
                QtCore.Qt.ToolTipRole)
        self._combo.setCurrentIndex(index)
        # Note: tabs shown implicitly by _changed_combo()?
        self._stack.setCurrentIndex(index)
        return tabs

    def _find_combo_index(self, key):
        for i in range(0, self._combo.count()):
            combo_key = unicode(self._combo.itemData(i).toString())
            if key == combo_key:
                return i
        return -1

    def _find_tab_index(self, tabs, key):
        for i in range(0, tabs.count()):
            tab_key = unicode(tabs.tabData(i).toString())
            if key == tab_key:
                return i
        return -1

    def _build_tabs(self):
        tabs = QtGui.QTabBar()
        tabs.setShape(QtGui.QTabBar.RoundedSouth)
        tabs.addActions([self._action_map["close"]])
        tabs.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
        self.connect(tabs, QtCore.SIGNAL("currentChanged(int)"),
            self._changed_tab)
        return tabs

    def _changed_tab(self, index):
        self._emit_current()

    def _add_tab(self, tabs, key, model):
        index = self._find_tab_index(tabs, key)
        #print "found tab %s at index %d" % (key, index)
        if index == -1:
            index = tabs.addTab(model.display_local_name())
            tabs.setTabIcon(index, model.display_icon())
            tabs.setTabToolTip(index, model.display_tooltip())
            tabs.setTabData(index, QtCore.QVariant(key))
        tabs.setCurrentIndex(index)

    def close_current(self):
        """Close the current location."""
        self._adding_or_closing = True
        try:
            self._close_current()
        finally:
            self._adding_or_closing = False
            self._emit_current()

    def _close_current(self):
        combo_index, combo_key, tabs, tab_index, tab_key = self._get_current()
        if tab_index >= 0:
            tabs.removeTab(tab_index)
            if combo_key != tab_key:
                del self._model_by_key[tab_key]
                self.emit(QtCore.SIGNAL("locationDeleted"), tab_key)
        if tabs.count() <= 0:
            self._combo.removeItem(combo_index)
            self._stack.removeWidget(tabs)
            del self._model_by_key[combo_key]
            del self._tabs_for_group[combo_key]
            self.emit(QtCore.SIGNAL("locationDeleted"), combo_key)

    def update_current_label(self):
        """Update the text/icon/tooltip for the current location."""
        combo_index, combo_key, tabs, tab_index, tab_key = self._get_current()
        combo_model = self._model_by_key[combo_key]
        self._combo.setItemText(combo_index, combo_model.display_name())
        self._combo.setItemIcon(combo_index, combo_model.display_icon())
        self._combo.setItemData(combo_index, QtCore.QVariant(combo_model.display_tooltip()),
            QtCore.Qt.ToolTipRole)
        if tab_key is not None:
            tab_model = self._model_by_key[tab_key]
            tabs.setTabText(tab_index, tab_model.display_local_name())
            tabs.setTabIcon(tab_index, tab_model.display_icon())
            tabs.setTabToolTip(tab_index, tab_model.display_tooltip())

    def select_key(self, key):
        """Select the given key. Raises KeyError if not found."""
        #print "known keys are %s" % (self._model_by_key.keys(),)
        model = self._model_by_key[key]
        group_key = self._get_group(key, model)
        if group_key is None:
            self._combo.setCurrentIndex(self._find_combo_index(key))
        else:
            combo_index = self._find_combo_index(group_key)
            if combo_index != self._combo.currentIndex():
                self._combo.setCurrentIndex(combo_index)
            tabs = self._tabs_for_group[group_key]
            tabs.setCurrentIndex(self._find_tab_index(tabs, key))


class TreeLocationSelector(LocationSelector):
    """Select locations arranged in a tree."""

    def _init_data(self):
        self._model_by_key = {}
        self._items_for_group = {}

    def ui_decorators(self):
        """Returns a tuple of widgets for each border (N.E.W.S)."""
        ui = QtGui.QWidget()
        ui.setLayout(self._layout_ui())
        return None, None, ui, None
    
    def ui_resizable(self):
        return False, False, True, False

    def _layout_ui(self):
        label = QtGui.QLabel(gettext("Locations"))
        label.setFrameShape(QtGui.QFrame.Box)
        label.setFrameShadow(QtGui.QFrame.Sunken)
        self._tree = self._build_tree()
        layout = QtGui.QVBoxLayout()
        layout.addWidget(label)
        layout.addWidget(self._tree)
        layout.setContentsMargins(0, 0, 0, 0)
        return layout

    def _build_tree(self):
        tree = QtGui.QTreeWidget()
        tree.setColumnCount(1)
        tree.headerItem().setHidden(True)
        tree.addActions([self._action_map["close"]])
        tree.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
        self.connect(tree, QtCore.SIGNAL(
            "currentItemChanged(QTreeWidgetItem*,QTreeWidgetItem*)"),
            self._changed_current)
        return tree

    def _changed_current(self, current, previous):
        key = None
        model = None
        if current:
            key = unicode(current.data(0, QtCore.Qt.UserRole).toString())
            if key:
                model = self._model_by_key[key]
        self.emit(QtCore.SIGNAL("currentChanged"), key, model)

    def add_location(self, key, model):
        """Add an entry for a location."""
        self._model_by_key[key] = model
        group_key = self._get_group_key(model)
        if group_key is None:
            item = self._add_group(key, model)
        else:
            try:
                group_model = self._model_by_key[group_key]
            except KeyError:
                # group model doesn't exist yet, so create it
                group_model = self._build_group_model(group_key, model)
            item = self._add_group(group_key, group_model)
            if model.kind() != kinds.REPOSITORY_KIND and \
                    group_model.kind() in (kinds.REPOSITORY_KIND,
                                           kinds.VIRTUAL_REPO_KIND):
                item = self._add_group_child(item, key, model)
        self._tree.setCurrentItem(item)

    def _add_group(self, key, model):
        try:
            item = self._items_for_group[key]
        except KeyError:
            item = QtGui.QTreeWidgetItem()
            item.setIcon(0, model.display_icon())
            item.setText(0, model.display_name())
            item.setData(0, QtCore.Qt.UserRole, QtCore.QVariant(key))
            item.setData(0, QtCore.Qt.ToolTipRole,
                         QtCore.QVariant(model.display_tooltip()))
            self._items_for_group[key] = item
            self._tree.addTopLevelItem(item)
        return item
    
    def _add_group_child(self, group_item, key, model):
        item = QtGui.QTreeWidgetItem()
        item.setIcon(0, model.display_icon())
        item.setText(0, model.display_local_name())
        item.setData(0, QtCore.Qt.UserRole, QtCore.QVariant(key))
        item.setData(0, QtCore.Qt.ToolTipRole,
                     QtCore.QVariant(model.display_tooltip()))
        group_item.addChild(item)
        return item
    
    def _get_group_key(self, model):
        """Return the group key for an item or None if none."""
        group = model.group()
        if group is not None and group.startswith("file://"):
            group = urlutils.local_path_from_url(group)
        return group

    def _build_group_model(self, group_key, item_model):
        action_callback = item_model.action_callback
        dry_run = item_model.dry_run
        model = location.LocationModel(group_key, action_callback, dry_run)
        self._model_by_key[group_key] = model
        return model

    def close_current(self):
        """Close the current location."""
        item = self._tree.currentItem()
        key = unicode(item.data(0, QtCore.Qt.UserRole).toString())
        if item.parent():
            item.parent().removeChild(item)
        else:
            for child in item.takeChildren():
                child_key = unicode(child.data(0, QtCore.Qt.UserRole).toString())
                del self._model_by_key[child_key]
                self.emit(QtCore.SIGNAL("locationDeleted"), child_key)
            self._tree.takeTopLevelItem(self._tree.indexOfTopLevelItem(item))
            del self._items_for_group[key]
        del self._model_by_key[key]
        self.emit(QtCore.SIGNAL("locationDeleted"), key)

    def update_current_label(self):
        """Update the text/icon/tooltip for the current location."""
        item, model = self._get_current_item_and_model()
        if item.parent():
            item.setText(0, model.display_local_name())
        else:
            item.setText(0, model.display_name())
        item.setIcon(0, model.display_icon())
        item.setData(0, QtCore.Qt.ToolTipRole,
                     QtCore.QVariant(model.display_tooltip()))

    def _get_current_item_and_model(self):
        item = self._tree.currentItem()
        key = unicode(item.data(0, QtCore.Qt.UserRole).toString())
        model = self._model_by_key[key]
        return item, model

    def select_key(self, key):
        """Select the given key. Raises KeyError if not found."""
        iter = QtGui.QTreeWidgetItemIterator(self._tree)
        while iter.value():
            item = iter.value()
            this_key = unicode(item.data(0, QtCore.Qt.UserRole).toString())
            if key == this_key:
                self._tree.setCurrentItem(item)
                return
            iter += 1
        else:
            raise KeyError(key)

## The location selector registry ##

location_selector_registry = registry.Registry()
location_selector_registry.register("tabs", TabsLocationSelector)
location_selector_registry.register("tab-groups", TabGroupsLocationSelector)
location_selector_registry.register("tree", TreeLocationSelector)
location_selector_registry.default_key = "tabs"
