# 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.

# This import statement is necessary so that loading default HellaNZB
# configuration files doesn't raise an unwanted exception.
# pylint: disable-msg=W0611
import os.path

import logging
log = logging.getLogger(__name__)

from gobject import list_properties
from os import getcwd
from os.path import isfile, join, expanduser
from string import ascii_lowercase, ascii_uppercase

from lottanzb.util import GObject, gproperty, gsignal, has_ssl_support, _
from lottanzb.resources.xdg import DATA_HOME, USER_DIRS

# The default HellaNZB data directory has been changed in order to adhere to
# the corresponding freedesktop.org standard. Existing users won't be affected.
_PREFIX_DIR = join(DATA_HOME, "hellanzb")

class ConfigManager(dict):
    """
    Holds references to all HellaConfig objects. The objects are grouped by
    the configuration file they point to.
    
    Whenever a configuration is changed, loaded or saved, ConfigFileManager
    will check which of the HellaConfig objects need to be marked as dirty
    (which means that such an object doesn't match the file on the hard disk
    anymore).
    """
    
    def add(self, hella_config):
        """
        Add a new HellaConfig object to the store.
        Create a namespace for this configuration file if it doesn't exist yet.
        """
        
        config_file = hella_config.config_file
        
        if not config_file in self:
            self[config_file] = ConfigFileManager(config_file)
        
        # Since the only place where HellaConfig objects are added to the
        # ConfigManager is in HellaConfig's constructor, we don't need to
        # check whether the object has already been added to the manager.
        self[config_file].add(hella_config)
    
    def remove(self, hella_config):
        """
        Remove a HellaConfig object from the store.
        """
        
        self[hella_config.config_file].remove(hella_config)

class ConfigFileManager(list):
    """
    Holds references to all HellaConfig objects pointing to a certain
    configuration file.
    
    Refer to the ConfigManager class for more information.
    """
    
    def __init__(self, config_file):
        self.config_file = config_file
        self._clean_config = None
        self._signal_ids = {}
        
        list.__init__(self)
    
    def add(self, hella_config):
        """
        Add a HellaConfig object to the store and set up all necessary hooks.
        """
        
        self.append(hella_config)
        self._signal_ids[hella_config] = [
            hella_config.connect("notify", self.on_config_changed),
            hella_config.connect("loaded", self.on_config_loaded),
            hella_config.connect("saved", self.on_config_saved)
        ]
    
    def remove(self, hella_config):
        """
        Remove a HellaConfig object from the store and remove all registered
        event handlers.
        """
        
        list.remove(self, hella_config)
        
        for signal_id in self._signal_ids[hella_config]:
            hella_config.disconnect(signal_id)
        
        del self._signal_ids[hella_config]
    
    def check(self, include=None):
        """
        Compare a single one or all configurations with the clean (unchanged)
        configuration and mark them as dirty if they're not equal.
        """
        
        if self.clean_config:
            include = include or self
            
            for config in include:
                if config is not self.clean_config:
                    config.dirty = self.clean_config != config
    
    def get_clean_config(self):
        return self._clean_config
    
    def set_clean_config(self, clean_config):
        """
        If clean_config exactly matches the corresponding configuration file
        on the disk, it can be registered using this function and will be
        used for comparison operations after it has been copied.
        """
        
        if self.clean_config is not clean_config:
            if self.clean_config:
                self.remove(self.clean_config)
                self._clean_config = None
            
            clean_config.dirty = False
            
            self._clean_config = clean_config.deep_copy()
        
        self.check()
    
    clean_config = property(get_clean_config, set_clean_config)
    
    def on_config_changed(self, changed_config, param):
        """
        Mark a configuration object as dirty when it has been changed.
        """
        
        if self.clean_config and not changed_config.loading and \
            not param.name == "dirty":
            self.check(include=[changed_config])
    
    def on_config_loaded(self, loaded_config, config_file):
        """
        Mark a configuration object as not dirty after it has been loaded and
        check whether the other configuration objects differ from this one and
        mark them as dirty if necessary.
        """
        
        if self.config_file == config_file:
            self.clean_config = loaded_config
    
    def on_config_saved(self, saved_config):
        """
        Saving a configuration object to the configuration file it points to
        causes all other configuration objects that aren't equal, but pointing
        to the same file to be marked as dirty.
        """
        
        self.clean_config = saved_config

class HellaConfig(GObject):
    prefix_dir = gproperty(
        type    = str,
        default = _PREFIX_DIR,
        nick    = "Directory prefix",
        blurb   = "This property doesn't have any effect in stand-alone mode.")
    
    queue_dir = gproperty(
        type    = str,
        default = join(_PREFIX_DIR, "nzb", "daemon.queue"),
        nick    = "Queue directory",
        blurb   = "Queued NZB files are stored here.")
    
    dest_dir = gproperty(
        type    = str,
        default = USER_DIRS.get("XDG_DOWNLOAD_DIR", expanduser("~/Downloads")),
        nick    = "Download directory",
        blurb   = "Completed downloads go here. Depending on your "
                  "configuration, they will already be validated and "
                  "extracted.")
    
    current_dir = gproperty(
        type    = str,
        default = join(_PREFIX_DIR, "nzb", "daemon.current"),
        nick    = "Current download directory",
        blurb   = "The NZB file currently being downloaded is stored here.")
    
    working_dir = gproperty(
        type    = str,
        default = join(_PREFIX_DIR, "nzb", "daemon.working"),
        nick    = "Working directory",
        blurb   = "The archive currently being downloaded is stored here.")
    
    postponed_dir = gproperty(
        type    = str,
        default = join(_PREFIX_DIR, "nzb", "daemon.postponed"),
        nick    = "Directory containing postponed downloads",
        blurb   = "Archives interrupted in the middle of downloading are "
                  "stored here temporarily.")
    
    processing_dir = gproperty(
        type    = str,
        default = join(_PREFIX_DIR, "nzb", "daemon.processing"),
        nick    = "Processing directory",
        blurb   = "Archives currently being processed are stored here. It "
                  "may contain archive directories or symbolic links to "
                  "archive directories.")
    
    processed_subdir = gproperty(
        type    = str,
        default = "processed",
        nick    = "Directory containing processed files",
        blurb   = "Sub-directory within the NZB archive directory to move "
                  "processed files to.")
    
    temp_dir = gproperty(
        type    = str,
        default = join(_PREFIX_DIR, "nzb", "daemon.temp"),
        nick    = "Temporary storage directory")
    
    max_rate = gproperty(
        type    = int,
        nick    = "Maximum download speed",
        blurb   = "Limit all server connections to the specified KB/s.")
    
    state_xml_file = gproperty(
        type    = str,
        default = join(_PREFIX_DIR, "nzb", "hellanzbState.xml"),
        nick    = "Location of HellaNZB's state file",
        blurb   = "This file is used to store HellaNZB's state when HellaNZB "
                  "isn't running. The state is intermittently written out as "
                  "XML to this file. It includes the order of the queue and "
                  "SmartPAR recovery information.")
    
    debug_mode = gproperty(
        type    = str,
        nick    = "Debug log file",
        blurb   = "Log debug messages to the specified file.")
    
    log_file = gproperty(
        type    = str,
        default = join(_PREFIX_DIR, "log"),
        nick    = "Log file",
        blurb   = "Log output to the specified file. Set to None for no "
                  "logging.")
    
    log_file_max_bytes = gproperty(
        type    = long,
        nick    = "Maximum log file size",
        blurb   = "Automatically roll over both log files when they reach "
                  "LOG_FILE_MAX_BYTES size.")
    
    log_file_backup_count = gproperty(
        type    = int,
        nick    = "Number of log files to be backed up")
    
    delete_processed = gproperty(
        type    = bool,
        default = True,
        nick    = "Remove the PROCESSED_SUBDIR if the archive was "
                  "successfully post-processed",
        blurb   = "Warning: The normal LOG_FILE should be enabled with "
                  "this option - for a record of what HellaNZB deletes.")
    
    cache_limit = gproperty(
        type    = str,
        default = "0",
        nick    = "Maximum amount of memory used to cache encoded article "
                  "data segments",
        blurb   = "HellaNZB will write article data to disk when this cache "
                  "is exceeded."
                  "Available settings: -1 for unlimited size, "
                  "0 to disable cache (only cache to disk), "
                  "> 0 to limit cache to this size, in bytes, KB, MB, "
                  "e.g. 1024 '1024KB' '100MB' '1GB'.")
    
    smart_par = gproperty(
        type    = bool,
        default = True,
        nick    = "Download PAR files only if necessary",
        blurb   = "Damaged downloads can be repaired using recovery files. "
                  "Downloading them only if necessary can significantly "
                  "decrease the data to transfer.")
    
    skip_unrar = gproperty(
        type    = bool,
        default = False,
        nick    = "Don't automatically extract downloads",
        blurb   = "Downloads regularly consists of compressed RAR archives. "
                  "HellaNZB can overtake the task of extracting the desired "
                  "files.")
    
    # We don't need a hard-coded default value here since HellaNZB will
    # automatically figure out the location of 'unrar' and 'rar' executables
    # no matter what value 'SKIP_UNRAR' is set to.
    unrar_cmd = gproperty(
        type    = str,
        default = "",
        nick    = "Path to unrar command",
        blurb   = "The unrar application is used for automatic extraction of "
                  "downloaded files and is usually located in /usr/bin.")
    
    # We don't need a hard-coded default value here since HellaNZB will
    # automatically figure out the location of the 'par2' executable.
    par2_cmd = gproperty(
        type    = str,
        default = "",
        nick    = "Path to the par2 command",
        blurb   = "The par2 application is used to check whether the "
                  "downloaded files are damaged or not and is usually located "
                  "in /usr/bin.")
    
    umask = gproperty(
        type    = int,
        nick    = "Force umask",
        blurb   = "HellaNZB inherits the umask from the current user's "
                  "environment (unless it's running in daemon mode).")
    
    max_decompression_threads = gproperty(
        type    = int,
        default = 2,
        nick    = "Maximum number of files to decompress at the same time",
        blurb   = "Please note that extracting downloaded files is a "
                  "ressource-demanding task and your system might become "
                  "unresponsive if many of these processes are running at "
                  "the same time.")
    
    xmlrpc_server = gproperty(
        type    = str,
        default = "localhost",
        nick    = "XML RPC hostname",
        blurb   = "Hostname for the XML RPC client to connect to. Defaults "
                  "to 'localhost'.")
    
    xmlrpc_password = gproperty(
        type    = str,
        default = "changeme",
        nick    = "XML RPC password",
        blurb   = "You might probably never use this, but the command line "
                  "XML RPC calls do - it should definitely be changed from "
                  "its default value. The XML RPC username is hardcoded as "
                  "'hellanzb'.")
    
    xmlrpc_port = gproperty(
        type    = int,
        default = 8760,
        nick    = "XML RPC port number",
        blurb   = "Port number the XML RPC server will listen on and the "
                  "client will connect to. None for no XML RPC server.")
    
    xmlrpc_server_bind = gproperty(
        type    = str,
        default = "127.0.0.1",
        nick    = "Bind XML RPC server to IP address",
        blurb   = "IP address on which the XML RPC server will be bound "
                  "to. '0.0.0.0' for any interfaces, '127.0.0.1' will "
                  "disable remote access.")
    
    newzbin_username = gproperty(
        type    = str,
        nick    = "Newzbin.com username",
        blurb   = "Newzbin.com username for automatic NZB downloading")
    
    newzbin_password = gproperty(
        type    = str,
        nick    = "Newzbin.com password",
        blurb   = "Newzbin.com password for automatic NZB downloading")
    
    categorize_dest = gproperty(
        type    = bool,
        default = True,
        nick    = "Categorize Newzbin.com downloads",
        blurb   = "Save archives into a sub-directory of DEST_DIR named after "
                  "their Newzbin.com category  e.g. Apps, Movies, Music")
    
    macbinconv_cmd = gproperty(
        type    = str,
        nick    = "Path to the optional macbinconv command",
        blurb   = "This command is used to convert MacBinary files.")
    
    growl_notify = gproperty(
        type    = bool,
        default = False,
        nick    = "Enable Mac OS X Growl notifications")
    
    growl_server = gproperty(
        type    = str,
        default = "IP",
        nick    = "Growl notification server")
    
    growl_password = gproperty(
        type    = str,
        default = "password",
        nick    = "Growl password")
    
    libnotify_notify = gproperty(
        type    = bool,
        default = False,
        nick    = "Enable libnotify daemon notifications")
    
    disable_colors = gproperty(
        type    = bool,
        default = False,
        nick    = "Disable ANSI color codes in the main screen",
        blurb   = "Preserves the in-place scroller.")
    
    disable_ansi = gproperty(
        type    = bool,
        default = False,
        nick    = "Disable ALL ANSI color codes in the main screen",
        blurb   = "For terminals that don't support ANY ANSI codes.")
    
    nzb_zips = gproperty(
        type    = str,
        default = ".nzb.zip",
        nick    = "Support extracting NZBs from ZIP files with this suffix "
                  "in QUEUE_DIR",
        blurb   = "Defaults to '.nzb.zip'. Set to False to disable. Case "
                  "insensitive.")
    
    nzb_gzips = gproperty(
        type    = str,
        default = ".nzb.gz",
        nick    = "Support extracting NZBs from GZIP files with this suffix "
                  "in QUEUE_DIR",
        blurb   = "Defaults to '.nzb.gz'. Set to False to disable. Case "
                  "insensitive.")
    
    external_handler_script = gproperty(
        type    = str,
        nick    = "Optional external handler script",
        blurb   = "HellaNZB will run this script after having post-processed "
                  "a download.")
    
    nzbqueue_mdelay = gproperty(
        type    = float,
        default = 10.0,
        nick    = "NZB queue delay",
        blurb   = "Delay enqueueing new, recently modified NZB files added to "
                  "the QUEUE_DIR until this many seconds have passed since "
                  "the NZB's last modification time. Defaults to 10 seconds.")
    
    keep_file_types = gproperty(
        nick    = "File types to keep",
        blurb   = "Don't get rid of these file types when finished "
                  "post-processing. Move them to PROCESSED_SUBDIR instead. "
                  "Case insensitive.")
    
    other_nzb_file_types = gproperty(
        nick    = "Alternative NZB file extensions",
        blurb   = "List of alternative file extensions matched as NZB files "
                  "in the QUEUE_DIR. The 'nzb' file extension is always "
                  "matched.")
    
    not_required_file_types = gproperty(
        nick    = "Not required file types",
        blurb   = "If any of the following file types are missing from the "
                  "archive and cannot be repaired, continue processing "
                  "because they are unimportant. Case insensitive.")
    
    servers = gproperty(type=object)
    music_types = gproperty(type=object)
    
    dirty = gproperty(
        type    = bool,
        default = True,
        blurb   = "Has to be true whenever this configuration and the saved "
                  "one aren't equal. So whenever a property is changed, "
                  "it's set to True until one calls the save method or the "
                  "load method. Calling the save method causes all other "
                  "configuration objects pointing to the same configuration "
                  "file to be marked as dirty. "
                  "This mechanism makes it possible to decide more "
                  "intelligently, whether the configuration file needs to be "
                  "saved or not (in the modes module).")
    
    gsignal("saved")
    gsignal("loaded", str)
    
    config_manager = ConfigManager()
    
    def __init__(self, config_file, read_only=False):
        GObject.__init__(self)
        
        self._initialize_object_properties()
        
        self.special_keys = ["servers", "music_types", "dirty"]
        
        self.loading = False
        self.read_only = read_only
        self.config_file = config_file
        
        self.last_server_touched = None
        
        self.connect("notify::servers", self._ensure_main_server)
        
        # Add this configuration object to the store.
        self.config_manager.add(self)
    
    def __eq__(self, other):
        """
        Check if two HellaNZB configurations are totally equal.
        """
        
        for key in self.keys():
            if key != "dirty" and self[key] != other[key]:
                return False
        
        return True
    
    def __ne__(self, other):
        """
        Check if there are any differences between two HellaNZB 
        configurations.
        """
        
        return not self.__eq__(other)
    
    def __getinitargs__(self):
        return (self.config_file, )
    
    def __getattr__(self, key):
        try:
            return self.get_property(key.lower())
        except:
            try:
                return self.__dict__[key]
            except:
                raise AttributeError
    
    def __setattr__(self, key, value):
        try:
            self.set_property(key.lower(), value)
        except:
            self.__dict__[key] = value
    
    def _initialize_object_properties(self):
        """
        Assigns meaningful initial values to properties of type 'object'.
        
        GObject properties of type 'object' can't have default values.
        """
        
        self.keep_file_types = ["nfo", "txt"]
        self.other_nzb_file_types = []
        self.not_required_file_types = [
            "log", "m3u", "nfo", "nzb", "sfv", "txt"
        ]
        
        self.servers = []
        self.music_types = []
    
    def get_property(self, key):
        """
        There are some HellaNZB configuration values which can be either
        integer or string for example. This method ensures that those
        properties are correctly saved.
        """
        
        value = GObject.get_property(self, key)
        none_keys = ["newzbin_username", "newzbin_password", "macbinconv_cmd"]
        
        if key in none_keys and not value:
            return None
        elif key in ["nzb_zips", "nzb_gzips"] and value == "False":
            return False
        elif key == "cache_limit":
            try:
                return int(value)
            except:
                pass
        
        return value
    
    def load(self, custom_config_file=None):
        config_file = custom_config_file or self.config_file
        
        if not isfile(config_file):
            raise HellaConfig.FileNotFoundError(config_file)
        
        self.loading = True
        self.servers = []
        self.music_types = []
        
        def defineServer(**kwargs):
            self.add_server(Server(**kwargs))
        
        def defineMusicType(*args):
            self.music_types.append(MusicType(*args))
        
        # Syntax sugar. *g*
        Hellanzb = self
        
        try:
            try:
                execfile(config_file)
            except Exception, e:
                raise HellaConfig.LoadingError(str(e), config_file)
            else:
                self.emit("loaded", config_file)
                
                log.debug(_("The HellaNZB configuration file %s has been loaded "
                    "successfully.") % config_file)
        finally:
            self.loading = False
    
    def add_server(self, server):
        def notify(*args):
            self.last_server_touched = server
            self.notify("servers")
        
        self.servers.append(server)
        
        notify()
        server.connect("notify", notify)
    
    def _ensure_main_server(self, *args):
        """
        Ensure that there is at least one server with a `fillserver` value of 0.
        
        If there's no server with a `fillserver` value of 0, the server with the
        lowest `fillserver` value becomes the new main server. However, this
        method is so intelligent that it won't revert the change of the
        `fillserver` value that has just been made if not absolutely necessary.
        
        This method doesn't need to be called by clients of this class as it's
        automatically called whenever a server is removed or changed.
        """
        
        main_servers = self.get_main_servers()
        
        if self.servers and not main_servers:
            new_main_server = None
            
            fill_servers = self.get_fill_servers()
            
            if len(fill_servers) == 1:
                # We only have a single server, whose `fillserver` value always
                # needs to be 0.
                new_main_server = fill_servers[0]
            else:
                for server in fill_servers:
                    if server is not self.last_server_touched:
                        if new_main_server is None or \
                            server.fillserver < new_main_server.fillserver:
                            new_main_server = server
            
            log.info(_("'%s' is now the new main server.") % \
                new_main_server.id)
            
            new_main_server.fillserver = 0
    
    def get_main_servers(self):
        """
        Return a list of the main servers that will always be used to download.
        """
        
        if self.servers:
            return [server for server in self.servers if server.fillserver == 0]
        else:
            return []
    
    def get_fill_servers(self):
        """
        Return a list of the fillservers that will only be used for downloads if
        it's not possible to download a certain file using the main servers.
        """
        
        if self.servers:
            return [server for server in self.servers if server.fillserver > 0]
        else:
            return []
    
    def remove_server(self, server):
        self.servers.remove(server)
        
        if self.last_server_touched is server:
            self.last_server_touched = None
        
        self.notify("servers")
    
    def save(self):
        if self.read_only:
            return
        
        try:
            config_file = open(self.config_file, "w")
            config_file.write(self.getConfigStr())
            config_file.close()
        except Exception, e:
            raise Exception(_("Unable to save the HellaNZB configuration to "
                "the file %s: %s") % (self.config_file, str(e)))
        else:
            self.emit("saved")
            
            log.debug(_("The HellaNZB preferences were successfully written "
                "into the file %s." % self.config_file))
    
    def reset(self):
        """
        Changes all options back to their default values and removes all
        servers and music types from the configuration.
        """
        
        for key in self.keys():
            self[key] = self.get_property_default_value(key)
        
        self._initialize_object_properties()
    
    # TODO: Better method name
    def getConfigStr(self):
        output = "# -*- coding: utf-8 -*-\n"
        output += "# HellaNZB configuration file - Managed by LottaNZB\n\n"
        
        for server in self.servers:
            output += server.getConfigStr()
        
        for music_type in self.music_types:
            output += music_type.getConfigStr()
        
        # These preferences should be commented out 'if not value:'
        deactivable = [
            "external_handler_script",
            "macbinconv_cmd",
            "umask", 
            "unrar_cmd",
            "par2_cmd",
            "debug_mode"
        ]
        
        for property in list_properties(self):
            key = property.name.replace("-", "_")
            value = self[key]
            prefix = ""
            
            if key in self.special_keys:
                continue
            
            if key in deactivable and not value:
                prefix = "# "
            
            if type(value) == str:
                value = "\"" + value + "\""
            else:
                value = str(value)
            
            output += "\n# " + property.nick + "\n"
            
            if property.blurb:
                output += "# " + property.blurb + "\n"
            
            # Don't use the locale dependent `upper` method of the key string.
            # Fixes bug #318328.
            output += "%sHellanzb.%s = %s\n" % (
                prefix, self.upper_ascii(key), value
            )
        
        return output
    
    def newzbin_support(self):
        return bool(self.newzbin_username and self.newzbin_password)
    
    @staticmethod
    def upper_ascii(value):
        """
        Returns a copy of value, but with lower case letters converted to
        upper case.
        
        Compared to the built-in string method `upper`, this function
        is locale-independent and only turns ASCII letters to upper case.
        """
        
        result = ""
        
        for letter in value:
            try:
                result += ascii_uppercase[ascii_lowercase.index(letter)]
            except ValueError:
                result += letter
        
        return result
    
    @staticmethod
    def locate():
        """
        Locate existing HellaNZB configuration files on the user's system.
        """
        
        places = [getcwd(), join(getcwd(), "etc"), "/etc"]
        
        for place in places:
            config_file = join(place, "hellanzb.conf")
            
            if isfile(config_file):
                return config_file
    
    class LoadingError(Exception):
        def __init__(self, message, config_file):
            self.message = message
            self.config_file = config_file
        
        def __str__(self):
            return _("Unable to load the HellaNZB configuration file %s: %s") \
                % (self.config_file, self.message)
    
    class FileNotFoundError(LoadingError):
        def __init__(self, config_file):
            message = _("File not found.")
            
            HellaConfig.LoadingError.__init__(self, message, config_file)

class Server(GObject):
    id = gproperty(type=str)
    username = gproperty(type=str)
    password = gproperty(type=str)
    address = gproperty(type=str)
    port = gproperty(type=int, default=119, minimum=1, maximum=65535)
    antiIdle = gproperty(type=int, default=270, minimum=0)
    idleTimeout = gproperty(type=int, default=30)
    connections = gproperty(type=int, default=8, minimum=1)
    ssl = gproperty(type=bool, default=False)
    fillserver = gproperty(type=int, default=0, minimum=0)
    enabled = gproperty(type=bool, default=True)
    skipGroupCmd = gproperty(type=bool, default=False)
    bindTo = gproperty(type=str)
    
    def _get_hosts(self):
        return ["%s:%i" % (self.address, self.port)]
    
    def _set_hosts(self, hosts):
        if hosts:
            host = hosts[0]
            
            try:
                address, port = host.split(":")
                
                self.address = address
                self.port = int(port)
            except ValueError:
                pass
    
    hosts = gproperty(type=object, getter=_get_hosts, setter=_set_hosts)
    
    def __init__(self, **kwargs):
        GObject.__init__(self)
        
        for key, value in kwargs.iteritems():
            try:
                self.set_property(key, value)
            except AttributeError:
                log.warning(_("Unsupported server option '%s'.") % key)
            except ValueError:
                raise ValueError(_("Invalid server option '%s'.") % key)
    
    def __cmp__(self, other):
        return cmp(self.getConfigStr(), other.getConfigStr())
    
    def getConfigStr(self):
        options = []
        
        for key in self.keys():
            value = self[key]
            
            if self.get_property_type(key) is str:
                if key in ("username", "password") and not value:
                    value = None
                else:
                    value = "'%s'" % value
            
            if not key in ("address", "port"):
                options.append("%s=%s" % (key, value))
        
        return "defineServer(%s)\n" % (", ".join(options))
    
    def needs_authentication(self):
        return bool(self.username and self.password)
    
    def set_property(self, key, value):
        """
        Automatically adjust the port number if SSL is enabled or disabled.
        
        Also make sure that SSL isn't enabled if the host doesn't have support
        for SSL. No exception is raised in this case as we want any HellaNZB
        configuration to be loaded successfully.
        """
        
        if key == "ssl":
            if value and not has_ssl_support():
                log.warning(_("SSL support has been disabled because the "
                    "necessary Python bindings for OpenSSL aren't installed."))
                
                value = False
            
            # Only switch between the two default ports and still allow users to
            # specify custom ports.
            if value:
                if self.port == 119:
                    self.port = 563
            elif self.port == 563:
                self.port = 119
        
        GObject.set_property(self, key, value)

class MusicType:
    """Defines a music file type and whether or not HellaNZB should attempt to
    decompress the music if it comes across this type of file.
    """
    
    def __init__(self, extension, decompressor, decompressToType):
        self.extension = extension
        self.decompressor = decompressor
        self.decompressToType = decompressToType
    
    def __cmp__(self, other):
        return cmp(self.extension, other.extension)
    
    def __str__(self):
        return "<MusicType: %s>" % self.extension
    
    def shouldDecompress(self):
        return self.decompressor is not None
    
    def getConfigStr(self):
        def escape(value):
            if type(value) == str:
                return "\"" + value + "\""
            
            return value
        
        return "defineMusicType(%s, %s, %s)\n" % (
            escape(self.extension),
            escape(self.decompressor),
            escape(self.decompressToType))
