# 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

"""Dialog for managing accessories."""

import os

from PyQt4 import QtCore, QtGui

from bzrlib import osutils, trace

from bzrlib.plugins.explorer.lib import (
    explorer_preferences,
    helpers,
    kinds,
    )
from bzrlib.plugins.explorer.lib.extensions import accessories
from bzrlib.plugins.explorer.lib.i18n import gettext, N_
from bzrlib.plugins.explorer.widgets import (
    conditional_dataview,
    tab_widget,
    )


_INTRO_TEXT = """<html>
Bazaar Explorer can be extended with accessories.
Several types of accessories are supported:
<ul>
<li><b>Clothes</b> - optional standard extensions</li>
<li><b>Bags</b> - extensions provided by plugins</li>
<li><b>Wallet</b> - your personal extensions</li>
<li><b>Hat</b> - directory-aware extensions, useful for project-specific tools.
</ul>
<p>
The built-in objects are shown below. If you wish to change these,
please submit a patch with your suggestions.
</p><p>
Other tabs lets you browse and manage accessories. You can also
view and edit the objects provided by each. After editing objects,
reload the accessories or restart the application for the changes
to take effect.
<p><p>
Note: Reloading is not required after enabling or disabling accessories.
</html>
"""

class ExplorerAccessoriesDialog(QtGui.QDialog):

    def __init__(self, accessories, editor_callback,
        reload_callback, preferences, parent=None):
        """Create the dialog.

        :param accessories: the collection of accessories
        :param parent: the parent Window. If not None, the dialog will be
          centered over this, otherwise the positioning is undefined.
        """
        self._accessories = accessories
        self._editor_callback = editor_callback
        self._reload_callback = reload_callback
        self._preferences = preferences
        QtGui.QDialog.__init__(self, parent)
        self._build_ui()
        # TODO: save and restore the preferred size
        self.resize(600, 400)

    def _build_ui(self):
        self.setWindowTitle(gettext("Accessories"))
        self._kinds = []
        self._viewers = []
        self._tabs = QtGui.QTabWidget()
        self._add_section(self._tabs, gettext("Introduction"),
            kinds.INFORMATION, self._build_introduction)
        self._add_section(self._tabs, gettext("Clothes"),
            kinds.CLOTHES_ACCESSORY, self._build_clothes)
        self._add_section(self._tabs, gettext("Bags"),
            kinds.BAG_ACCESSORY, self._build_bags)
        self._add_section(self._tabs, gettext("Wallet"),
            kinds.WALLET_ACCESSORY, self._build_wallet)
        self._add_section(self._tabs, gettext("Hat"),
            kinds.HAT_ACCESSORY, self._build_hat)

        # Build the button bar and hook up the visibility control logic.
        # (The button to edit the active set of accessories is hidden
        # by default. Selecting the Clothes or Bags tabs makes it visible.)
        buttons = self._build_button_bar()
        self._enable_button.setVisible(False)
        self._disable_button.setVisible(False)
        def tab_selected(index):
            kind, name, status, index = self._selected_accessory_info()
            active_set_exists = kind in [kinds.CLOTHES_ACCESSORY,
                kinds.BAG_ACCESSORY]
            self._enable_button.setVisible(active_set_exists)
            self._disable_button.setVisible(active_set_exists)
            self._enable_button.setEnabled(status == kinds.DISABLED)
            self._disable_button.setEnabled(status == kinds.ENABLED)
        self.connect(self._tabs, QtCore.SIGNAL("currentChanged(int)"),
            tab_selected)

        # Build the layout
        layout = QtGui.QVBoxLayout()
        layout.addWidget(self._tabs)
        layout.addWidget(buttons)
        self.setLayout(layout)

    def _add_section(self, container, title, icon_kind, layout_callable):
        layout, viewer = layout_callable()
        widget= QtGui.QWidget()
        widget.setLayout(layout)
        self._kinds.append(icon_kind)
        self._viewers.append(viewer)
        icon = kinds.icon_for_kind(icon_kind)
        container.addTab(widget, icon, title)

    def _build_button_bar(self):
        bar = QtGui.QDialogButtonBox()
        # Some accessories may be enabled/disabled
        enable_icon = kinds.icon_for_kind(kinds.ENABLED)
        enable_button = QtGui.QPushButton(enable_icon, gettext("&Enable"))
        bar.addButton(enable_button, QtGui.QDialogButtonBox.ActionRole)
        disable_icon = kinds.icon_for_kind(kinds.DISABLED)
        disable_button = QtGui.QPushButton(disable_icon, gettext("&Disable"))
        bar.addButton(disable_button, QtGui.QDialogButtonBox.ActionRole)

        # The reload button will reload explorer's menus
        reload_icon = kinds.icon_for_kind(kinds.REFRESH_ACTION)
        reload_button = QtGui.QPushButton(reload_icon,
            gettext("&Reload"))
        bar.addButton(reload_button, QtGui.QDialogButtonBox.ActionRole)

        # Hook up the handlers
        bar.connect(enable_button, QtCore.SIGNAL("clicked(bool)"),
            self._toggle_status)
        bar.connect(disable_button, QtCore.SIGNAL("clicked(bool)"),
            self._toggle_status)
        bar.connect(reload_button, QtCore.SIGNAL("clicked(bool)"),
            self._reload_callback)

        # Save away buttons required later and return
        self._enable_button = enable_button
        self._disable_button = disable_button
        return bar

    def _build_introduction(self):
        info = QtGui.QTextBrowser()
        info.setText(gettext(_INTRO_TEXT))
        edit_bar = _AccessoryEditBar(self._accessories.skin,
            self._editor_callback)
        layout = QtGui.QVBoxLayout()
        layout.addWidget(info)
        layout.addWidget(edit_bar)
        return layout, None

    def _build_clothes(self):
        # Build the data model - list of (name,status) tuples - and view
        data, paths = self._build_list_data(
            accessories.clothing_names_and_directories,
            self._accessories.clothes_to_wear, [kinds.ENABLED, kinds.DISABLED])
        footer_text = gettext("Clothes found: %(rows)d")
        edit_bar = _AccessoryEditBar(None, self._editor_callback)
        viewer = self._build_list_viewer(footer_text, data, paths, edit_bar)

        # Build the layout
        layout = QtGui.QVBoxLayout()
        layout.addWidget(viewer)
        layout.addWidget(edit_bar)
        return layout, viewer

    def _build_bags(self):
        # Build the data model - list of (name,status) tuples - and view
        # (In the future, we may want to display the paths in a table)
        data, paths = self._build_list_data(
            accessories.bag_names_and_directories,
            self._accessories.bags_to_blacklist, [kinds.DISABLED, kinds.ENABLED])
        footer_text = gettext("Bags found: %(rows)d")
        edit_bar = _AccessoryEditBar(None, self._editor_callback)
        viewer = self._build_list_viewer(footer_text, data, paths, edit_bar)

        # Build the layout
        layout = QtGui.QVBoxLayout()
        layout.addWidget(viewer)
        layout.addWidget(edit_bar)
        return layout, viewer

    def _build_wallet(self):
        wallet_dir = accessories.wallet_dir()
        wallet_info = QtGui.QLineEdit(wallet_dir)
        wallet_info.setEnabled(False)
        edit_bar = _AccessoryEditBar(self._accessories.wallet,
            self._editor_callback)
        layout = QtGui.QFormLayout()
        layout.addRow(gettext("Directory:"), wallet_info)
        layout.addRow(edit_bar)
        return layout, None

    def _build_hat(self):
        # Build the hats panel
        data, paths = self._build_list_data(
            accessories.hat_names_and_directories, [],
            [None, None])
        footer_text = gettext("Hats found: %(rows)d")
        edit_bar = _AccessoryEditBar(None, self._editor_callback)
        viewer = self._build_list_viewer(footer_text, data, paths, edit_bar,
            use_accessory_icon=True)
        hats_group = helpers.wrap_widget_in_group_box(viewer,
            gettext("Installed hats"))

        # Put the rules panel above the hats panel
        rules_panel = self._build_rules_panel()
        splitter = QtGui.QSplitter()
        splitter.setOrientation(QtCore.Qt.Vertical)
        splitter.addWidget(rules_panel)
        splitter.addWidget(hats_group)

        # Build the layout
        layout = QtGui.QVBoxLayout()
        layout.addWidget(splitter)
        layout.addWidget(edit_bar)
        return layout, viewer

    def _build_rules_panel(self):
        # A viewer with directory and hat columns.
        columns = [gettext("Directory"), gettext("Hat")]
        footer_text = gettext("Selection rules found: %(rows)d")
        data_viewer = conditional_dataview.QBzrConditionalDataView("tree",
            columns, footer_text, None)
        # It would be nice to give the first column maximum space.
        # But I'm not sure how to do that exactly  :-(
        #data_viewer.view().resizeColumnToContents(2)

        # Build the data model
        data = []
        rules = self._accessories.hat_selector.rules()
        for path in sorted(rules):
            data.append((path, rules[path]))
        data_viewer.setData(data)

        # Wrap it in a group box
        group = helpers.wrap_widget_in_group_box(data_viewer,
            gettext("Hat selection rules"))
        return group

    def _build_list_data(self, name_dir_iter, status_set, status_kinds):
        # The data model is a list of (label,status,name,obj) tuples
        data = []
        paths = []
        for name, path in name_dir_iter():
            paths.append(path)
            title = accessories.display_name(name)
            if name in status_set:
                status = status_kinds[0]
            else:
                status = status_kinds[1]
            acc = accessories.Accessory(path, title)
            data.append((title, status, name, acc))
        return data, paths

    def _build_list_viewer(self, footer_text, data, paths, edit_bar,
            use_accessory_icon=False):
        def do_clicked(model_index):
            model = model_index.model()
            row = model_index.row()
            acc = data[row][-1]
            edit_bar.set_accessory(acc)
            status = data[row][1]
            self._enable_button.setEnabled(status == kinds.DISABLED)
            self._disable_button.setEnabled(status == kinds.ENABLED)
        data_viewer = conditional_dataview.QBzrConditionalDataView("list",
            True, footer_text, None)
        # TODO: show the path as a tooltip
        if use_accessory_icon:
            decorator_provider = lambda row, record: \
                kinds.icon_by_resource_path(record[-1].icon_path(),
                "mimetypes/text-x-generic")
        else:
            decorator_provider = \
                lambda row, record: kinds.icon_for_kind(record[1])
        data_viewer.setData(data, decorator_provider)
        # To work for keyboard users, need a different signal?
        data_viewer.connect(data_viewer.view(),
            QtCore.SIGNAL("clicked(QModelIndex)"), do_clicked)
        return data_viewer

    def _toggle_status(self):
        # Build the new data model
        kind, name, status, index = self._selected_accessory_info()
        if status == kinds.ENABLED:
            status = kinds.DISABLED
        else:
            status = kinds.ENABLED
        viewer = self._viewers[self._tabs.currentIndex()]
        data = viewer.data()
        new_row = list(data[index])
        new_row[1] = status
        data[index] = tuple(new_row)

        # Update the preference setting and configuration
        enabled, disabled = self._get_enabled_disabled_set(data)
        if kind == kinds.CLOTHES_ACCESSORY:
            self._update_preference("clothes-to-wear", list(enabled))
            self._accessories.set_clothes_to_wear(enabled)
        else:
            self._update_preference("bags-to-blacklist", list(disabled))
            self._accessories.set_bags_to_blacklist(disabled)

        # Refresh the view and reload the menus
        self._enable_button.setEnabled(status == kinds.DISABLED)
        self._disable_button.setEnabled(status == kinds.ENABLED)
        item_view = viewer.view()
        model = item_view.model()
        model_index =  model.index(index, 0)
        icon = kinds.icon_for_kind(status)
        model.setData(model_index, QtCore.QVariant(icon),
            QtCore.Qt.DecorationRole)
        self._reload_callback()

    def _selected_accessory_info(self):
        # Return kind, name, status, index
        current_tab = self._tabs.currentIndex()
        kind = self._kinds[current_tab]
        viewer = self._viewers[current_tab]
        if viewer is None:
            return kind, None, None, -1
        index, row = viewer.current_row_info()
        if row:
            return kind, row[2], row[1], index
        else:
            return kind, None, None, -1

    def _get_enabled_disabled_set(self, data):
        enabled = set()
        disabled = set()
        for row in data:
            name = row[2]
            if row[1] == kinds.ENABLED:
                enabled.add(name)
            else:
                disabled.add(name)
        return enabled, disabled

    def _update_preference(self, key, value):
        #print "saving preference %s to %s" % (key, value)
        self._preferences[key] = value
        explorer_preferences.save_preferences(self._preferences)


class _AccessoryEditBar(QtGui.QFrame):

    def __init__(self, accessory, editor_callback, parent=None):
        QtGui.QFrame.__init__(self, parent)
        self._accessory = accessory
        self._editor_callback = editor_callback
        toolbar = self._build_toolbar()
        layout = QtGui.QVBoxLayout()
        layout.addWidget(toolbar)
        self.setLayout(layout)
        self.setFrameShape(QtGui.QFrame.Box)
        self.setFrameShadow(QtGui.QFrame.Sunken)
        
    def _build_toolbar(self):
        toolbar = QtGui.QToolBar()
        toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextUnderIcon)
        self._actions_types = []
        for id, ext_type in accessories.extension_type_registry.items():
            def editor(t):
                return lambda: self._edit_file_for_type(t)
            icon = kinds.icon_for_kind(ext_type.icon_kind)
            action = QtGui.QAction(icon, ext_type.label, self)
            path = self._path_for_type(ext_type)
            exists = path is not None and os.path.exists(path)
            action.setEnabled(exists)
            self.connect(action, QtCore.SIGNAL("triggered()"), editor(ext_type))
            toolbar.addAction(action)
            self._actions_types.append((action, ext_type))
        return toolbar

    def set_accessory(self, accessory):
        self._accessory = accessory
        # Enable/disable actions depending on whether extensions found or not
        for action, ext_type in self._actions_types:
            path = self._path_for_type(ext_type)
            exists = path is not None and os.path.exists(path)
            action.setEnabled(exists)

    def _path_for_type(self, ext_type):
        if self._accessory:
            return osutils.pathjoin(self._accessory.directory,
                ext_type.filename)
        else:
            return None

    def _edit_file_for_type(self, ext_type):
        path = self._path_for_type(ext_type)
        self._editor_callback(path)
