"""
Maps Qt signals to Command objects.

The command factory connects to the global notifier and
creates commands objects as registered signals are
encountered.

The factory itself is undoable in that it responds to
the undo and redo signals and manages the undo/redo stack.

"""
import cola
from cola import signals
from cola.decorators import memoize


@memoize
def factory():
    """Return a static instance of the command factory."""
    return CommandFactory()


def SLOT(signal, *args, **opts):
    """
    Returns a callback that broadcasts a message over the notifier.

    """
    def broadcast(*local_args, **opts):
        cola.notifier().broadcast(signal, *args, **opts)
    return broadcast


class CommandFactory(object):
    def __init__(self):
        """Setup the undo/redo stacks and register for notifications."""
        self.undoable = True
        self.undostack = []
        self.redostack = []
        self.signal_to_command = {}
        self.callbacks = {}

        cola.notifier().connect(signals.undo, self.undo)
        cola.notifier().connect(signals.redo, self.redo)

        self.model = cola.model()
        self.model.add_observer(self)

    def add_command(self, signal, command):
        """Register a signal/command pair."""
        self.signal_to_command[signal] = command
        cola.notifier().connect(signal, self.cmdrunner(signal))

    def add_command_wrapper(self, cmd_wrapper):
        self.callbacks.update(cmd_wrapper.callbacks)

    def prompt_user(self, name, *args, **opts):
        try:
            return self.callbacks[name](*args, **opts)
        except KeyError:
            raise NotImplementedError('No callback for "%s' % name)

    def notify(self, *params):
        """
        Observe model changes.

        This captures model parameters and maps them to signals that
        are observed by the UIs.

        """
        actions = {
            'diff_text': SLOT(signals.diff_text, self.model.diff_text),
            'commitmsg': SLOT(signals.editor_text, self.model.commitmsg),
            'mode': SLOT(signals.mode, self.model.mode),
        }
        for param in params:
            action = actions.get(param, lambda: None)
            action()

    def clear(self):
        """Clear the undo and redo stacks."""
        self.undostack = []
        self.redostack = []

    def cmdrunner(self, signal):
        """Return a function to create and run a signal's command."""
        def run(*args, **opts):
            return self.do(signal, *args, **opts)
        return run

    def do(self, signal, *args, **opts):
        """Given a signal and arguments, run its corresponding command."""
        cmdclass = self.signal_to_command[signal]
        cmdobj = cmdclass(*args, **opts)
        # TODO we disable undo/redo for now; views just need to
        # inspect the stack and add menu entries when we enable it.
        if self.undoable and cmdobj.is_undoable():
            self.undostack.append(cmdobj)
        return cmdobj.do()

    def undo(self):
        """Undo the last command and add it to the redo stack."""
        if self.undostack:
            cmdobj = self.undostack.pop()
            cmdobj.undo()
            self.redostack.append(cmdobj)
        else:
            print 'warning: undo stack is empty, doing nothing'

    def redo(self):
        """Redo the last command and add it to the undo stack."""
        if self.redostack:
            cmdobj = self.redostack.pop()
            cmdobj.do()
            self.undo.append(cmd)
        else:
            print 'warning: redo stack is empty, doing nothing'

    def is_undoable(self):
        """Does the undo stack contain any commands?"""
        return bool(self.undostack)

    def is_redoable(self):
        """Does the redo stack contain any commands?"""
        return bool(self.redostack)
