#!/usr/bin/python2 -s

import hashlib
import os
import random
import select
import sys
import termios
import traceback
import uuid

from collections import deque
from datetime import datetime
from optparse import OptionParser
from threading import RLock, Thread
from time import time

from application import log
from application.notification import IObserver, NotificationCenter, NotificationData
from application.python.queue import EventQueue
from eventlib.twistedutil import join_reactor
from twisted.internet import reactor
from twisted.internet.error import ReactorNotRunning
from zope.interface import implements

from sipsimple.core import Engine, FromHeader, Publication, PublicationError, RouteHeader, SIPCoreError, SIPURI
from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.configuration import ConfigurationError, ConfigurationManager
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.lookup import DNSLookup
from sipsimple.payloads import BuilderError
from sipsimple.payloads.pidf import Contact, Device, DeviceInfo, DMNote, DeviceTimestamp, Person, PIDFNote, PersonTimestamp, PIDF, Service, ServiceTimestamp, Status
from sipsimple.payloads.rpid import Activities, ActivityRegistry, Mood, MoodRegistry, RPIDNote, TimeOffset
from sipsimple.storage import FileStorage
from sipsimple.threading import run_in_twisted_thread

from sipclient.configuration import config_directory
from sipclient.configuration.account import AccountExtension
from sipclient.configuration.settings import SIPSimpleSettingsExtension
from sipclient.log import Logger


class KeyBinding(object):
    def __init__(self, description, handler):
        self.description = description
        self.handler = handler


class Prompt(str):
    """Used to make a distinction between normal strings written to the console and prompts."""


class Menu(object):
    key_bindings = {}

    def __init__(self, interface):
        self.interface = interface

    def print_prompt(self):
        buf = ['Commands:']
        for key, binding in self.key_bindings.items():
            buf.append('  %s: %s' % (key, binding.description))
        self.interface.application.output.put('\n'+'\n'.join(buf)+'\n')

    def _exit(self):
        self.interface.exit_menu()

    def _exit_program(self):
        self.interface.application.stop()


# Mood manipulation menu
class MoodMenu(Menu):
    def _show_moods(self):
        person = self.interface.application.person
        buf = ['Moods:']
        if person.mood is not None:
            for m in list(person.mood):
                buf.append('  %s' % str(m))
        self.interface.application.output.put('\n'.join(buf))

    def _add_mood(self):
        person = self.interface.application.person
        buf = ['Possible moods:']
        values = list(MoodRegistry.names)
        values.sort()
        max_len = max(len(s) for s in values)+2
        format = ' %%02d) %%-%ds' % max_len
        num_line = 80/(max_len+5)
        i = 0
        text = ''
        for val in values:
            text += format % (i+1, val)
            i += 1
            if i % num_line == 0:
                buf.append(text)
                text = ''
        buf.append(text)
        self.interface.application.output.put('\n'.join(buf)+'\n')
        m = self.interface.getstring('Select mood to add (any non-number will return)')
        try:
            m = int(m)
            if m not in xrange(len(values)):
                raise ValueError
        except ValueError:
            self.interface.application.output.put('Invalid input')
        else:
            if person.mood is None:
                person.mood = Mood()
            person.mood.add(values[m-1])
            person.timestamp = PersonTimestamp(datetime.now())
            self.interface.application.publish()
            self.interface.application.output.put('Mood added')
        self.interface.show_top_level()

    def _del_mood(self):
        person = self.interface.application.person
        if person.mood is None:
            self.interface.application.output.put('There is no current mood set')
            self.print_prompt()
            return
        buf = ['Current moods:']
        values = list(person.mood)
        values.sort()
        max_len = max(len(s) for s in values)+2
        format = " %%02d) %%-%ds" % max_len
        num_line = 80/(max_len+5)
        i = 0
        text = ''
        for val in values:
            text += format % (i+1, val)
            i += 1
            if i % num_line == 0:
                buf.append(text)
                text = ''
        buf.append(text)
        self.interface.application.output.put('\n'.join(buf)+'\n')
        m = self.interface.getstring('Select mood to delete (any non-number will return)')
        try:
            m = int(m)
        except ValueError:
            self.interface.application.output.put('Invalid input')
        else:
            person.mood.remove(values[m-1])
            person.timestamp = PersonTimestamp(datetime.now())
            self.interface.application.publish()
            self.interface.application.output.put('Mood deleted')
        self.interface.show_top_level()

    def _clear_moods(self):
        person = self.interface.application.person
        if person.mood is None:
            self.interface.application.output.put('There is no current mood set')
            self.print_prompt()
            return
        person.mood = None
        person.timestamp = PersonTimestamp(datetime.now())
        self.interface.application.publish()
        self.interface.application.output.put('Mood information cleared')
        self.interface.show_top_level()

    def _set_note(self):
        person = self.interface.application.person
        if person.mood is not None and person.mood.notes:
            a_note = iter(person.mood.notes).next()
            self.interface.application.output.put('Current note: %s' % a_note)
        note = self.interface.getstring('Set note (press return to delete)')
        if note == '':
            person.mood.notes.clear()
            self.interface.application.output.put('Note removed')
        else:
            if person.mood is None:
                person.mood = Mood()
            person.mood.notes.add(RPIDNote(note, lang='en'))
            self.interface.application.output.put('Note set')
        person.timestamp = PersonTimestamp(datetime.now())
        self.interface.application.publish()
        self.interface.show_top_level()

    def _set_random(self):
        person = self.interface.application.person
        values = list(value for value in MoodRegistry.names if value != 'unknown')
        random.shuffle(values)

        if person.mood is None:
            person.mood = Mood()
        else:
            person.mood.clear()
        values = values[:3]
        for mood in values:
            person.mood.add(mood)
        person.timestamp = PersonTimestamp(datetime.now())
        self.interface.application.publish()
        self.interface.application.output.put('You are now ' + ', '.join(values))
        self.interface.show_top_level()

    key_bindings = {'s': KeyBinding(description='show current moods', handler=_show_moods),
                    'a': KeyBinding(description='add a mood', handler=_add_mood),
                    'd': KeyBinding(description='delete a mood', handler=_del_mood),
                    'c': KeyBinding(description='clear all mood data', handler=_clear_moods),
                    'n': KeyBinding(description='set mood note', handler=_set_note),
                    'r': KeyBinding(description='set random mood', handler=_set_random),
                    'x': KeyBinding(description='exit to upper level menu', handler=Menu._exit),
                    'q': KeyBinding(description='quit program', handler=Menu._exit_program)}


# Activities manipulation menu
class ActivitiesMenu(Menu):
    def _show_activity(self):
        person = self.interface.application.person
        buf = ["Activity:"]
        if person.activities is not None:
            for a in list(person.activities):
                buf.append("  %s" % str(a))
        self.interface.application.output.put('\n'.join(buf))

    def _set_activity(self):
        person = self.interface.application.person
        buf = ["Possible activities:"]
        values = list(ActivityRegistry.names)
        values.sort()
        max_len = max(len(s) for s in values)+2
        format = " %%02d) %%-%ds" % max_len
        num_line = 80/(max_len+5)
        i = 0
        text = ''
        for val in values:
            text += format % (i+1, val)
            i += 1
            if i % num_line == 0:
                buf.append(text)
                text = ''
        self.interface.application.output.put('\n'.join(buf)+'\n')
        a = self.interface.getstring('Select activity to add (any non-number will return)')
        try:
            a = int(a)
            if a-1 not in xrange(len(values)):
                raise ValueError
        except ValueError:
            self.interface.application.output.put('Invalid input')
        else:
            if person.activities is None:
                person.activities = Activities()
            else:
                person.activities.clear()
            person.activities.add(values[a-1])
            person.timestamp = PersonTimestamp(datetime.now())
            self.interface.application.publish()
            self.interface.application.output.put('Activity set')
        self.interface.show_top_level()

    def _del_activity(self):
        person = self.interface.application.person
        if person.activities is None or len(person.activities.values) == 0:
            self.interface.application.output.put('There is no current activity set')
            return
        person.activities.clear()
        person.activities.add('unknown')
        person.timestamp = PersonTimestamp(datetime.now())
        self.interface.application.publish()
        self.interface.application.output.put('Activity deleted')
        self.interface.show_top_level()

    def _clear_activity(self):
        person = self.interface.application.person
        if person.activities is None:
            self.interface.application.output.put('There is no current activity set')
            return
        person.activities = None
        person.timestamp = PersonTimestamp(datetime.now())
        self.interface.application.publish()
        self.interface.application.output.put('Activities information cleared')
        self.interface.show_top_level()

    def _set_note(self):
        person = self.interface.application.person
        if person.activities is not None and person.activities.notes:
            a_note = iter(person.activities.notes).next()
            self.interface.application.output.put('Current note: %s' % a_note)
        note = self.interface.getstring('Set note (press return to delete)')
        if note == '':
            person.activities.notes.clear()
            self.interface.application.output.put('Note deleted')
        else:
            if person.activities is None:
                person.activities = Activities()
                person.activities.add('unknown')
            person.activities.notes.add(RPIDNote(note, lang='en'))
            self.interface.application.output.put('Note set')
        person.timestamp = PersonTimestamp(datetime.now())
        self.interface.application.publish()
        self.interface.show_top_level()

    def _set_random(self):
        person = self.interface.application.person
        values = list(value for value in ActivityRegistry.names if value != 'unknown')
        activity = random.choice(values)

        if person.activities is None:
            person.activities = Activities()
        else:
            person.activities.clear()
        person.activities.add(activity)
        person.timestamp = PersonTimestamp(datetime.now())
        self.interface.application.publish()
        self.interface.application.output.put('You are now %s' % activity)
        self.interface.show_top_level()

    key_bindings = {'s': KeyBinding(description='show current activity', handler=_show_activity),
                    'a': KeyBinding(description='set activity', handler=_set_activity),
                    'd': KeyBinding(description='delete activity', handler=_del_activity),
                    'c': KeyBinding(description='clear all acitivity data', handler=_clear_activity),
                    'n': KeyBinding(description='set activity note', handler=_set_note),
                    'r': KeyBinding(description='set random activity', handler=_set_random),
                    'x': KeyBinding(description='exit to upper level menu', handler=Menu._exit),
                    'q': KeyBinding(description='quit program', handler=Menu._exit_program)}


# Extended status manipulation menu
class ExtendedStatusMenu(Menu):
    def _show_extended_status(self):
        service = self.interface.application.service
        buf = ["Extended status:"]
        if service.status.extended is not None:
            buf.append("  %s" % str(service.status.extended))
        self.interface.application.output.put('\n'.join(buf))

    def _set_extended_status(self):
        service = self.interface.application.service
        buf = ["Possible statuses:"]
        values = ['available', 'away', 'extended-away', 'busy', 'offline']
        max_len = max(len(s) for s in values)+2
        format = " %%02d) %%-%ds" % max_len
        num_line = 80/(max_len+5)
        i = 0
        text = ''
        for val in values:
            text += format % (i+1, val)
            i += 1
            if i % num_line == 0:
                buf.append(text)
                text = ''
        buf.append(text)
        self.interface.application.output.put('\n'.join(buf)+'\n')
        a = self.interface.getstring('Select status (any non-number will return)')
        try:
            a = int(a)
            if a-1 not in xrange(len(values)):
                raise ValueError
        except ValueError:
            self.interface.application.output.put('Invalid input')
        else:
            status = values[a-1]
            if status == 'offline':
                service.status.basic = 'closed'
            else:
                service.status.basic = 'open'
            service.status.extended = status
            self.interface.application.publish()
            self.interface.application.output.put('Extended atatus set')
        self.interface.show_top_level()

    def _del_extended_status(self):
        self.interface.application.service.status.extended = None
        self.interface.application.publish()
        self.interface.application.output.put('Activity deleted')
        self.interface.show_top_level()

    key_bindings = {'s': KeyBinding(description='show current extended status', handler=_show_extended_status),
                    'e': KeyBinding(description='set extended status', handler=_set_extended_status),
                    'd': KeyBinding(description='delete extended status', handler=_del_extended_status),
                    'x': KeyBinding(description='exit to upper level menu', handler=Menu._exit),
                    'q': KeyBinding(description='quit program', handler=Menu._exit_program)}


class TopLevelMenu(Menu):
    def _show_pidf(self):
        try:
            pidf = self.interface.application.pidf.toxml(pretty_print=True)
        except BuilderError, e:
            print "PIDF as currently defined is invalid: %s" % str(e)
        except:
            traceback.print_exc()
        else:
            self.interface.application.output.put(pidf)
        self.print_prompt()

    def _set_mood_info(self):
        mood_menu = MoodMenu(self.interface)
        self.interface.add_menu(mood_menu)

    def _set_activity_info(self):
        activities_menu = ActivitiesMenu(self.interface)
        self.interface.add_menu(activities_menu)

    def _toggle_basic(self):
        service = self.interface.application.service
        if service.status.basic == 'open':
            service.status.basic = 'closed'
        else:
            service.status.basic = 'open'
        service.timestamp = ServiceTimestamp(datetime.now())
        self.interface.application.output.put("Your basic status is now '%s'" % service.status.basic)
        self.interface.application.publish()
        self.print_prompt()

    def _set_extended_status(self):
        extended_status_menu = ExtendedStatusMenu(self.interface)
        self.interface.add_menu(extended_status_menu)

    def _set_note(self):
        service = self.interface.application.service
        if service.notes:
            a_note = iter(service.notes).next()
            self.interface.application.output.put('Current note: %s' % a_note)
        note = self.interface.getstring('Set note (press return to delete)')
        if note == '':
            if service.notes:
                service.notes.clear()
            self.interface.application.output.put('Note removed')
        else:
            service.notes.add(PIDFNote(note, lang='en'))
            self.interface.application.output.put('Note added')
        self.interface.application.publish()
        self.print_prompt()

    key_bindings = {'s': KeyBinding(description='show PIDF', handler=_show_pidf),
                    'm': KeyBinding(description='set mood information', handler=_set_mood_info),
                    'a': KeyBinding(description='set activities information', handler=_set_activity_info),
                    'b': KeyBinding(description='toggle basic status', handler=_toggle_basic),
                    'e': KeyBinding(description='set extended status', handler=_set_extended_status),
                    'n': KeyBinding(description='set note', handler=_set_note),
                    'q': KeyBinding(description='quit program', handler=Menu._exit_program)}


class UserInterface(Thread):
    def __init__(self, application):
        Thread.__init__(self)
        self.application = application
        self.daemon = True
        self.menu_stack = deque([TopLevelMenu(self)])
        self._old_terminal_settings = None

    def run(self):
        self.menu_stack[-1].print_prompt()
        notification_center = NotificationCenter()
        while True:
            for char in self._getchars():
                menu = self.menu_stack[-1]
                if char == '\x04':
                    self.application.stop()
                    return
                elif char in menu.key_bindings:
                    menu.key_bindings[char].handler(menu)
                else:
                    notification_center.post_notification('SAInputWasReceived', sender=self, data=NotificationData(input=char))

    def stop(self):
        self._termios_restore()

    def add_menu(self, menu):
        self.menu_stack.append(menu)
        menu.print_prompt()

    def show_top_level(self):
        main = self.menu_stack[0]
        self.menu_stack.clear()
        self.menu_stack.append(main)
        main.print_prompt()

    def exit_menu(self):
        if len(self.menu_stack) > 1:
            self.menu_stack.pop()
        self.menu_stack[-1].print_prompt()

    def getstring(self, prompt='selection'):
        self.application.output.put(Prompt(prompt))
        return sys.stdin.readline().strip()

    def _termios_restore(self):
        if self._old_terminal_settings is not None:
            termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, self._old_terminal_settings)

    def _getchars(self):
        fd = sys.stdin.fileno()
        if os.isatty(fd):
            self._old_terminal_settings = termios.tcgetattr(fd)
            new = termios.tcgetattr(fd)
            new[3] = new[3] & ~termios.ICANON & ~termios.ECHO
            new[6][termios.VMIN] = '\000'
            try:
                termios.tcsetattr(fd, termios.TCSADRAIN, new)
                if select.select([fd], [], [], None)[0]:
                    return sys.stdin.read(4192)
            finally:
                self._termios_restore()
        else:
            return os.read(fd, 4192)


class PublicationApplication(object):
    implements(IObserver)

    def __init__(self, account_name, trace_sip, trace_pjsip, trace_notifications):
        self.account_name = account_name
        self.interface = UserInterface(self)
        self.output = EventQueue(self._output_handler)
        self.logger = Logger(sip_to_stdout=trace_sip, pjsip_to_stdout=trace_pjsip, notifications_to_stdout=trace_notifications)
        self.lookup = DNSLookup()
        self.publication_lock = RLock()
        self.success = False
        self.account = None
        self.publication = None
        self.pidf = None
        self.service = None
        self.person = None
        self.device = None
        self.stopping = False
        self.publishing = False

        self._publication_routes = None
        self._publication_timeout = 0.0
        self._publication_wait = 0.5

        account_manager = AccountManager()
        engine = Engine()
        notification_center = NotificationCenter()
        notification_center.add_observer(self, sender=account_manager)
        notification_center.add_observer(self, sender=engine)
        notification_center.add_observer(self, sender=self.interface)
        notification_center.add_observer(self, sender=self.lookup)

        log.level.current = log.level.WARNING

    def run(self):
        account_manager = AccountManager()
        configuration = ConfigurationManager()
        engine = Engine()
        notification_center = NotificationCenter()

        # start output thread
        self.output.start()

        # startup configuration
        Account.register_extension(AccountExtension)
        BonjourAccount.register_extension(AccountExtension)
        SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
        SIPApplication.storage = FileStorage(config_directory)
        try:
            configuration.start()
        except ConfigurationError, e:
            raise RuntimeError("Failed to load sipclient's configuration: %s\nIf an old configuration file is in place, delete it or move it and recreate the configuration using the sip_settings script." % str(e))
        account_manager.load()
        if self.account_name is None:
            self.account = account_manager.default_account
        else:
            possible_accounts = [account for account in account_manager.iter_accounts() if self.account_name in account.id and account.enabled]
            if len(possible_accounts) > 1:
                raise RuntimeError("More than one account exists which matches %s: %s" % (self.account_name, ", ".join(sorted(account.id for account in possible_accounts))))
            if len(possible_accounts) == 0:
                raise RuntimeError("No enabled account that matches %s was found. Available and enabled accounts: %s" % (self.account_name, ", ".join(sorted(account.id for account in account_manager.get_accounts() if account.enabled))))
            self.account = possible_accounts[0]
        if self.account is None:
            raise RuntimeError("Unknown account %s. Available enabled accounts: %s" % (self.account_name, ', '.join(sorted(account.id for account in account_manager.iter_accounts() if account.enabled))))
        elif self.account == BonjourAccount():
            raise RuntimeError("Cannot use bonjour account to publish presence information")
        elif not self.account.presence.enabled:
            raise RuntimeError("Presence is not enabled for account %s" % self.account.id)
        for account in account_manager.iter_accounts():
            if account == self.account:
                account.sip.register = False
            else:
                account.enabled = False
        self.output.put('Using account %s' % self.account.id)
        settings = SIPSimpleSettings()

        # start logging
        self.logger.start()

        # start the engine
        engine.start(
            udp_port=settings.sip.udp_port if "udp" in settings.sip.transport_list else None,
            tcp_port=settings.sip.tcp_port if "tcp" in settings.sip.transport_list else None,
            tls_port=settings.sip.tls_port if "tls" in settings.sip.transport_list else None,
            tls_verify_server=self.account.tls.verify_server,
            tls_ca_file=os.path.expanduser(settings.tls.ca_list) if settings.tls.ca_list else None,
            tls_cert_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
            tls_privkey_file=os.path.expanduser(self.account.tls.certificate) if self.account.tls.certificate else None,
            tls_timeout=settings.tls.timeout,
            user_agent=settings.user_agent,
            sample_rate=settings.audio.sample_rate,
            rtp_port_range=(settings.rtp.port_range.start, settings.rtp.port_range.end),
            trace_sip=settings.logs.trace_sip or self.logger.sip_to_stdout,
            log_level=settings.logs.pjsip_level if (settings.logs.trace_pjsip or self.logger.pjsip_to_stdout) else 0
        )

        # initialize pidf
        self.pidf = PIDF(entity=self.account.id) # entity will be determined when account is selected

        # initialize top level elements
        device_id = settings.instance_id
        self.service = Service("ID-"+str(uuid.UUID(hex=settings.instance_id)))
        self.service.status = Status(basic='open')
        self.service.status.extended = 'available'
        contact = "sip:%s;gr=%s" % (self.account.id, device_id)
        self.service.contact = Contact(contact)
        self.service.contact.priority = 0
        self.service.device_id = device_id
        self.service.device_info = DeviceInfo(device_id)
        self.service.timestamp = ServiceTimestamp(datetime.now())
        self.pidf.add(self.service)

        self.person = Person("ID-"+hashlib.md5(self.account.id).hexdigest())
        self.person.time_offset = TimeOffset()
        self.person.timestamp = PersonTimestamp(datetime.now())
        self.pidf.add(self.person)

        self.device = Device(''.join(chr(random.randint(97, 122)) for i in xrange(8)))
        self.device.device_id = device_id
        self.device.notes.add(DMNote('Powered by %s' % engine.user_agent, lang='en'))
        self.device.timestamp = DeviceTimestamp(datetime.now())
        self.pidf.add(self.device)

        # start the interface thread
        self.interface.start()

        # initialize publication object
        self.publication = Publication(FromHeader(self.account.uri, self.account.display_name), "presence", "application/pidf+xml",
                                       credentials=self.account.credentials, duration=self.account.sip.publish_interval)
        notification_center.add_observer(self, sender=self.publication)

        reactor.callLater(0, self.publish)

        # start twisted
        try:
            reactor.run()
        finally:
            self.interface.stop()

        # stop the output
        self.output.stop()
        self.output.join()

        self.logger.stop()

        return 0 if self.success else 1

    def stop(self):
        self.stopping = True
        if self.publication is not None:
            self.service.status.basic = 'closed'
            self.service.status.extended = 'offline'
            self.publish()
        else:
            engine = Engine()
            engine.stop()

    def print_help(self):
        message  = 'Available control keys:\n'
        message += '  t: toggle SIP trace on the console\n'
        message += '  j: toggle PJSIP trace on the console\n'
        message += '  n: toggle notifications trace on the console\n'
        message += '  Ctrl-d: quit the program\n'
        message += '  ?: display this help message\n'
        self.output.put('\n'+message)

    def publish(self):
        with self.publication_lock:
            if self.publishing:
                return

            settings = SIPSimpleSettings()

            self._publication_timeout = time() + 30

            if self.account.sip.outbound_proxy is not None:
                uri = SIPURI(host=self.account.sip.outbound_proxy.host, port=self.account.sip.outbound_proxy.port, parameters={'transport': self.account.sip.outbound_proxy.transport})
            else:
                uri = SIPURI(host=self.account.id.domain)
            self.lookup.lookup_sip_proxy(uri, settings.sip.transport_list)
            self.publishing = True

    def unpublish(self):
        try:
            self.publication.end(timeout=1)
            return
        except PublicationError:
            pass
        engine = Engine()
        engine.stop()

    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, None)
        if handler is not None:
            handler(notification)

    def _NH_SIPPublicationDidSucceed(self, notification):
        with self.publication_lock:
            self._publication_routes = None
            self._publication_wait = 0.5
            self.success = True
            self.publishing = False
            if self.stopping:
                self.unpublish()
                return

    def _NH_SIPPublicationDidFail(self, notification):
        with self.publication_lock:
            self.success = False
            self.output.put('Publishing failed: %d %s' % (notification.data.code, notification.data.reason))
            if notification.data.code in (401, 403, 407):
                self.publishing = False
                self.stop()
            else:
                if self.stopping:
                    self.unpublish()
                    return
                if not self._publication_routes or time() > self._publication_timeout:
                    self._publication_wait = min(self._publication_wait*2, 30)
                    timeout = random.uniform(self._publication_wait, 2*self._publication_wait)
                    reactor.callFromThread(reactor.callLater, timeout, self.publish)
                    self.publishing = False
                else:
                    route = self._publication_routes.popleft()
                    self._do_publish(route)

    def _NH_SIPPublicationWillExpire(self, notification):
        # For now, just re-publish the whole document instead of sending a refresh
        self.publish()

    def _NH_SIPPublicationDidNotEnd(self, notification):
        self.success = False
        engine = Engine()
        engine.stop()

    def _NH_SIPPublicationDidEnd(self, notification):
        if notification.data.expired:
            self.output.put('Publication expired')
        else:
            self.output.put('Unpublished')
        if self.stopping:
            self.success = True
            engine = Engine()
            engine.stop()
        else:
            self.publish()

    def _NH_DNSLookupDidSucceed(self, notification):
        with self.publication_lock:
            self._publication_routes = deque(notification.data.result)
            route = self._publication_routes.popleft()
            self._do_publish(route)

    def _NH_DNSLookupDidFail(self, notification):
        with self.publication_lock:
            self.output.put('DNS lookup failed: %s' % notification.data.error)
            timeout = random.uniform(1.0, 2.0)
            reactor.callLater(timeout, self.publish)
            self.publishing = False

    def _NH_SAInputWasReceived(self, notification):
        engine = Engine()
        settings = SIPSimpleSettings()
        key = notification.data.input
        if key == 't':
            self.logger.sip_to_stdout = not self.logger.sip_to_stdout
            engine.trace_sip = self.logger.sip_to_stdout or settings.logs.trace_sip
            self.output.put('SIP tracing to console is now %s.' % ('activated' if self.logger.sip_to_stdout else 'deactivated'))
        elif key == 'j':
            self.logger.pjsip_to_stdout = not self.logger.pjsip_to_stdout
            engine.log_level = settings.logs.pjsip_level if (self.logger.pjsip_to_stdout or settings.logs.trace_pjsip) else 0
            self.output.put('PJSIP tracing to console is now %s.' % ('activated' if self.logger.pjsip_to_stdout else 'deactivated'))
        elif key == 'n':
            self.logger.notifications_to_stdout = not self.logger.notifications_to_stdout
            self.output.put('Notification tracing to console is now %s.' % ('activated' if self.logger.notifications_to_stdout else 'deactivated'))
        elif key == '?':
            self.print_help()

    @run_in_twisted_thread
    def _NH_SIPEngineDidEnd(self, notification):
        self._stop_reactor()

    @run_in_twisted_thread
    def _NH_SIPEngineDidFail(self, notification):
        self.output.put('Engine failed.')
        self._stop_reactor()

    def _NH_SIPEngineGotException(self, notification):
        self.output.put('An exception occured within the SIP core:\n'+notification.data.traceback)

    def _stop_reactor(self):
        try:
            reactor.stop()
        except ReactorNotRunning:
            pass

    def _do_publish(self, route):
        try:
            route_header = RouteHeader(route.uri)
            self.publication.publish(self.pidf.toxml(), route_header, timeout=5)
        except BuilderError, e:
            self.output.put("PIDF as currently defined is invalid: %s" % str(e))
            self.publishing = False
        except:
            traceback.print_exc()
            self.publishing = False

    def _output_handler(self, event):
        if isinstance(event, Prompt):
            sys.stdout.write(event+'> ')
            sys.stdout.flush()
        else:
            sys.stdout.write(event+'\n')


if __name__ == "__main__":
    description = "This script publishes the presence event package to a SIP Presence Agent for the given SIP account, the presence information can be changed using a menu-driven interface."
    usage = "%prog [options]"
    parser = OptionParser(usage=usage, description=description)
    parser.print_usage = parser.print_help
    parser.add_option("-a", "--account-name", type="string", dest="account_name", help="The name of the account to use.")
    parser.add_option("-s", "--trace-sip", action="store_true", dest="trace_sip", default=False, help="Dump the raw contents of incoming and outgoing SIP messages (disabled by default).")
    parser.add_option("-j", "--trace-pjsip", action="store_true", dest="trace_pjsip", default=False, help="Print PJSIP logging output (disabled by default).")
    parser.add_option("-n", "--trace-notifications", action="store_true", dest="trace_notifications", default=False, help="Print all notifications (disabled by default).")
    options, args = parser.parse_args()

    try:
        application = PublicationApplication(options.account_name, options.trace_sip, options.trace_pjsip, options.trace_notifications)
        return_code = application.run()
    except RuntimeError, e:
        print "Error: %s" % str(e)
        sys.exit(1)
    except SIPCoreError, e:
        print "Error: %s" % str(e)
        sys.exit(1)
    else:
        sys.exit(return_code)
