# Copyright (C) 2008 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 pango
import gobject

import logging
log = logging.getLogger(__name__)

from kiwi.ui.delegates import Delegate, SlaveDelegate
from kiwi.datatypes import ValidationError
from kiwi.ui.objectlist import Column
from kiwi.ui.dialogs import info, error
from kiwi.python import Settable

from lottanzb.core import App
from lottanzb.modes import standalone
from lottanzb.hellaconfig import Server
from lottanzb.util import _

class PrefsWindow(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 in this file.
    
    This class ensures that copies of both HellaNZB's and LottaNZB's settings
    are created and saved and applied properly when the user hits 'Save'.
    
    TODO: This window is currently meant to be used only in stand-alone mode,
    since most settings only make sense if LottaNZB is able and authorized to
    modify HellaNZB's configuration file. However, some of the preferences are
    also of interest in other usage modes (such as bandwidth throttling). That's
    why we need to make it even more modular. Blueprint:
    
    https://blueprints.launchpad.net/lottanzb/+spec/modular-preferences-window
    """
    
    gladefile = "prefs_window"
    
    def __init__(self):
        """
        Creates temporary copies of both HellaNZB's and LottaNZB's settings and
        displays the preferences window.
        """
        
        Delegate.__init__(self)
        
        self.active_mode = App().mode_manager.active_mode
        
        if not self.active_mode:
            raise ValueError("The preferences window requires an usage mode to "
                 "be active.")
        elif not isinstance(self.active_mode, standalone.Mode):
            raise ValueError("The preferences window can only be used in "
                "stand-alone mode.")
        
        self.hella_config = self.active_mode.hella_config.deep_copy()
        self.lotta_config = App().config.deep_copy()
        
        self.add_tab(PrefsTabGeneral)
        self.add_tab(PrefsTabServers)
        self.add_tab(PrefsTabNewzbin)
        self.add_tab(PrefsTabCategories)
        
        self.show()
    
    def add_tab(self, tab_class):
        """
        Add a tab to the preferences window.
        
        @param tab_class: The class the tab has to be created with.
        @type tab_class: Subclass of PrefsTabBase
        """
        
        tab = tab_class(self.lotta_config, self.hella_config)
        
        # Kiwi doesn't want to attach SlaveViews to dynamically created EventBox
        # containers if the window was created using glade. That's why we have
        # to temporarily cut the link to the glade adaptor.
        adaptor = self._glade_adaptor
        eventbox = gtk.EventBox()
        eventbox.show()
        
        setattr(self, tab.toplevel_name, eventbox)
        
        self.tabs.append_page(eventbox, gtk.Label(tab.label))
        self._glade_adaptor = None
        self.attach_slave(tab.toplevel_name, tab)
        self._glade_adaptor = adaptor
    
    def on_save__clicked(self, widget):
        """
        Save the configuration files of both LottaNZB and HellaNZB and decide
        whether to restart HellaNZB or not.
        
        @param widget: The save button.
        @type widget: gtk.Button
        """
        
        # HellaNZB's maximum download speed can be changed using XMLRPC, that's
        # why the HellaNZB daemon isn't restarted if and only if this HellaNZB
        # option has been altered.
        max_rate = self.hella_config.max_rate
        max_rate_changed = self.active_mode.hella_config.max_rate != max_rate
        
        # Sync the download speed in both configuration versions so that it
        # doesn't get in the way during the comparison of the two.
        self.active_mode.hella_config.max_rate = max_rate
        
        App().config.merge(self.lotta_config)
        App().config.save()
        
        if self.active_mode.hella_config != self.hella_config:
            # It's official. We need to restart HellaNZB in order to make sure
            # that the changes take effect.
            log.info(_("HellaNZB needs to be restarted for the configuration "
                "changes to take effect."))
            
            # Create a new mode object rather than just inserting the new
            # configuration. This way, we enable the modes module to recover
            # properly from an invalid configuration file.
            mode_config = self.lotta_config.modes.standalone
            mode = standalone.Mode(mode_config, self.hella_config)
            
            App().mode_manager.set_mode(mode)
        elif max_rate_changed:
            # Only the maximum download speed has been changed.
            self.active_mode.hella_config.save()
            App().backend.set_max_rate(max_rate)
        
        self.hide()
    
    def on_cancel__clicked(self, widget):
        """
        Simply closes the preferences window if the user clicks on the 'Cancel'
        button. Since all changes were applied to proper configuration copies,
        there's nothing to revert.
        """
        
        self.hide()

class PrefsTabBase(SlaveDelegate):
    label = ""
    gladefile = ""
    lotta_fields = []
    hella_fields = []
    
    def __init__(self, lotta_config, hella_config):
        self.lotta_config = lotta_config
        self.hella_config = hella_config
        
        SlaveDelegate.__init__(self)
        
        if self.lotta_fields:
            self.add_proxy(self.lotta_config, self.lotta_fields)
        
        if self.hella_fields:
            self.add_proxy(self.hella_config, self.hella_fields)

class PrefsTabGeneral(PrefsTabBase):
    gladefile = "prefs_tab_general"
    label = _("General")
    lotta_fields = ["gui.start_minimized"]
    hella_fields = ["smart_par", "libnotify_notify", "max_rate"]
    
    def __init__(self, lotta_config, hella_config):
        PrefsTabBase.__init__(self, lotta_config, hella_config)
        
        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):
        self.hella_config.DEST_DIR = dialog.get_current_folder()
    
    def on_enforce_max_rate__toggled(self, widget):
        enforce = widget.read()
        
        if not enforce:
            self.max_rate.update(0)
        
        self.max_rate.set_sensitive(enforce)
        self.max_rate_scale.set_sensitive(enforce)
    
    def on_unrar__content_changed(self, widget):
        self.hella_config.SKIP_UNRAR = not widget.read()

class PrefsTabServers(PrefsTabBase):
    gladefile = "prefs_tab_servers"
    label = _("Servers")
    
    def __init__(self, lotta_config, hella_config):
        PrefsTabBase.__init__(self, lotta_config, hella_config)
        
        ssl_icon_filename = App().glade_dir("small_lock.svg")
        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)
        ])
        
        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])
    
    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.server_list.remove(server, True)
                self.hella_config.remove_server(server)
    
    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()
        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)
                self.server_list.update(server)
        
        server = self.server_list.get_selected()
        dialog = ServerDialog(server.deep_copy())
        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")
        
        text = "<b>%s</b>\n<small>%s\n%s</small>" % \
            (server.id, server.address, username)
        
        renderer.set_property(renderer_prop, text)

class PrefsTabNewzbin(PrefsTabBase):
    gladefile = "prefs_tab_newzbin"
    label = _("Newzbin")
    hella_fields = ["newzbin_username", "newzbin_password"]
    
    def __init__(self, lotta_config, hella_config):
        PrefsTabBase.__init__(self, lotta_config, hella_config)
        
        self.enable.set_active(self.hella_config.newzbin_support())
    
    def on_enable__toggled(self, widget):
        enabled = widget.read()
        
        if not enabled:
            self.newzbin_username.set_text("")
            self.newzbin_password.set_text("")
        
        for field in [self.newzbin_username, self.newzbin_password,
            self.newzbin_username_label, self.newzbin_password_label]:
            
            field.set_sensitive(enabled)

class PrefsTabCategories(PrefsTabBase):
    gladefile = "prefs_tab_categories"
    label = _("Categories")
    lotta_fields = ["enabled"]
    creation_row = None
    creation_hint = _("Add a new category...")
    
    def __init__(self, lotta_config, hella_config):
        PrefsTabBase.__init__(self, lotta_config.plugins.categories, \
            hella_config)
        
        self.categories_column = CategoryColumn(self.creation_hint)
        self.categories_list.set_columns([self.categories_column]) 
        
        renderer = self.categories_column.renderer
        renderer.connect("editing-started", self.on_editing_started)
        
        self.add_creation_cell()
        
        for category in self.lotta_config.categories_list:
            self.categories_list.append(Settable(category=category))
        
        self.set_enabled(self.lotta_config.enabled)
    
    def add_creation_cell(self):
        self.creation_row = Settable(category=self.creation_hint)
        self.categories_list.insert(0, self.creation_row)
    
    def on_categories_list__cell_edited(self, widget, category_row, event):
        category = category_row.category
        
        if category_row is self.creation_row:
            if not category:
                category_row.category = self.creation_hint
                self.categories_list.update(category_row)
            else:
                self.add_creation_cell()
                gobject.idle_add(widget.get_treeview().scroll_to_point, 0, 0)
        elif not category:
            self.categories_list.remove(category_row, True)
        
        self.lotta_config.categories_list = []
        
        for category_row in self.categories_list:
            if not category_row is self.creation_row:
                self.lotta_config.categories_list.append(category_row.category)
    
    def on_categories_list__key_press_event(self, widget, event):
        selected_row = self.categories_list.get_selected()
        
        if event.keyval == gtk.keysyms.Delete and \
            selected_row is not self.creation_row:
            self.categories_list.remove(selected_row, True)
    
    def on_editing_started(self, renderer, editable, path):
        category_row = self.categories_list[path]
        
        if category_row.category == self.creation_hint:
            editable.delete_text(0, -1)
    
    def on_enabled__toggled(self, widget):
        self.set_enabled(widget.read())
    
    def set_enabled(self, enabled):
        self.categories_list.set_sensitive(enabled)
        self.categories_label.set_sensitive(enabled)
        
        if enabled:
            script = App().find_resource("code", "postprocessor.py")
        else:
            script = ""
        
        self.hella_config.external_handler_script = script
        
        if not enabled:
            self.categories_list.unselect_all()

class CategoryColumn(Column):
    def __init__(self, magic_content):
        Column.__init__(self, "category", editable=True, use_markup=True, \
            expand=True)
        
        self.magic_content = magic_content
        self.renderer = None
    
    def on_attach_renderer(self, renderer):
        self.renderer = renderer
    
    def renderer_func(self, renderer, data):
        if data == self.magic_content:
            renderer.set_property("foreground", "gray")
            renderer.set_property("style", pango.STYLE_ITALIC)
        else:
            renderer.set_property("foreground", None)
            renderer.set_property("style", pango.STYLE_NORMAL)

class ServerDialog(Delegate):
    gladefile = "server_dialog"
    fields = ["id", "address", "port", "username", "password", "ssl",
        "connections"]
    
    def __init__(self, server=None):
        Delegate.__init__(self)
        
        self.server = server or Server()
        self.id.mandatory = True
        self.address.mandatory = True
        
        size_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
        size_group.add_widget(self.id_label)
        size_group.add_widget(self.username_label)
        
        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.register_validate_function(self.validity)
        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()
        self.show()
    
    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():
            try:
                import OpenSSL
            except ImportError:
                widget.set_active(False)
                error(_("OpenSSL for Python not installed"), \
                    ("In order to use SSL, you need to install the Python"
                     "bindings for OpenSSL. The package is usually called"
                     "python-openssl."), self.toplevel)
