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

import logging
log = logging.getLogger(__name__)

from threading import Lock

from lottanzb.core import App
from lottanzb.util import Thread, GObject, gproperty, gsignal, _
from lottanzb.config import ConfigSection
from lottanzb.modes import base, standalone, local_frontend, remote_frontend

mode_list = [standalone.Mode, local_frontend.Mode, remote_frontend.Mode]

class Config(ConfigSection):
    """Represents the configuration section 'modes'"""
    
    # This section only contains a single option: The string representation of
    # the currently active usage mode, as defined by each Mode's name.
    active = gproperty(type=str)
    
    # Subsections. Every single mode has its own place to store config data.
    standalone = gproperty(type=object)
    local_frontend = gproperty(type=object)
    remote_frontend = gproperty(type=object)
    
    def set_property(self, key, value):
        """Ensures that the 'active' option never holds an invalid value."""
        
        if key == "active" and value:
            # Makes it possible to directly assign a Mode based class or an
            # instance of it to the 'active' option.
            try:
                value = value.get_name()
            except:
                pass
            
            if not value in self:
                # Resetting the mode name is probably the better idea than
                # raising an exception.
                value = ""
        
        ConfigSection.set_property(self, key, value)

class ModeManager(GObject):
    active_mode = gproperty(type=object)
    
    gsignal("unexpected-mode-leaving", str)
    
    def __init__(self, config):
        self.config = config
        
        # This lock ensures that set_mode(...) cannot be called if another mode
        # is still being activated.
        self.lock = Lock()
        
        GObject.__init__(self)
        
        App().backend.connect(
            "unexpected-disconnect",
            self.on_unexpected_disconnect
        )
    
    def set_mode(self, mode, on_success=None, on_failure=None, auto_reenter=True):
        self.lock.acquire()
        
        thread = ModeActionThread(mode, old_mode=self.active_mode)
        thread.connect("success", self.on_set_mode_success, mode)
        thread.connect("failure", self.on_set_mode_failure, auto_reenter)
        
        if callable(on_success):
            # Remove the thread object from the event arguments.
            def success_handler(thread, *args):
                on_success(*args)
            
            thread.connect_async("success", success_handler)
        
        if callable(on_failure):
            # Remove the thread object from the event arguments.
            def failure_handler(thread, *args):
                on_failure(*args)
            
            thread.connect_async("failure", failure_handler)
        
        thread.start()
    
    def on_set_mode_success(self, thread, mode):
        self.active_mode = mode
        
        if mode:
            self.config.active = mode
            self.config[self.config.active] = mode.config
        
        self.lock.release()
    
    def on_set_mode_failure(self, thread, exception, auto_reenter):
        # The lock needs to be released before set_mode is called
        # again or they will wait for each other for evermore.
        self.lock.release()
        
        if auto_reenter:
            self.reenter_mode()
        else:
            self.config.active = None
    
    def reenter_mode(self, **kwargs):
        self.set_mode(self.active_mode, auto_reenter=False, **kwargs)
    
    def leave_mode(self, **kwargs):
        self.set_mode(None, **kwargs)
    
    def load(self, on_failure=None):
        if not callable(on_failure):
            on_failure = lambda exception: None
        
        if self.config.active:
            cls = get_mode_class(self.config.active)
            mode = cls(self.config[self.config.active])
            
            if mode.init_error:
                on_failure(Exception(mode.init_error))
            else:
                self.set_mode(mode, on_failure=on_failure)
        else:
            on_failure(Exception(""))
    
    def on_unexpected_disconnect(self, backend):
        mode = self.active_mode
        
        def on_failure(*args):
            log.error(mode.disconnect_message)
            
            self.emit("unexpected-mode-leaving", mode.disconnect_message)
        
        self.reenter_mode(on_failure=on_failure)
    
    @property
    def current_hella_config(self):
        if self.active_mode and hasattr(self.active_mode, "hella_config"):
            return self.active_mode.hella_config
    
    @property
    def is_changing_mode(self):
        return self.lock.locked()

class ModeActionThread(Thread):
    """Entering and leaving usage modes takes place within this util.Thread, 
    so that the UI isn't blocked.
    """
    
    def __init__(self, mode, old_mode=None):
        Thread.__init__(self)
        
        # An instance of Mode or None, if there's nothing but to leave the
        # current usage mode.
        self.mode = mode
        
        self.old_mode = old_mode
    
    def run(self):
        try:
            if self.old_mode:
                try:
                    self.old_mode.leave()
                except:
                    log.error(_("Could not leave current usage mode '%s'.") % self.old_mode)
                    raise
            
            if self.mode:
                try:
                    self.mode.enter()
                except:
                    log.error(_("Could not enter usage mode '%s'.") % self.mode)
                    raise
        
        except Exception, e:
            self.exception = e
        else:
            if self.old_mode:
                if not self.mode:
                    log.debug(_("Left usage mode '%s'.") % self.old_mode)
                elif self.old_mode.__class__ is self.mode.__class__:
                    log.debug(_("Reentered usage mode '%s'.") % self.mode)
                else:
                    log.debug(_("Changed usage mode from '%s' to '%s'.") % (self.old_mode, self.mode))
            elif self.mode:
                log.debug(_("Set usage mode to '%s'.") % self.mode)
        
        self.emit("completed")

def get_mode_class(name):
    """Turns a mode string into the corresponding modes.*.Mode class."""
    
    for mode_class in mode_list:
        if mode_class.get_name() == name:
            return mode_class
