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

import logging
log = logging.getLogger(__name__)

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

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

class Config(ConfigSection):
    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
    
    disconnect_message = _("The connection to the HellaNZB daemon was lost "
        "unexpectedly and trying to restart and reconnect failed.")
    
    # The HellaNZB configuration file to be used in stand-alone mode.
    managed_config_file = resources.get_config("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)
        
        self.hella_config_dirty_signal_id = None
    
    def _import_existing_hella_config(self):
        """
        Try to locate existing HellaNZB configuration files on the users
        machine and import it.
        """
        
        log.info(_("Looking for existing HellaNZB configuration files..."))
        
        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)
                
                if self.hella_config.servers:
                    log.info(_("HellaNZB configuration file %s imported.") \
                       % existing_config_file)
                    
                    return
                else:
                    # If no servers have been specified, the configuration file
                    # most probably hasn't been used, that's why we ignore it
                    # and use the default configuration values instead.
                    # On Debian/Ubuntu machines, this prevents DEST_DIR from
                    # being set to ~/.hellanzb/done because of the file
                    # /etc/hellanzb.conf.
                    self.hella_config.reset()
        
        log.info(_("Could not find any existing 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()
        
        # If the configuration is changed while the mode is active, try to
        # restart HellaNZB so that the changes take effect.
        self.hella_config_dirty_signal_id = self.hella_config.connect(
            "notify::dirty",
            self.handle_dirty_hella_config
        )
        
        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)
        
        # Make sure that the handler isn't connected more than one time in the
        # `enter` method. As soon as the mode isn't active anymore, no actions
        # need to be performed when the configuration is changed.
        self.hella_config.disconnect(self.hella_config_dirty_signal_id)
        
        if App().lock.own_lock():
            self.stopHella()
    
    def handle_dirty_hella_config(self, *args):
        if self.hella_config.dirty:
            config_file = self.hella_config.config_file
            config_file_manager = self.hella_config.config_manager[config_file]
            clean_config = config_file_manager.clean_config
            
            if clean_config.max_rate != self.hella_config.max_rate:
                clean_config.max_rate = self.hella_config.max_rate
                clean_config.save()
                
                if not self.hella_config.dirty:
                    App().backend.set_max_rate(self.hella_config.max_rate)
                    config_file_manager.check()
                    
                    return
            
            if not App().mode_manager.is_changing_mode:
                log.info(_("HellaNZB is restarted for the configuration "
                    "changes to take effect."))
                
                App().mode_manager.reenter_mode(
                    on_success=self.handle_dirty_hella_config
                )
    
    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:
            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():
                lines = process.stderr.readlines()
                
                if lines:
                    # We only take a look at the last error messages because the
                    # other ones might be deprecation warnings. Refer to bug
                    # #336639 for more information.
                    line = lines[-1]
                    
                    if "Cannot bind to XML RPC port" in line:
                        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."))

class AlreadyRunningError(Exception):
    pass
