# Copyright (C) 2008-2009 LottaNZB Development Team
# 
# 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; version 3.
# 
# 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 re
import gtk

import logging
log = logging.getLogger(__name__)

from gobject import idle_add

from kiwi.datatypes import ValidationError
from kiwi.ui.delegates import Delegate, SlaveDelegate
from kiwi.ui.objectlist import Column
from kiwi.ui.dialogs import info, error
from kiwi.ui.widgets.entry import ProxyEntry

from lottanzb import resources
from lottanzb.core import App
from lottanzb.hellaconfig import Server
from lottanzb.util import gproperty, has_ssl_support, _

class Window(Delegate):
    """
    The preferences window used to edit both LottaNZB's and HellaNZB's settings.
    
    Each preferences category has its own tab, whereas every tab is represented
    by a certain class.
    """
    
    gladefile = "prefs_window"
    
    hella_config = gproperty(
        type    = object,
        nick    = "Current HellaNZB configuration",
        blurb   = "The HellaConfig object that is used by the currently "
                  "running HellaNZB instance. This property is `None` if "
                  "LottaNZB is in front-end mode, because LottaNZB cannot "
                  "change the configuration in this case."  
    )
    
    lotta_config = gproperty(
        type    = object,
        nick    = "Active LottaNZB configuration"
    )
    
    def __init__(self):
        Delegate.__init__(self)
        
        self.lotta_config = App().config
        
        # The `hella_config` property must always point to the active HellaNZB
        # configuration object.
        self.update_hella_config()
        
        App().mode_manager.connect_async("notify::active-mode",
            self.update_hella_config)
        
        self.tabs = []
        self.default_tabs = []
        
        self.general_tab = PrefsTabGeneral(self)
        self.server_tab = PrefsTabServers(self)
        self.plugins_tab = PrefsTabPlugins(self)
        
        self.default_tabs.extend([
            self.general_tab,
            self.server_tab,
            self.plugins_tab
        ])
        
        # Both the 'General' and the 'Plug-ins' tab will always be visible.
        self.add_tab(self.general_tab)
        self.add_tab(self.plugins_tab)
        
        # The 'Servers' tab will only be visible if the HellaNZB confiugration
        # file is writable.
        self.update_server_tab()
        self.connect("notify::hella-config", self.update_server_tab)
        
        # Show the 'General' tab
        self.notebook.set_current_page(0)
    
    def update_hella_config(self, *args):
        """
        Changes the `hella_config` property point to the HellaNZB configuration
        used by the current usage mode unless there isn't any or it isn't
        writable.
        """
        
        hella_config = App().mode_manager.current_hella_config
        
        if hella_config and not hella_config.read_only:
            self.hella_config = hella_config
        else:
            self.hella_config = None
    
    def update_server_tab(self, *args):
        """
        Makes sure that the 'Servers' tab is only be visible if the HellaNZB
        confiugration file is writable.
        """
        
        if self.hella_config and not self.server_tab in self.tabs:
            self.add_tab(self.server_tab)
        elif not self.hella_config and self.server_tab in self.tabs:
            self.remove_tab(self.server_tab)
    
    def add_tab(self, tab):
        """
        Add a tab to the preferences window.
        """
        
        name = tab.toplevel_name
        position = len(self.tabs)
        
        eventbox = gtk.EventBox()
        eventbox.show()
        
        setattr(self, name, eventbox)
        
        for index, other_tab in enumerate(self.tabs):
            if self._compare_tabs(tab, other_tab):
                position = index
                break
        
        self.tabs.insert(position, tab)
        self.notebook.insert_page(eventbox, gtk.Label(tab.label), position)
        self.attach_slave(name, tab)
        
        delattr(self, name)
    
    def remove_tab(self, tab):
        """Removes a tab from the preferences window."""
        
        name = tab.toplevel_name
        eventbox = tab.toplevel.parent
        
        if eventbox and name in self.slaves:
            self.tabs.remove(tab)
            self.detach_slave(name)
            self.notebook.remove_page(self.notebook.page_num(eventbox))
    
    def _compare_tabs(self, tab, other_tab):
        """
        Can be used to check if a tab has a higher priority than an other tab.
        
        This method is used to determine the position of certain tabs in the
        preferences window.
        
        Returns `True` if the tab passed as the first parameter should be placed
        in before the tab passed as the second parameter, `False` otherwise.
        """
        
        # Default always have to be at the front of the tab list.
        if other_tab in self.default_tabs:
            if tab in self.default_tabs:
                return self.default_tabs.index(tab) - \
                    self.default_tabs.index(other_tab) <= 0
            else:
                return False
        elif tab in self.default_tabs:
            return True
        else:
            # Alphabetically sort the non-default preferences tabs (plugin
            # tabs), so that the order of the tabs is always the same.
            return tab.toplevel_name < other_tab.toplevel_name
    
    def attach_slave(self, name, tab):
        """
        Kiwi doesn't want to attach SlaveViews to dynamically created EventBox
        containers if the window was created using glade. That's why this method
        temporarily cuts the link to the glade adaptor.
        """
        
        adaptor = self._glade_adaptor
        
        self._glade_adaptor = None
        Delegate.attach_slave(self, name, tab)
        self._glade_adaptor = adaptor
    
    def on_prefs_window__response(self, window, response):
        """
        Saves the LottaNZB configuration when the preferences window is being
        closed.
        """
        
        if response == gtk.RESPONSE_CLOSE:
            self.lotta_config.save()
        
        window.destroy()
    
    def on_prefs_window__set_focus(self, window, widget):
        """
        Temporarily prevents modes.standalone.Mode from restarting HellaNZB
        while the user edits a text field.
        
        Of course, this only applies if there's really a HellaNZB configuration.
        """
        
        if self.hella_config:
            if self.hella_config.get_data("frozen"):
                self.hella_config.thaw_notify()
                self.hella_config.set_data("frozen", False)
            
            if isinstance(widget, ProxyEntry):
                self.hella_config.freeze_notify()
                self.hella_config.set_data("frozen", True)

class PrefsTabBase(SlaveDelegate):
    label = ""
    gladefile = ""
    lotta_fields = []
    hella_fields = []
    
    def __init__(self, prefs_window):
        self.prefs_window = prefs_window
        self.lotta_config = prefs_window.lotta_config
        self.hella_config = prefs_window.hella_config
        
        SlaveDelegate.__init__(self)
        
        self.lotta_proxy = self.add_proxy(self.lotta_config, self.lotta_fields)
        self.hella_proxy = self.add_proxy(None, self.hella_fields)
        
        # Watch for changes of the HellaNZB configuration object.
        self.on_hella_config_changed()
        self.prefs_window.connect(
            "notify::hella-config", self.on_hella_config_changed)
    
    def on_hella_config_changed(self, *args):
        """
        Makes sure that the input fields always represent the currently used
        HellaNZB configuration.
        """
        
        self.hella_config = self.prefs_window.hella_config
        self.hella_proxy.set_model(self.hella_config)

class PrefsTabGeneral(PrefsTabBase):
    gladefile = "prefs_tab_general"
    label = _("General")
    lotta_fields = ["backend.delete_enqueued_nzb_files"]
    hella_fields = ["smart_par", "max_rate"]
    
    # TODO: Documentation needed, if possible more elegant code.
    max_rate_initialized = False
    
    def update_unproxified_fields(self, *args):
        """
        Some fields in this tab aren't kept in sync with the HellaNZB
        configuration by Kiwi because the widget type is not supported or the
        data needs to be converted in some way.
        
        This is why this method can be used to update these widgets with the
        values by the HellaNZB configuration.
        """
        
        self.dest_dir.set_current_folder(self.hella_config.dest_dir)
        self.enforce_max_rate.set_active(bool(self.hella_config.max_rate))
        self.unrar.set_active(not self.hella_config.skip_unrar)
    
    def on_dest_dir__selection_changed(self, dialog):
        """
        Stores the selected download directory in the HellaNZB configuration
        because this is not done automatically by Kiwi.
        """
        
        if self.hella_config:
            self.hella_config.dest_dir = dialog.get_current_folder()
    
    def on_enforce_max_rate__toggled(self, widget):
        """
        Toggles the sensitivity of the download speed spin widget if the
        download speed limit is enabled or disabled, respectively.
        
        If LottaNZB is in front-end mode, directly alter the maximum download
        speed using the corresponding API method. Let the usage mode do this
        otherwise.
        """
        
        enforce = widget.read()
        
        self.max_rate.set_sensitive(bool(enforce))
        self.max_rate_scale.set_sensitive(bool(enforce))
        
        if not enforce:
            if self.hella_config:
                self.hella_config.max_rate = 0
            else:
                App().backend.set_max_rate(0)
    
    def on_max_rate__content_changed(self, widget):
        """
        If LottaNZB is in front-end mode, this method directly alters the
        maximum download speed using the corresponding API method.
        """
        
        if not self.hella_config and self.max_rate_initialized:
            App().backend.set_max_rate(widget.read())
    
    def on_unrar__content_changed(self, widget):
        if self.hella_config:
            self.hella_config.skip_unrar = not widget.read()
    
    def on_hella_config_changed(self, *args):
        """
        Makes sure that the widgets in this tab display the information of
        the configuration object that is currently being used.
        
        Hide all HellaNZB configuration widgets if the configuration is not
        accessible or cannot be changed.
        """
        
        PrefsTabBase.on_hella_config_changed(self, *args)
        
        for i in range(1, 5):
            widget = self.get_widget("hella_config_%i" % i)
            widget.set_property("visible", bool(self.hella_config))
        
        if self.hella_config:
            # Refer to the `update_unproxified_fields` method for more
            # information.
            self.update_unproxified_fields()
            
            for field in ["dest-dir", "skip-unrar", "max-rate"]:
                self.hella_config.connect("notify::%s" % field,
                    self.update_unproxified_fields)
        else:
            # Get the current maximum download speed using the corresponding
            # API method.
            def apply_max_rate():
                max_rate = App().backend.get_max_rate()
                
                self.enforce_max_rate.set_active(bool(max_rate))
                self.max_rate.update(max_rate)
                self.max_rate_initialized = True
            
            idle_add(apply_max_rate)

class PrefsTabServers(PrefsTabBase):
    gladefile = "prefs_tab_servers"
    label = _("Servers")
    
    def __init__(self, prefs_window):
        PrefsTabBase.__init__(self, prefs_window)
        
        ssl_icon_filename = resources.get_glade("small_lock.png")
        ssl_pixbuf = gtk.gdk.pixbuf_new_from_file(ssl_icon_filename)
        
        def format_ssl(ssl):
            if ssl:
                return ssl_pixbuf
        
        self.server_list.set_columns([
            ServerInfoColumn(),
            Column("ssl", width=25, data_type=gtk.gdk.Pixbuf, \
                format_func=format_ssl)
        ])
    
    def on_hella_config_changed(self, *args):
        PrefsTabBase.on_hella_config_changed(self, *args)
        
        if self.hella_config:
            self.server_list.add_list(self.hella_config.servers)
            
            # Automatically select the first server in the list.
            if len(self.server_list):
                self.server_list.select(self.server_list[0])
        else:
            self.server_list.clear()
    
    def remove_selected_server(self):
        server = self.server_list.get_selected()
        
        # Don't allow the user to have an empty server list.
        if server and len(self.server_list) > 1:
            question = _("Are you sure you want to delete this server?")
            
            if info(question, buttons=gtk.BUTTONS_OK_CANCEL) == gtk.RESPONSE_OK:
                self.hella_config.remove_server(server)
                self.server_list.remove(server, True)
                
                # It's necessary to refresh the whole server list as other
                # servers might have been changed as well (`fillserver` value).
                self.server_list.refresh()
    
    def on_server_list__key_press_event(self, widget, event):
        if event.keyval == gtk.keysyms.Delete:
            self.remove_selected_server()
    
    def on_add__clicked(self, widget):
        def handle_response(widget, response):
            if response == gtk.RESPONSE_OK:
                self.server_list.append(dialog.server, True)
                self.hella_config.add_server(dialog.server)
        
        dialog = ServerDialog(server_count=len(self.server_list))
        dialog.show(self.prefs_window)
        
        dialog.toplevel.connect("response", handle_response)
    
    def on_remove__clicked(self, widget):
        self.remove_selected_server()
    
    def on_edit__clicked(self, widget, *args):
        def handle_response(widget, response):
            if response == gtk.RESPONSE_OK:
                server.update(dialog.server)
                
                # It's necessary to refresh the whole server list as other
                # servers might have been changed as well (`fillserver` value).
                self.server_list.refresh()
        
        server = self.server_list.get_selected()
        server_count = len(self.server_list)
        
        dialog = ServerDialog(server.deep_copy(), server_count=server_count)
        dialog.show(self.prefs_window)
        
        dialog.toplevel.connect("response", handle_response)
    
    def on_server_list__selection_changed(self, widget, server):
        self.edit.set_sensitive(bool(server))
        
        # Disable the "Remove" button if the list only contains a single server.
        # Entering stand-alone mode will fail if there's no server.
        self.remove.set_sensitive(bool(server) and len(self.server_list) > 1)
    
    on_server_list__row_activated = on_edit__clicked

class ServerInfoColumn(Column):
    def __init__(self):
        Column.__init__(self, "id", expand=True, use_markup=True)
    
    def _cell_data_text_func(self, tree_column, renderer, model, treeiter, \
        (column, renderer_prop)):
        server = model[treeiter][0]
        
        if server.needs_authentication():
            username = server.username
        else:
            username = "<i>%s</i>" % _("No authentication required")
        
        if server.fillserver:
            # LottaNZB users are much more likely to be used to the term
            # 'Backup server' compared to 'Fillserver'.
            first_line = _("%s (Backup server)") % server.id
        else:
            first_line = server.id
        
        first_line = "<b>%s</b>" % first_line
        
        text = "%s\n<small>%s\n%s</small>" % \
            (first_line, server.address, username)
        
        renderer.set_property(renderer_prop, text)

class PrefsTabPlugins(PrefsTabBase):
    gladefile = "prefs_tab_plugins"
    label = _("Plug-ins")
    
    def __init__(self, prefs_window):
        PrefsTabBase.__init__(self, prefs_window)
        
        self.plugin_config = self.lotta_config.plugins
        self.plugin_tabs = {}
        
        self.plugins = App().plugin_manager.plugins
        
        plugin_names = self.plugins.keys()
        plugin_names.sort()
        
        self.plugin_list.set_columns([
            PluginEnabledColumn(),
            PluginIconColumn(),
            PluginDescriptionColumn()
        ])
        
        def on_plugin_enabled_changed(config, param, plugin):
            self.update_prefs_tab(plugin)
        
        def on_plugin_locked(plugin, param):
            self.plugin_list.update(plugin)
        
        for name in plugin_names:
            plugin = self.plugins[name]
            
            plugin.config.connect("notify::enabled",
                on_plugin_enabled_changed, plugin)
            plugin.connect("notify::locked", on_plugin_locked)
            
            self.update_prefs_tab(plugin)
            self.plugin_list.append(plugin)
    
    def update_prefs_tab(self, plugin, *args):
        tab_cls = plugin.get_prefs_tab_class()
        
        if tab_cls:
            if plugin.enabled and not plugin in self.plugin_tabs:
                tab = tab_cls(self.prefs_window, plugin)
                
                self.plugin_tabs[plugin] = tab
                self.prefs_window.add_tab(tab)
            elif not plugin.enabled and plugin in self.plugin_tabs:
                self.prefs_window.remove_tab(self.plugin_tabs[plugin])
                del self.plugin_tabs[plugin]

class PluginEnabledColumn(Column):
    """
    Column that contains the checkbox used to enable and disable the
    corresponding plug-in.
    
    If the plug-in is locked, the checkbox will be insensitive.
    """
    
    def __init__(self):
        Column.__init__(self, "enabled", data_type=bool, editable=True)
    
    def _on_renderer_toggle_check__toggled(self, *args):
        """
        Displays an error message if the plug-in cannot be enabled. This might
        be due to missing dependencies.
        """
        
        from lottanzb.plugins import PluginEnablingError
        
        try:
            Column._on_renderer_toggle_check__toggled(self, *args)
        except PluginEnablingError, exc:
            log.error(str(exc))
            error(exc.message)
    
    def _cell_data_text_func(self, tree_column, renderer, model, treeiter,
            (column, renderer_prop)):
        Column._cell_data_text_func(self, tree_column, renderer, model,
            treeiter, (column, renderer_prop))
        
        plugin = model[treeiter][0]
        
        renderer.set_property("activatable", not plugin.locked)

class PluginIconColumn(Column):
    """
    Column that simply displays a package icon.
    
    Depending on the plug-in being locked or not, the package icon will be
    greyed out.
    """
    
    def __init__(self):
        normal_icon_filename = resources.get_glade("package.png")
        locked_icon_filename = resources.get_glade("package_locked.png")
        
        self.normal_icon = gtk.gdk.pixbuf_new_from_file(normal_icon_filename)
        self.locked_icon = gtk.gdk.pixbuf_new_from_file(locked_icon_filename)
        
        Column.__init__(self, "name", data_type=gtk.gdk.Pixbuf)

    def _cell_data_text_func(self, tree_column, renderer, model, treeiter,
        (column, renderer_prop)):
        
        plugin = model[treeiter][0]
        
        if plugin.locked:
            icon = self.locked_icon
        else:
            icon = self.normal_icon
        
        renderer.set_property(renderer_prop, icon)

class PluginDescriptionColumn(Column):
    """
    The column that holds both the plug-in title and its description.
    """
    
    def __init__(self):
        self._normal_color = None
        
        Column.__init__(self, "name", expand=True, use_markup=True)
    
    def on_attach_renderer(self, renderer):
        renderer.set_property("foreground-set", True)
        
        self._normal_color = renderer.get_property("foreground-gdk")
        
        # TODO: Is it possible to get the 'official' insensitivity color here?
        # This might have an effect depending on the theme.
        self._locked_color = gtk.gdk.color_parse("gray")
    
    def _cell_data_text_func(self, tree_column, renderer, model, treeiter, \
        (column, renderer_prop)):
        plugin = model[treeiter][0]
        text = "<b>%s</b>" % plugin.title
        
        # Displaying both the reason for the lock and the verbose description
        # would use too much space.
        if plugin.locked and plugin.lock_message:
            text += "\n<small>%s</small>" % plugin.lock_message
        elif plugin.description:
            text += "\n<small>%s</small>" % plugin.description
        
        if plugin.locked:
            color = self._locked_color
        else:
            color = self._normal_color
        
        renderer.set_property(renderer_prop, text)
        renderer.set_property("foreground-gdk", color)

class ServerDialog(Delegate):
    gladefile = "server_dialog"
    fields = ["id", "address", "port", "username", "password", "ssl",
        "connections"]
    
    def __init__(self, server=None, server_count=0):
        Delegate.__init__(self)
        
        self.server = server or Server()
        
        # The number of server that will exist if the dialog isn't closed using
        # 'Cancel'.
        self.server_count = server_count
        
        self.id.mandatory = True
        self.address.mandatory = True
        
        if server:
            self.toplevel.set_title(_("Edit server"))
            self.auth_required.set_active(self.server.needs_authentication())
        else:
            self.toplevel.set_title(_("Add a new server"))
            self.auth_required.set_active(True)
            self.server_count += 1
        
        # When dealing with only one server, `fillserver` will always be set to
        # 0 and it shouldn't be possible to change it using the UI.
        # When dealing with two or more servers, display a checkbox that makes
        # it possible to set `fillserver` to 0 or 1, which should cover most of
        # the usage scenarios.
        if self.server_count > 1:
            self.fillserver.show()
            self.fillserver.set_active(bool(self.server.fillserver))
        
        self.register_validate_function(self.validity)
        self.proxy = self.add_proxy(self.server, self.fields)
        
        # Ensures that the username and password entries are marked as
        # mandatory.
        self.auth_required.emit("toggled")
        
        self.focus_topmost()
    
    def validity(self, valid):
        self.save.set_sensitive(valid)
    
    def on_id__validate(self, widget, name):
        # TODO: What characters are allowed in fact?
        if not re.compile("^[A-Za-z0-9_() -]+$").match(name):
            return ValidationError(_("Contains invalid characters."))
    
    def on_auth_required__toggled(self, widget):
        required = widget.read()
        
        for widget in [self.username_label, self.password_label]:
            widget.set_sensitive(required)
        
        for widget in [self.username, self.password]:
            widget.mandatory = required
            
            if not required:
                widget.set_text("")
            
            widget.set_sensitive(required)
            widget.validate()
    
    def on_cancel__clicked(self, widget):
        self.hide()
    
    on_save__clicked = on_cancel__clicked
    
    def on_ssl__toggled(self, widget):
        if widget.read() and not has_ssl_support():
            title = _("OpenSSL for Python not installed")
            detailed = _("In order to use SSL, you need to install the Python "
                "bindings for OpenSSL. The package is usually called "
                "python-openssl.")
            
            error(title, detailed, self.toplevel)
        
        # We need to manually read the new `ssl` and `port` value from the model
        # as they might have been changed while the model was updating the `ssl`
        # and therefore didn't receive any notifications. 
        # It's ugly, but it works.
        self.proxy.update("ssl")
        self.proxy.update("port")
    
    def on_fillserver__toggled(self, widget):
        self.server.fillserver = widget.read()
