"""
Low level *Skype for Linux* interface implemented using *dbus-python* package.

This module handles the options that you can pass to `Skype.__init__`
for Linux machines when the transport is set to *DBus*. See below.

- ``RunMainLoop`` (bool) - If set to False, Skype4Py won't start the GLib main
  loop. Otherwise it is started in a separate thread. The loop must be running for
  Skype4Py events to work properly. Set this option to False if you plan to run the
  loop yourself or if, for example, your GUI framework does it for you.

:requires: Skype for Linux 2.0 (beta) or newer.
"""
__docformat__ = 'restructuredtext en'


import sys
import threading
import time
import warnings
import logging

from Skype4Py.api import Command, SkypeAPIBase, \
                         timeout2float, finalize_opts
from Skype4Py.enums import *
from Skype4Py.errors import SkypeAPIError
from Skype4Py.utils import cndexp


__all__ = ['SkypeAPI']


if getattr(sys, 'skype4py_setup', False):
    # we get here if we're building docs; to let the module import without
    # exceptions, we emulate the dbus module using a class:
    class dbus(object):
        class service(object):
            class Object(object):
                pass
            @staticmethod
            def method(*args, **kwargs):
                return lambda *args, **kwargs: None
else:
    import dbus
    import dbus.service
    from dbus.mainloop.glib import DBusGMainLoop
    import gobject


class SkypeNotify(dbus.service.Object):
    """DBus object which exports a Notify method. This will be called by Skype for all
    notifications with the notification string as a parameter. The Notify method of this
    class calls in turn the callable passed to the constructor.
    """

    def __init__(self, bus, notify):
        dbus.service.Object.__init__(self, bus, '/com/Skype/Client')
        self.notify = notify

    @dbus.service.method(dbus_interface='com.Skype.API.Client')
    def Notify(self, com):
        self.notify(unicode(com))


class SkypeAPI(SkypeAPIBase):
    def __init__(self, opts):
        self.logger = logging.getLogger('Skype4Py.api.posix_dbus.SkypeAPI')
        SkypeAPIBase.__init__(self)
        self.run_main_loop = opts.pop('RunMainLoop', True)
        finalize_opts(opts)
        self.skype_in = self.skype_out = self.dbus_name_owner_watch = None

        # initialize glib multithreading support
        gobject.threads_init()

        # dbus-python calls object.__init__() with arguments passed to SessionBus(),
        # this throws a warning on newer Python versions; here we suppress it
        warnings.simplefilter('ignore')
        try:
            self.bus = dbus.SessionBus(mainloop=DBusGMainLoop())
        finally:
            warnings.simplefilter('default')
        
        if self.run_main_loop:
            self.mainloop = gobject.MainLoop()

    def run(self):
        self.logger.info('thread started')
        if self.run_main_loop:
            self.mainloop.run()
        self.logger.info('thread finished')

    def close(self):
        if self.run_main_loop:
            self.mainloop.quit()
        self.skype_in = self.skype_out = None
        if self.dbus_name_owner_watch is not None:
            self.bus.remove_signal_receiver(self.dbus_name_owner_watch)
        self.dbus_name_owner_watch = None
        SkypeAPIBase.close(self)

    def set_friendly_name(self, friendly_name):
        SkypeAPIBase.set_friendly_name(self, friendly_name)
        if self.skype_out:
            self.send_command(Command('NAME %s' % friendly_name))

    def start_watcher(self):
        # starts a signal receiver detecting Skype being closed/opened
        self.dbus_name_owner_watch = self.bus.add_signal_receiver(self.dbus_name_owner_changed,
            'NameOwnerChanged',
            'org.freedesktop.DBus',
            'org.freedesktop.DBus',
            '/org/freedesktop/DBus',
            arg0='com.Skype.API')

    def attach(self, timeout, wait=True):
        self.acquire()
        try:
            try:
                if not self.isAlive():
                    self.start_watcher()
                    self.start()
            except AssertionError:
                pass
            try:
                self.wait = True
                t = threading.Timer(timeout2float(timeout), lambda: setattr(self, 'wait', False))
                if wait:
                    t.start()
                while self.wait:
                    if not wait:
                        self.wait = False
                    try:
                        if not self.skype_out:
                            self.skype_out = self.bus.get_object('com.Skype.API', '/com/Skype')
                        if not self.skype_in:
                            self.skype_in = SkypeNotify(self.bus, self.notify)
                    except dbus.DBusException:
                        if not wait:
                            break
                        time.sleep(1.0)
                    else:
                        break
                else:
                    raise SkypeAPIError('Skype attach timeout')
            finally:
                t.cancel()
            command = Command('NAME %s' % self.friendly_name, '', True, timeout)
            if self.skype_out:
                self.release()
                try:
                    self.send_command(command)
                finally:
                    self.acquire()
            if command.Reply != 'OK':
                self.skype_out = None
                self.set_attachment_status(apiAttachRefused)
                return
            self.set_attachment_status(apiAttachSuccess)
        finally:
            self.release()
        command = Command('PROTOCOL %s' % self.protocol, Blocking=True)
        self.send_command(command)
        self.protocol = int(command.Reply.rsplit(None, 1)[-1])

    def is_running(self):
        try:
            self.bus.get_object('com.Skype.API', '/com/Skype')
            return True
        except dbus.DBusException:
            return False

    def startup(self, minimized, nosplash):
        # options are not supported as of Skype 1.4 Beta for Linux
        if not self.is_running():
            import os
            if os.fork() == 0: # we're child
                os.setsid()
                os.execlp('skype')

    def shutdown(self):
        import os
        from signal import SIGINT
        fh = os.popen('ps -o %p --no-heading -C skype')
        pid = fh.readline().strip()
        fh.close()
        if pid:
            os.kill(int(pid), SIGINT)
            self.skype_in = self.skype_out = None

    def send_command(self, command):
        if not self.skype_out:
            self.attach(command.Timeout)
        self.push_command(command)
        self.notifier.sending_command(command)
        cmd = u'#%d %s' % (command.Id, command.Command)
        self.logger.debug('sending %s', repr(cmd))
        if command.Blocking:
            if self.run_main_loop:
                command._event = event = threading.Event()
            else:
                command._loop = loop = gobject.MainLoop()
                command._set = False
        else:
            command._timer = timer = threading.Timer(command.timeout2float(), self.pop_command, (command.Id,))
        try:
            result = self.skype_out.Invoke(cmd)
        except dbus.DBusException, err:
            raise SkypeAPIError(str(err))
        if result.startswith(u'#%d ' % command.Id):
            self.notify(result)
        if command.Blocking:
            if self.run_main_loop:
                event.wait(command.timeout2float())
                if not event.isSet():
                    raise SkypeAPIError('Skype command timeout')
            elif not command._set:
                gobject.timeout_add_seconds(int(command.timeout2float()), loop.quit)
                loop.run()
                if not command._set:
                    raise SkypeAPIError('Skype command timeout')
        else:
            timer.start()

    def notify(self, cmd):
        cmd = unicode(cmd)
        self.logger.debug('received %s', repr(cmd))
        if cmd.startswith(u'#'):
            p = cmd.find(u' ')
            command = self.pop_command(int(cmd[1:p]))
            if command is not None:
                command.Reply = cmd[p + 1:]
                if command.Blocking:
                    if self.run_main_loop:
                        command._event.set()
                    else:
                        command._set = True
                        command._loop.quit()
                else:
                    command._timer.cancel()
                self.notifier.reply_received(command)
            else:
                self.notifier.notification_received(cmd[p + 1:])
        else:
            self.notifier.notification_received(cmd)

    def dbus_name_owner_changed(self, owned, old_owner, new_owner):
        self.logger.debug('received dbus name owner changed')
        if new_owner == '':
            self.skype_out = None
        self.set_attachment_status(cndexp((new_owner == ''),
            apiAttachNotAvailable,
            apiAttachAvailable))
