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

# This import statement is necessary so that loading default HellaNZB
# configuration files doesn't raise an unwanted exception.

import os.path

import logging
log = logging.getLogger(__name__)

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

from lottanzb.core import App
from lottanzb.util import GObject, gproperty, _

class HellaConfig(GObject):
    prefix_dir = gproperty(
        type    = str,
        default = App().home_dir(".hellanzb"),
        nick    = "Directory prefix",
        blurb   = "This property doesn't have any effect in stand-alone mode.")
    
    queue_dir = gproperty(
        type    = str,
        default = App().home_dir(".hellanzb", "nzb", "daemon.queue"),
        nick    = "Queue directory",
        blurb   = "Queued NZB files are stored here.")
    
    dest_dir = gproperty(
        type    = str,
        default = App().home_dir("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 = App().home_dir(".hellanzb", "nzb", "daemon.current"),
        nick    = "Current download directory",
        blurb   = "The NZB file currently being downloaded is stored here.")
    
    working_dir = gproperty(
        type    = str,
        default = App().home_dir(".hellanzb", "nzb", "daemon.working"),
        nick    = "Working directory",
        blurb   = "The archive currently being downloaded is stored here.")
    
    postponed_dir = gproperty(
        type    = str,
        default = App().home_dir(".hellanzb", "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 = App().home_dir(".hellanzb", "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 = App().home_dir(".hellanzb", "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 = App().home_dir(".hellanzb", "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 = App().home_dir(".hellanzb", "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.")
    
    unrar_cmd = gproperty(
        type    = str,
        default = "/usr/bin/unrar",
        nick    = "Path to unrar command",
        blurb   = "The unrar application is used for automatic extraction of "
                  "downloaded files and is usually located in /usr/bin.")
    
    par2_cmd = gproperty(
        type    = str,
        default = "/usr/bin/par2",
        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.")
    
    # Holds references to all HellaConfig objects. The objects are grouped by
    # the configuration file they point to.
    __configurations = {}
    
    def __init__(self, config_file, read_only=False):
        GObject.__init__(self)
        
        # 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 = []
        
        # This property has to be true whenever this configuration and the saved
        # one aren't equal. So whenever a property is changed, dirty is 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).
        self.dirty = True
        
        self.read_only = read_only
        self.config_file = config_file
        
        # Create a namespace for this configuration file if it doesn't exist yet
        if not self.config_file in self.__configurations:
            self.__configurations[self.config_file] = []
        
        # Add this configuration object to the store.
        self.__configurations[self.config_file].append(self)
    
    def __eq__(self, other):
        """Check if two HellaNZB configurations are totally equal."""
        
        for key in self.keys():
            if self[key] != other[key]:
                return False
          
        if self.servers != other.servers:
            return False
        
        if self.music_types != other.music_types:
            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[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 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_username", "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 set_property(self, key, value):
        GObject.set_property(self, key, value)
        
        self.dirty = True
    
    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.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:
            execfile(config_file)
        except Exception, e:
            raise HellaConfig.LoadingError(str(e), config_file)
        else:
            if not custom_config_file:
                self.dirty = False
            
            log.debug(_("The HellaNZB configuration file %s has been loaded "
                "successfully." % config_file))
        
    def add_server(self, server):
        # TODO: We need an object that enables us to observe such changes.
        # This wrapper method isn't very elegant.
        self.dirty = True
        self.servers.append(server)
        
        def mark_as_dirty(server, param):
            self.dirty = True
        
        server.connect("notify", mark_as_dirty)
    
    def remove_server(self, server):
        self.servers.remove(server)
        self.dirty = True
    
    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:
            # Saving this 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 this object to be marked
            # as dirty.
            for config in self.__configurations[self.config_file]:
                config.dirty = self != config
            
            log.debug(_("The HellaNZB preferences were successfully written "
                "into the file %s." % self.config_file))
    
    # 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", "debug_mode"]
        
        for property in list_properties(self):
            key = property.name.replace("-", "_")
            value = self[key]
            prefix = ""
            
            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 a existing HellaNZB configuration file on the user's system.
        """
        
        places = [
            getcwd(),
            join(getcwd(), "etc"),
            App().home_dir(".hellanzb"),
            "/etc",
            "/etc/hellanzb"
        ]
        
        for place in places:
            file = join(place, "hellanzb.conf")
            
            if isfile(file):
                return 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)
    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)
    
    _hosts = []
    
    def _get_hosts(self):
        return self._hosts
    
    def _set_hosts(self, hosts):
        def to_host(host):
            if isinstance(host, Host):
                return host
            else:
                return Host(host)
        
        self._hosts = map(to_host, hosts)
    
    hosts = gproperty(type=object, getter=_get_hosts, setter=_set_hosts)
    
    def _get_address(self):
        try:
            return self.hosts[0].address
        except:
            return Host.address.default
    
    def _set_address(self, value):
        if not self.hosts:
            self.hosts.append(Host(value))
        else:
            self.hosts[0].address = value
    
    address = gproperty(type=str, getter=_get_address, setter=_set_address)
    
    def _get_port(self):
        try:
            return self.hosts[0].port
        except:
            return Host.port.default
    
    def _set_port(self, value):
        if not self.hosts:
            self.hosts.append(Host("", value))
        else:
            self.hosts[0].port = value
    
    port = gproperty(type=int, getter=_get_port, setter=_set_port)
    
    def __init__(self, **kwargs):
        GObject.__init__(self)
        
        self.hosts = []
        
        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_option_type(key) is str:
                if key in ("username", "password") and not value:
                    value = None
                else:
                    value = "'%s'" % value
            
            if key == "hosts":
                value = [host.uri for host in self.hosts]
            
            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)

class Host(GObject):
    address = gproperty(type=str)
    port = gproperty(type=int, default=119, minimum=1)
    
    def _get_uri(self):
        return "%s:%s" % (self.address, self.port)
    
    def _set_uri(self, uri):
        try:
            parts = uri.split(":")
            
            self.address = parts[0]
            self.port = int(parts[1])
        except:
            pass
    
    uri = gproperty(type=str, getter=_get_uri, setter=_set_uri)
    
    def __init__(self, address, port=119):
        GObject.__init__(self)
        
        if ":" in address:
            self.uri = address
        else:
            self.address = address
            self.port = port
    
    def __getinitargs__(self):
        return (self.address, self.port)
    
    def __str__(self):
        return self.uri

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))
