'''
Provide an interface class for handling pubsub notification messages,
and an example class (though very useful in practice) showing how to
use it.

Notification messages are generated by pubsub

- if a handler has been configured via pubsubconf.setNotificationHandler()
- when pubsub does certain tasks, such as when a listener subscribes to
  or unsubscribes from a topic

Derive from this class to handle notification events from
various parts of pubsub. E.g. when a listener subscribes,
unsubscribes, or dies, a notification handler, if you
specified one via pubsubconf.setNotificationHandler(), is given the
relevant information.
'''


from .. import pubsubconf


class INotificationHandler:
    '''
    Defines the interface expected by pubsub for notification
    messages. Any instance that supports the same methods, or
    derives from this class, will work as a notification handler
    for pubsub events.
    '''

    def notifySubscribe(self, subdLisnr, topicObj, didit):
        pass
    def notifyUnsubscribe(self, subdLisnr, topicObj):
        pass
    def notifySend(self, stage, topicObj):
        pass

    def notifyNewTopic(self, topicObj, description, required, argsDocs):
        pass
    def notifyDelTopic(self, topicName):
        pass
    def notifyDeadListener(self, topicObj, listener):
        pass


class NotificationToStdout:
    '''
    Print a message to stdout when a notification is received.
    '''

    def notifySubscribe(self, listener, topicObj, didit):
        if listener is None:
            msg = '%sSubscription of "%s" to topic "%s" redundant'
        else:
            msg = '%sSubscribed listener "%s" to topic "%s"'
        print msg % (self.prefix, listener, topicObj.getName())

    def notifyUnsubscribe(self, subdLisnr, topicObj):
        if didit:
            msg = '%sUnsubscribed listener "%s" from topic "%s"'
        else:
            msg = '%sUnsubscription of "%s" from topic "%s" redundant'
        print msg % (self.prefix, listener, topicObj.getName())

    def notifySend(self, stage, topicObj):
        if stage == 'pre':
            print '%sSending message of topic "%s"' % (self.prefix, topicObj.getName())

    def notifyNewTopic(self, topicObj, description, required, argsDocs):
        print '%sNew topic "%s" created' % (self.prefix, topicObj.getName())
    def notifyDelTopic(self, topicName):
        print '%sTopic "%s" destroyed' % (self.prefix, topicName)
    def notifyDeadListener(self, topicObj, listener):
        print '%sListener "%s" of Topic "%s" has died' % (self.prefix, listener, topic.getName())


class NotifyByPubsubMessage(INotificationHandler):
    '''
    Handle pubsub notification messages by generating
    messages of a 'pubsub.' subtopic. Also provides
    an example of how to create a notification handler.

    Use it by calling::

        import pubsub.utils
        notifHandler = pubsub.utils.useNotifyByPubsubMessage()
        ...

        from pubsub import pub
        notifHandler.createNotificationTopics()
        ...

        pub.setNotification(...)

    E.g. whenever a listener is unsubscribed, a 'pubsub.unsubscribe'
    message is generated. If you have subscribed a listener of
    this topic, your listener will be notified of what listener
    unsubscribed from what topic.
    '''

    topicRoot = 'pubsub'
    topics = dict(
        send         = '%s.sendMessage'  % topicRoot,
        subscribe    = '%s.subscribe'    % topicRoot,
        unsubscribe  = '%s.unsubscribe'  % topicRoot,
        newTopic     = '%s.newTopic'     % topicRoot,
        delTopic     = '%s.delTopic'     % topicRoot,
        deadListener = '%s.deadListener' % topicRoot)

    def createNotificationTopics(self, topicMgr=None):
        '''Create the notification topics. The root of the topics created
        is self.topicRoot. Note that if topicMgr not given, then it will
        be obtained by importing pubsub. Since it is important that your
        code control when the first import occurs, this method raises a
        RuntimeError if pubsub hasn't already been imported. '''
        if topicMgr is None:
            if not pubsubconf.isPackageImported():
                raise RuntimeError('your code must import pubsub first')
            from pubsub import pub
            topicMgr = pub.getDefaultTopicMgr()

        # see if the special topics have already been defined
        try:
            topicMgr.getTopic(self.topicRoot)

        except RuntimeError:
            # no, so create them
            topicMgr.newTopic(
                _name = self.topicRoot,
                _desc = 'root of all pubsub-specific topics')
            self._pubTopic = topicMgr.getTopic(self.topicRoot)
            assert hasattr(self, '_pubTopic')

            _createTopics(self.topics, topicMgr)

    def notifySubscribe(self, subdLisnr, topicObj, didit):
        if hasattr(self, '_pubTopic'):
            pubTopic = self._pubTopic.subscribe
            if topicObj is not pubTopic:
                kwargs = dict(listener=subdLisnr, topic=topicObj, didit=didit)
                pubTopic.publish(kwargs)

    def notifyUnsubscribe(self, subdLisnr, topicObj):
        if hasattr(self, '_pubTopic'):
            pubTopic = self._pubTopic.unsubscribe
            if topicObj is not pubTopic:
                kwargs = dict(
                    topic       = topicObj,
                    listenerRaw = subdLisnr.getCallable(),
                    listener    = subdLisnr)
                pubTopic.publish(kwargs)

    def notifySend(self, stage, topicObj):
        '''Stage must be 'pre' or 'post'.'''
        if hasattr(self, '_pubTopic'):
            sendMsgTopic = self._pubTopic.sendMessage
            if stage == 'pre' and (topicObj is sendMsgTopic):
                    msg = 'Not allowed to send messages of topic %s' % topicObj.getName()
                    raise ValueError(msg)

            sendMsgTopic.publish( dict(topic=topicObj, stage=stage) )

    def notifyNewTopic(self, topicObj, desc, required, argsDocs):
        if hasattr(self, '_pubTopic'):
            pubTopic = self._pubTopic.newTopic
            pubTopic.publish(
                dict(topic=topicObj, description=desc, required=required, args=argsDocs))

    def notifyDelTopic(self, topicName):
        if hasattr(self, '_pubTopic'):
            pubTopic = self._pubTopic.delTopic
            pubTopic.publish( dict(name=topicName) )

    def notifyDeadListener(self, topicObj, listener):
        if hasattr(self, '_pubTopic'):
            pubTopic = self._pubTopic.deadListener
            kwargs = dict(topic=topicObj, listener=listener)
            pubTopic.publish(kwargs)


def _createTopics(topicMap, topicMgr):
    '''
    Create notification topics. These are used when
    some of the notification flags have been set to True (see
    pub.setNotification(). The topicMap is a dict where key is
    the notification type, and value is the topic name to create.
    Notification type is a string in ('send', 'subscribe',
    'unsubscribe', 'newTopic', 'delTopic', 'deadListener'.
    '''
    topicMgr.newTopic(
        _name = topicMap['subscribe'],
        _desc = 'whenever a listener is subscribed to a topic',
        topic = 'topic that listener has subscribed to',
        listener = 'instance of pub.Listener containing listener',
        didit = 'false if listener already subscribed, true otherwise')

    topicMgr.newTopic(
        _name = topicMap['unsubscribe'],
        _desc = 'whenever a listener is unsubscribed from a topic',
        topic = 'instance of Topic that listener has been unsubscribed from',
        listener = 'instance of pub.Listener unsubscribed; None if listener not found',
        listenerRaw = 'listener unsubscribed')

    topicMgr.newTopic(
        _name = topicMap['send'],
        _desc = 'sent at beginning and end of sendMessage()',
        topic = 'instance of topic for message being sent',
        stage = 'stage of send operation: "pre" or "post"')

    topicMgr.newTopic(
        _name = topicMap['newTopic'],
        _desc = 'whenever a new topic is defined',
        topic       = 'instance of Topic created',
        description = 'description of topic (use)',
        args        = 'the argument names/descriptions for arguments that listeners must accept',
        required    = 'which args are required (all others are optional)')

    topicMgr.newTopic(
        _name = topicMap['delTopic'],
        _desc = 'whenever a topic is deleted',
        name  = 'full name of the Topic instance that was destroyed')

    topicMgr.newTopic(
        _name = topicMap['deadListener'],
        _desc = 'whenever a listener dies without having unsubscribed',
        topic = 'instance of Topic that listener was subscribed to',
        listener = 'instance of pub.Listener containing dead listener')


class PubsubTopicMsgLogger:
    '''
    Default logger for 'pubsub.*' topics. These topics are used
    automatically in various pubsub calls; e.g. when a new topic is
    created, a 'pubsub.newTopic' message is generated. Note that
    such messages are generated only if pub.setNotification() was called.

    Each method of PubsubTopicMsgLogger can be given
    to pub.subscribe() as a listener. A method's name indicates
    which 'pubsub' subtopic it can be listening for. E.g. you would
    subscribe the PubsubTopicMsgLogger.subscribe method to listen for
    'pubsub.subscribe' messages:

        from pubsub import pub
        from pubsubutils import PubsubTopicMsgLogger
        logger = PubsubTopicMsgLogger(pub)
        pub.subscribe('pubsub.subscribe', logger.subscribe)

    Any number of instances can be created. By default, the __init__
    will subscribe instance to all 'pubsub' subtopics. Class can also
    be derived to override default behavior.
    '''

    prefix = 'PUBSUB: '

    def __init__(self, publisher=None):
        '''If publisher is not None, then all self's methods
        are subscribed to the 'pubsub.*' topics by using
        publisher.subscribe(). '''
        if publisher is not None:
            pub = publisher
            pub.subscribe(self.psOnSubscribe,   'pubsub.subscribe')
            pub.subscribe(self.psOnUnsubscribe, 'pubsub.unsubscribe')
            pub.subscribe(self.psOnNewTopic,    'pubsub.newTopic')
            pub.subscribe(self.psOnDelTopic,    'pubsub.delTopic')
            pub.subscribe(self.psOnSendMessage, 'pubsub.sendMessage')
            pub.subscribe(self.psOnDeadListener,'pubsub.deadListener')
            pub.subscribe(self.psOnUncaughtExcInListener, 'uncaughtExcInListener')

    def psOnSubscribe(self, topic=None, listener=None, didit=None):
        '''Give this to pub.subscribe() as listener of
        'pubsub.subscribe' messages. '''
        if listener is None:
            msg = '%sSubscription of "%s" to topic "%s" redundant'
        else:
            msg = '%sSubscribed listener "%s" to topic "%s"'
        print msg % (self.prefix, listener, topic.getName())

    def psOnUnsubscribe(self, topic=None, listener=None, listenerRaw=None):
        '''Give this to pub.subscribe() as listener of
        'pubsub.unsubscribe' messages. '''
        if didit:
            msg = '%sUnsubscribed listener "%s" from topic "%s"'
        else:
            msg = '%sUnsubscription of "%s" from topic "%s" redundant'
        print msg % (self.prefix, listener, topic.getName())

    def psOnNewTopic(self, topic=None, description=None, args=None, required=None):
        '''Give this to pub.subscribe() as listener of
        'pubsub.newTopic' messages. '''
        print '%sNew topic "%s" created' % (self.prefix, topic.getName())

    def psOnDelTopic(self, name=None):
        '''Give this to pub.subscribe() as listener of
        'pubsub.delTopic' messages. '''
        print '%sTopic "%s" destroyed' % (self.prefix, name)

    def psOnSendMessage(self, topic=None, stage=None):
        '''Give this to pub.subscribe() as listener of
        'pubsub.sendMessage' messages. '''
        if stage == 'pre':
            print '%sSending message of topic "%s"' % (self.prefix, topic.getName())

    def psOnDeadListener(self, topic=None, listener=None):
        print '%sListener "%s" of Topic "%s" has died' % (self.prefix, listener, topic.getName())

    def psOnUncaughtExcInListener(self, listenerStr=None, excTraceback=None):
        '''Give this to pub.subscribe() as listener of
        'uncaughtExcInListener' messages. '''
        print '%sListener "%s" raised an exception. Traceback:' \
            % (self.prefix, listenerStr)
        print excTraceback.getFormattedString()


def useNotifyByPubsubMessage():
    '''Install an instance of NotifyByPubsubMessage as notification
    handler. Return the instance. You must call its init() once
    pubsub imported (since instance defines some pubsub topics):

    import pubsub.utils
    notifHandler = pubsub.utils.useNotifyByPubsubMessage()

    ...

    from pubsub import pub
    notifHandler.createNotificationTopics()
    '''
    import pubsub.utils
    notifHandler = pubsub.utils.NotifyByPubsubMessage()
    import pubsubconf
    pubsubconf.setNotificationHandler(notifHandler)

    return notifHandler


def useDefaultLoggingNotification(all=True, **kwargs):
    '''The kwargs are the same as for PubsubTopicMsgLogger constructor, except
    that 'all' defaults to True instead of None (ie assumes that if you
    don't specify what notifications you want, that you want them all).'''
    import pubsub.utils
    notifHandler = pubsub.utils.NotifyByPubsubMessage()
    import pubsubconf
    pubsubconf.setNotificationHandler(notifHandler)

    from pubsub import pub
    notifHandler.createNotificationTopics()
    pub.setNotification(all=all, **kwargs)

    return PubsubTopicMsgLogger(pub)


