# 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

import sys

from PyQt4 import QtCore, QtGui

from bzrlib import osutils, trace

from bzrlib.plugins.explorer.lib import (
    app_suite,
    i18n,
    kinds,
    location_selector,
    workspace_models,
    )
from bzrlib.plugins.explorer.lib.extensions import accessories
from bzrlib.plugins.explorer.lib.i18n import gettext, N_
from bzrlib.plugins.explorer.widgets import tab_widget

# Preferences types supported are string, boolean and list.
# String preferences may be linked to a registry to limit the permitted values.

DEFAULT_PREFERENCES = {
    "app-suite": app_suite.app_suite_registry.default_key,
    "toolbar-contents": "kind-specific",
    "toolbar-style": "text-under-icon",
    "status-auto-refresh": True,
    "custom-dialog-init": True,
    "custom-dialog-branch": True,
    "custom-dialog-checkout": True,
    "custom-dialog-switch": True,
    "advanced-commands-terminal": False,
    "workingtree-style": "qbrowse",
    "workspace-model": "feature-branches",
    "bind-branches-by-default": False,
    "lightweight-checkout-by-default": False,
    "location-selector-style": "tabs",
    "clothes-to-wear": [
        "Bazaar support",
        "Register on Launchpad",
        ],
    "bags-to-blacklist": [],
    "toolbox-on-status-view": True,
    "toolbox-on-repository-view": False,
    "toolbox-on-welcome-view": False,
    "workingtree-default-to-edit": False,
    "toolbox-style": "tabs",
    "delete-to-trash": True,
    }
_BOOLEAN_PREFERENCES = [
    "status-auto-refresh",
    "custom-dialog-init",
    "custom-dialog-branch",
    "custom-dialog-checkout",
    "custom-dialog-switch",
    "advanced-commands-terminal",
    "bind-branches-by-default",
    "lightweight-checkout-by-default",
    "toolbox-on-status-view",
    "toolbox-on-repository-view",
    "toolbox-on-welcome-view",
    "workingtree-default-to-edit",
    "delete-to-trash",
    ]
_LIST_PREFERENCES = [
    "clothes-to-wear",
    "bags-to-blacklist",
    ]

# key,value pairs to upgrade to new values.
_UPGRADES = {
    "toolbar-contents\0default": "kind-specific",
    }

def load_preferences():
    """Load and return the user preferences.

    :return: a dictionary of preferences
    """
    result = DEFAULT_PREFERENCES.copy()
    upgrade_detected = False
    settings = _get_settings()
    settings.beginGroup("Preferences")
    for key in settings.allKeys():
        name = str(key)
        if name in _BOOLEAN_PREFERENCES:
            value = settings.value(name).toBool()
        elif name in _LIST_PREFERENCES:
            value = unicode(settings.value(name).toString())
            value = _parse_list_preference(value, name)
        else:
            value = str(settings.value(name).toString())
        # Check for values that need to be upgraded
        upgrade_nv = "%s\0%s" % (name, value)
        if upgrade_nv in _UPGRADES:
            value = _UPGRADES[upgrade_nv]
            upgrade_detected = True
        result[name] = value
    settings.endGroup()
    if upgrade_detected:
        save_preferences(result)
    return result


def save_preferences(preferences):
    """Save preferences.

    :param preferences: a dictionary of preferences to save
    """
    settings = _get_settings()
    settings.beginGroup("Preferences")
    for name in sorted(preferences):
        value = preferences[name]
        if name in _LIST_PREFERENCES:
            value = _format_list_preference(value, name)
        value = QtCore.QVariant(value)
        settings.setValue(name, value)
    settings.endGroup()


_LIST_SEPARATOR = "|"

def _format_list_preference(items, name):
    """Convert a list into a string to be saved."""
    if not items:
        return ''
    # We join using a magic separator and encode as utf8.
    raw = _LIST_SEPARATOR.join(items)
    try:
        return raw.encode('utf_8')
    except UnicodeDecodeError, ex:
        trace.mutter("storing default for list preference %s: %s", name, ex)
        return _LIST_SEPARATOR.join(DEFAULT_PREFERENCES.get(name, []))

    
def _parse_list_preference(s, name):
    """Convert a saved string into a list."""
    if not s:
        return []
    # Decode from utf-8
    try:
        raw = s.decode('utf_8')
    except UnicodeDecodeError:
        # Log the problem and return the default value:
        # that's more robust that failing to start the
        # application (and these are only preferences after all).
        trace.mutter("loading default for list preference %s: %s", name, ex)
        return DEFAULT_PREFERENCES.get(name, [])
    return raw.split(_LIST_SEPARATOR)


_settings = None


def _get_settings():
    """Return the QSettings object to use for preference loading/saving."""
    global _settings
    if _settings is None:
        config_dir = accessories.wallet_dir()
        config_file = osutils.pathjoin(config_dir, "explorer.conf")
        _settings = QtCore.QSettings(config_file, QtCore.QSettings.IniFormat)
    return _settings


class ExplorerPreferencesDialog(QtGui.QDialog):

    def __init__(self, preferences, callback, toolbar_builder, title=None,
        parent=None):
        """Create the dialog.

        :param preferences: a dictionary of preference names and values
        :param callback: a callable to call when a preference is changed.
          The required signature is callback(name, new_value, old_value).
        :param toolbar_builder: the ToolbarBuilder to lookup names from
        :param title: the dialog title. None for "Preferences" (localised).
        :param parent: the parent Window. If not None, the dialog will be
          centered over this, otherwise the positioning is undefined.
        """
        QtGui.QDialog.__init__(self, parent)
        self._preferences = preferences
        self._callback = callback
        self._toolbar_builder = toolbar_builder
        self._build_ui(title)
        # NOTE: It would be nice to restore the previous position and size.
        # QBzrDialog/QBzrWindow has support for the latter but doesn't
        # seem to restore the position (which is the interesting bit).
        # It's probably more appropriate to store them in our own settings
        # file anyhow, now that it exists. The default sizing is OK though
        # except on Windows where it needs a tweak. See
        # https://bugs.launchpad.net/bzr-explorer/+bug/513108.
        if sys.platform == 'win32':
            self.adjustSize()
            self.resize(self.size().width() + 50, self.size().height())

    def _build_ui(self, title=None):
        if title is None:
            title = gettext("Preferences")
        self.setWindowTitle(title)
        container = tab_widget.QBzrPreferencesTabWidget()
        self._add_section(container, gettext("Appearance"),
            kinds.APPEARANCE_PREFS, self._build_appearance)
        self._add_section(container, gettext("Behavior"),
            kinds.BEHAVIOUR_PREFS, self._build_behaviour)
        self._add_section(container, gettext("Applications"),
            kinds.APPLICATION_PREFS, self._build_applications)
        layout = QtGui.QVBoxLayout()
        layout.addWidget(container)
        self.setLayout(layout)

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

    def _build_applications(self):
        # Build the custom dialogs group
        custom_dialog_init_cb = self._build_checkbox_field("custom-dialog-init",
            gettext("Use custom initialize dialog"))
        custom_dialog_branch_cb = self._build_checkbox_field("custom-dialog-branch",
            gettext("Use custom branch dialog"))
        custom_dialog_checkout_cb = self._build_checkbox_field("custom-dialog-checkout",
            gettext("Use custom checkout dialog"))
        custom_dialog_switch_cb = self._build_checkbox_field("custom-dialog-switch",
            gettext("Use custom switch dialog"))
        cd_layout = QtGui.QFormLayout()
        cd_layout.addRow(custom_dialog_init_cb)
        cd_layout.addRow(custom_dialog_branch_cb)
        cd_layout.addRow(custom_dialog_checkout_cb)
        cd_layout.addRow(custom_dialog_switch_cb)
        cd_group = QtGui.QGroupBox(gettext("Custom Dialogs"))
        cd_group.setLayout(cd_layout)

        # And the other controls
        suite_combo = self._build_combo_from_registry("app-suite",
            app_suite.app_suite_registry)
        terminal_cb = self._build_checkbox_field("advanced-commands-terminal",
            gettext("Open terminal shell for 'All commands'"))

        # Put it all together
        layout = QtGui.QFormLayout()
        layout.addRow(gettext("Suite"), suite_combo)
        layout.addRow(cd_group)
        layout.addRow(terminal_cb)
        return layout

    def _build_appearance(self):
        # Toolbar
        tbar_contents_combo = self._build_combo_field("toolbar-contents",
            self._toolbar_content_names())
        tbar_style_combo = self._build_combo_field("toolbar-style", [
            "text-under-icon",
            "text-beside-icon",
            "icon-only",
            "text-only",
            ])
        tbar_layout = QtGui.QFormLayout()
        tbar_layout.addRow(gettext("Contents"), tbar_contents_combo)
        tbar_layout.addRow(gettext("Style"), tbar_style_combo)
        tbar_group = QtGui.QGroupBox(gettext("Toolbar"))
        tbar_group.setLayout(tbar_layout)

        # Location Selector
        ls_style_combo = self._build_combo_from_registry(
            "location-selector-style",
            location_selector.location_selector_registry)
        ls_layout = QtGui.QFormLayout()
        ls_layout.addRow(gettext("Style"), ls_style_combo)
        ls_group = QtGui.QGroupBox(gettext("Location Selector"))
        ls_group.setLayout(ls_layout)

        # Toolbox
        tb_styles = ["tabs", "tabs-icons-only", "tabs-verbose", "tree"]
        tbox_style_combo = self._build_combo_field("toolbox-style", tb_styles)
        tbox_on_status = self._build_checkbox_field("toolbox-on-status-view",
            gettext("Show the toolbox on the status view"))
        tbox_on_repo = self._build_checkbox_field("toolbox-on-repository-view",
            gettext("Show the toolbox on the repository view"))
        tbox_on_welcome = self._build_checkbox_field("toolbox-on-welcome-view",
            gettext("Show the toolbox on the welcome view"))
        tbox_layout = QtGui.QFormLayout()
        tbox_layout.addRow(gettext("Style"), tbox_style_combo)
        tbox_layout.addRow(tbox_on_status)
        tbox_layout.addRow(tbox_on_repo)
        tbox_layout.addRow(tbox_on_welcome)
        tbox_group = QtGui.QGroupBox(gettext("Toolbox"))
        tbox_group.setLayout(tbox_layout)

        # Working Tree
        wt_style_combo = self._build_combo_field("workingtree-style", [
            "qbrowse",
            "classic",
            ])
        wt_layout = QtGui.QFormLayout()
        wt_layout.addRow(gettext("Style"), wt_style_combo)
        wt_group = QtGui.QGroupBox(gettext("Working Tree"))
        wt_group.setLayout(wt_layout)

        # Language
        lang_combo = self._build_combo_language()
        lang_layout = QtGui.QFormLayout()
        lang_layout.addRow(gettext("Language"), lang_combo)
        lang_group = QtGui.QGroupBox(gettext("User Interface"))
        lang_group.setLayout(lang_layout)

        layout = QtGui.QVBoxLayout()
        layout.addWidget(tbar_group)
        layout.addWidget(ls_group)
        layout.addWidget(tbox_group)
        layout.addWidget(wt_group)
        layout.addWidget(lang_group)
        return layout

    def _build_behaviour(self):
        # Status view
        wt_edit_cb = self._build_checkbox_field("workingtree-default-to-edit",
            gettext("Edit items in Working Tree browser by default"))
        auto_refresh_cb = self._build_checkbox_field("status-auto-refresh",
            gettext("Automatically refresh status report"))
        status_layout = QtGui.QFormLayout()
        status_layout.addRow(wt_edit_cb)
        status_layout.addRow(auto_refresh_cb)
        status_group = QtGui.QGroupBox(gettext("Status View"))
        status_group.setLayout(status_layout)

        # Dialog defaults
        ws_model_combo = self._build_combo_from_registry(
            "workspace-model",
            workspace_models.workspace_model_registry)
        bind_branches_cb = self._build_checkbox_field(
            "bind-branches-by-default",
            gettext("Bind branches by default"))
        checkout_branches_cb = self._build_checkbox_field(
            "lightweight-checkout-by-default",
            gettext("Lightweight checkout by default"))
        defaults_layout = QtGui.QFormLayout()
        defaults_layout.addRow(gettext("Workspace Model"), ws_model_combo)
        defaults_layout.addRow(bind_branches_cb)
        defaults_layout.addRow(checkout_branches_cb)
        defaults_group = QtGui.QGroupBox(gettext(
            "Default Values in Dialogs"))
        defaults_group.setLayout(defaults_layout)

        # Actions
        del_cb = self._build_checkbox_field("delete-to-trash",
            gettext("Delete files and directories to trash"))
        actions_layout = QtGui.QFormLayout()
        actions_layout.addRow(del_cb)
        actions_group = QtGui.QGroupBox(gettext("Actions"))
        actions_group.setLayout(actions_layout)

        # Put it all together
        layout = QtGui.QFormLayout()
        layout.addRow(status_group)
        layout.addRow(defaults_group)
        layout.addRow(actions_group)
        return layout

    def _build_checkbox_field(self, pref_name, text):
        default_value = DEFAULT_PREFERENCES.get(pref_name)
        current_value = self._preferences.get(pref_name, default_value)
        checkbox = QtGui.QCheckBox(text)
        checkbox.setChecked(current_value)

        # Set-up change detection
        def checkbox_changed(checked):
            value = checked == QtCore.Qt.Checked
            old_value = self._preferences.get(pref_name)
            self._preferences[pref_name] = value
            self._callback(pref_name, value, old_value, self)
            save_preferences(self._preferences)
        self.connect(checkbox, QtCore.SIGNAL("stateChanged(int)"),
            checkbox_changed)
        return checkbox

    def _build_combo_field(self, pref_name, pref_values):
        combo = QtGui.QComboBox()
        # Set-up options and current selection
        combo.insertItems(0, pref_values)
        default_value = DEFAULT_PREFERENCES.get(pref_name)
        current_value = self._preferences.get(pref_name, default_value)
        try:
            current_index = pref_values.index(current_value)
        except ValueError:
            # The current value is no longer legal. That can happen when
            # the registry underlying the combo model has items removed
            # thanks to plugins being uninstalled, say. Fall back to the
            # default.
            current_index = pref_values.index(default_value)
            self._preferences[pref_name] = default_value
        combo.setCurrentIndex(current_index)

        # Set-up change detection
        def combo_changed(value):
            # value is a QVariant that gets reused so we need a copy here
            value = str(value)
            old_value = self._preferences.get(pref_name)
            self._preferences[pref_name] = value
            self._callback(pref_name, value, old_value, self)
            save_preferences(self._preferences)
        self.connect(combo, QtCore.SIGNAL("currentIndexChanged(QString)"),
            combo_changed)
        return combo

    def _build_combo_from_registry(self, pref_name, registry):
        # TODO: Use the help as descriptions or tooltips?
        items = registry.keys()
        return self._build_combo_field(pref_name, items)

    def _toolbar_content_names(self):
        result = ["kind-specific"]
        actual_names = sorted(self._toolbar_builder.keys())
        for name in actual_names:
            # Skip names like "kind:foo and "empty:"
            if name.find(':') >= 0:
                continue
            result.append(name)
        return result

    def _build_combo_language(self):
        lang_combo = QtGui.QComboBox()
        current = i18n.get_current_locale() or 'C locale'
        # extract only language code
        cur_lang = current.split(':')[0].split('.')[0]
        lang_combo.addItem((gettext("System default (%s)") %
                            i18n.LANGUAGES.get(cur_lang, current)),
                           QtCore.QVariant(current))
        trans = []
        for t in i18n.get_available_translations():
            trans.append((gettext(i18n.LANGUAGES.get(t,t)), t))
        for item in sorted(trans):
            lang_combo.addItem(item[0], QtCore.QVariant(item[1]))
        self._preferences['language'] = current

        def change_language(index):
            pref_name = 'language'
            old = self._preferences[pref_name]
            new = str(lang_combo.itemData(index).toString())
            if old == new:
                return
            self._preferences[pref_name] = new
            i18n.save_default_language(new)
            self._callback(pref_name, new, old, self)
        self.connect(lang_combo, QtCore.SIGNAL('currentIndexChanged(int)'),
            change_language)
        return lang_combo
