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

import logging
log = logging.getLogger(__name__)

from time import sleep
from subprocess import Popen, PIPE
from distutils.spawn import find_executable

from kiwi.ui.delegates import SlaveDelegate

from lottanzb.util import get_hellanzb_cmd, gproperty, _
from lottanzb.core import App
from lottanzb.config import GConfigSection
from lottanzb.hellaconfig import HellaConfig, Server
from lottanzb.modes.base import Mode as _Mode

class Config(GConfigSection):
    hellanzb_command = gproperty(type=str, default=get_hellanzb_cmd())

class Mode(_Mode):
    title = _("Stand-alone mode")
    short = _("Stand-alone")
    description = _("Use LottaNZB as a normal NZB Usenet client. When "
        "launching LottaNZB, HellaNZB is started in the background. You can "
        "easily change the HellaNZB preferences from within LottaNZB.")
    
    icon = gtk.STOCK_PREFERENCES
    
    # The HellaNZB configuration file to be used in stand-alone mode.
    managed_config_file = App().user_dir("hellanzb.conf")
    
    def __init__(self, config, hella_config=None):
        """
        Creates a new stand-alone mode instance with its own LottaNZB and
        HellaNZB configuration object.
        
        @param config: The config section that holds all required stand-alone
        mode options.
        @type config: lottanzb.modes.standalone.Config
        @param hella_config: Use an already existing HellaConfig object.
        If None, LottaNZB automatically loads the existing configuration file
        and - if necessary - creates it using the default values or another
        existing HellaNZB file.
        @type hella_config: lottanzb.hellaconfig.HellaConfig
        """
        
        _Mode.__init__(self, config)
        
        if hella_config:
            self.hella_config = hella_config
        else:
            self.hella_config = HellaConfig(self.managed_config_file)
            
            try:
                self.hella_config.load()
            except HellaConfig.FileNotFoundError:
                self._import_existing_hella_config()
            except HellaConfig.LoadingError, error:
                self.init_error = str(error)
    
    def _import_existing_hella_config(self):
        """
        Try to locate existing HellaNZB configuration files on the users
        machine and import it.
        """
        
        log.debug("A HellaNZB configuration file managed by LottaNZB doesn't "
            "exist yet. Trying to locate other HellaNZB configuration files in "
            "different places...")
        
        existing_config_file = HellaConfig.locate()
        
        if existing_config_file:
            try:
                self.hella_config.load(existing_config_file)
            except HellaConfig.LoadingError:
                pass
            else:
                # The default configuration file bundled with HellaNZB has a
                # dummy server entry which is completely useless. That's why
                # it's removed here.
                for server in self.hella_config.servers:
                    if server.id == "changeme" and server.username == "changeme":
                        self.hella_config.remove_server(server)
                        
                        log.debug("Dummy server entry removed from HellaNZB "
                           "configuration file %s." % existing_config_file)
                
                log.debug("HellaNZB configuration file %s imported." \
                   % existing_config_file)
        else:
            log.debug("Could not find any HellaNZB configuration files in "
               "common places.")
    
    def enter(self):
        """
        Saves the configuration stored in the hella_config property if
        necessary, does some initial configuration validation and finally tries
        to launch the HellaNZB daemon.
        """
        
        if self.hella_config.config_file != self.managed_config_file:
            raise ValueError("The HellaNZB configuration file %s isn't "
                "managed by LottaNZB." % self.hella_config.config_file)
        
        assert self.config.hellanzb_command and self.hella_config.servers
        
        # In LottaNZB 0.3 `unrar-free` was the default value for the UNRAR_CMD
        # option in HellaNZB's configuration on Ubuntu systems. Unfortunately,
        # a majority of archives that can be downloaded from the Usenet cannot
        # be extracted using this free alternative.
        # That's why we decided to switch back to the proprietary `unrar` in
        # LottaNZB 0.4. If there are any user who have used LottaNZB 0.3 and
        # didn't change to `unrar` manually, we do it for them using the
        # following lines of code.
        if self.hella_config.unrar_cmd.endswith("unrar-free"):
            nonfree_unrar_cmd = find_executable("unrar")
            
            # Since LottaNZB >= 0.4 depends on `unrar`, this should always be
            # a valid path to the `unrar` executable.
            if nonfree_unrar_cmd:
                self.hella_config.unrar_cmd = nonfree_unrar_cmd
                
                log.info(_("LottaNZB now uses the more reliable 'unrar' "
                    "application to extract downloaded archives."))
        
        if self.hella_config.dirty:
            self.hella_config.save()
        
        self.startHella()
        
        _Mode.enter(self)
    
    def get_connection_args(self):
        return (
            self.hella_config.XMLRPC_SERVER,
            self.hella_config.XMLRPC_PORT,
            self.hella_config.XMLRPC_PASSWORD
        )
    
    def leave(self):
        _Mode.leave(self)
        
        if App().lock_acquired:
            self.stopHella()
    
    def handle_disconnect(self, *args):
        try:
            self.reenter()
        except Exception, e:
            message = _("The connection to the HellaNZB daemon was lost "
                "unexpectedly and trying to restart and reconnect failed.") + \
                "\n\n" + str(e)
            
            App().mode_manager.show_selection_window(error_message=message)
    
    def usesCorrectConfigFile(self):    
        try:
            proxy = App().backend.checkConnection(*self.get_connection_args())
            
            return proxy.status()["config_file"] == self.hella_config.config_file
        except:
            pass
        
        return False
    
    def startHella(self):
        generalMessage = _("Could not start the HellaNZB daemon.")
        
        try:
            self.fixMissingSSLModule()
            
            if self.usesCorrectConfigFile():
                log.info(_("The HellaNZB daemon is already running using the "
                    "configuration file managed by LottaNZB."))
                
                return
            
            command = "%s -D -c %s" % \
                (self.config.hellanzb_command, self.hella_config.config_file)
            process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE)
            
            if process.wait():
                line = process.stderr.readline()
                
                if line:
                    if line.find("Address already in use") != -1:
                        message = _("The port %s is already in use. Another "
                            "HellaNZB daemon might be already running.") % \
                            (self.hella_config.XMLRPC_PORT)
                        
                        raise AlreadyRunningError, message
                    else:
                        raise Exception, line
                else:
                    raise Exception, _("The HellaNZB daemon could not be "
                        "started for an unknown reason.")
            
            log.info(_("Started HellaNZB daemon."))
        except (OSError, ValueError), e:
            log.error(generalMessage)
            
            if e.errno == 2:
                raise Exception, _("Could not find the HellaNZB executable "
                    "(%s).") % (self.config.hellanzb_command)
            else:
                raise Exception, str(e)
        except:
            log.error(generalMessage)
            raise
    
    def stopHella(self):
        try:
            # The output of the HellaNZB executable is passed to grep,
            # which will return anything in this case. We don't want to show
            # the output to the user.
            command = '%s -c %s shutdown | grep nothingatall' % \
                (self.config.hellanzb_command, self.hella_config.config_file)
            
            Popen(command, shell=True)
            
            # Try to avoid starting an instance of HellaNZB while an old one is
            # still being shut down.
            if not App().is_quitting:
                running = True
                
                while running:
                    try:
                        # This will raise an exception as soon as the
                        # connection can't be established anymore. It's not a
                        # very elegant approach but it's certainly better than
                        # the old one.
                        App().backend.checkConnection(*self.get_connection_args())
                    except:
                        running = False
                    
                    sleep(0.2)
        
        # This shouldn't happen in general.
        except (OSError, ValueError), e:
            log.error(_("Could not stop the HellaNZB daemon."))
            raise
        else:
            self.process = None
            log.info(_("HellaNZB daemon stopped."))
    
    # Some magic behind the scenes
    def fixMissingSSLModule(self):
        try:
            import OpenSSL
        except ImportError:
            for server in self.hella_config.servers:
                server.ssl = False
    
    def get_config_view(self):
        return ConfigView(self)

class AlreadyRunningError(Exception):
    pass

class ConfigView(SlaveDelegate):
    gladefile = "standalone_config_view"
    
    lotta_config_fields = ["hellanzb_command"]
    hella_config_fields = ["address", "port", "username", "password"]
    
    def __init__(self, mode):
        self.mode = mode
        
        SlaveDelegate.__init__(self)
        
        for widget in [self.address, self.port, self.hellanzb_command]:
            widget.mandatory = True
        
        size_group = gtk.SizeGroup(gtk.SIZE_GROUP_HORIZONTAL)
        size_group.add_widget(self.get_widget("address_label"))
        size_group.add_widget(self.get_widget("username_label"))
        size_group.add_widget(self.get_widget("hellanzb_command_label"))
        
        if not self.mode.hella_config.servers:
            self.mode.hella_config.add_server(Server(id=_("Default Server")))
        
        self.server = self.mode.hella_config.servers[0]
        
        self.add_proxy(self.mode.config, self.lotta_config_fields)
        self.add_proxy(self.server, self.hella_config_fields)
        
        self.auth_required.set_active(self.server.needs_authentication())
        
        # The toggle event isn't emitted if needs_authentication() is False.
        # That's why it's emitted manually here.
        self.auth_required.emit("toggled")
    
    def on_auth_required__toggled(self, widget):
        required = widget.read()
        
        self.username_label.set_sensitive(required)
        self.password_label.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()
