"""
Low level *Skype for Mac OS X* interface implemented using *Carbon
distributed notifications*. Uses direct *Carbon*/*CoreFoundation*
calls through the *ctypes* module.

This module handles the options that you can pass to
`Skype.__init__` for *Mac OS X* machines.

- ``RunMainLoop`` (bool) - If set to False, Skype4Py won't start the Carbon event
  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.

Thanks to **Eion Robb** for reversing *Skype for Mac* API protocol.
"""
__docformat__ = 'restructuredtext en'


import sys
from ctypes import *
from ctypes.util import find_library
import threading
import time
import logging

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


__all__ = ['SkypeAPI']


class CFType(object):
    """Fundamental type for all CoreFoundation types.

    :see: http://developer.apple.com/documentation/CoreFoundation/Reference/CFTypeRef/
    """

    def __init__(self, init):
        self.owner = True
        if isinstance(init, CFType):
            # copy the handle and increase the use count
            self.handle = init.get_handle()
            coref.CFRetain(self)
        elif isinstance(init, c_void_p):
            self.handle = init
        else:
            raise TypeError('illegal init type: %s' % type(init))

    @classmethod
    def from_handle(cls, handle):
        if isinstance(handle, (int, long)):
            handle = c_void_p(handle)
        elif not isinstance(handle, c_void_p):
            raise TypeError('illegal handle type: %s' % type(handle))
        obj = cls(handle)
        obj.owner = False
        return obj

    def __del__(self):
        if not coref:
            return
        if self.owner:
            coref.CFRelease(self)

    def __repr__(self):
        return '%s(handle=%s)' % (self.__class__.__name__, repr(self.handle))

    def retain(self):
        if not self.owner:
            coref.CFRetain(self)
            self.owner = True

    def get_retain_count(self):
        return coref.CFGetRetainCount(self)

    def get_handle(self):
        return self.handle

    # allows passing CF types as ctypes function parameters
    _as_parameter_ = property(get_handle)


class CFString(CFType):
    """CoreFoundation string type.

    Supports Python unicode type only. String is immutable.

    :see: http://developer.apple.com/documentation/CoreFoundation/Reference/CFStringRef/
    """

    def __init__(self, init=u''):
        if isinstance(init, (str, unicode)):
            s = unicode(init).encode('utf-8')
            init = c_void_p(coref.CFStringCreateWithBytes(None,
                                    s, len(s), 0x08000100, False))
        CFType.__init__(self, init)

    def __str__(self):
        i = coref.CFStringGetLength(self)
        size = c_long()
        if coref.CFStringGetBytes(self, 0, i, 0x08000100, 0, False, None, 0, byref(size)) > 0:
            buf = create_string_buffer(size.value)
            coref.CFStringGetBytes(self, 0, i, 0x08000100, 0, False, buf, size, None)
            return buf.value
        else:
            raise UnicodeError('CFStringGetBytes() failed')

    def __unicode__(self):
        return self.__str__().decode('utf-8')

    def __len__(self):
        return coref.CFStringGetLength(self)

    def __repr__(self):
        return 'CFString(%s)' % repr(unicode(self))


class CFNumber(CFType):
    """CoreFoundation number type.

    Supports Python int type only. Number is immutable.

    :see: http://developer.apple.com/documentation/CoreFoundation/Reference/CFNumberRef/
    """

    def __init__(self, init=0):
        if isinstance(init, (int, long)):
            init = c_void_p(coref.CFNumberCreate(None, 3, byref(c_int(int(init)))))
        CFType.__init__(self, init)

    def __int__(self):
        n = c_int()
        if coref.CFNumberGetValue(self, 3, byref(n)):
            return n.value
        return 0

    def __repr__(self):
        return 'CFNumber(%s)' % repr(int(self))


class CFDictionary(CFType):
    """CoreFoundation immutable dictionary type.

    :see: http://developer.apple.com/documentation/CoreFoundation/Reference/CFDictionaryRef/
    """

    def __init__(self, init={}):
        if isinstance(init, dict):
            d = dict(init)
            keys = (c_void_p * len(d))()
            values = (c_void_p * len(d))()
            for i, (k, v) in enumerate(d.items()):
                keys[i] = k.get_handle()
                values[i] = v.get_handle()
            init = c_void_p(coref.CFDictionaryCreate(None, keys, values, len(d),
                coref.kCFTypeDictionaryKeyCallBacks, coref.kCFTypeDictionaryValueCallBacks))
        CFType.__init__(self, init)

    def get_dict(self):
        n = len(self)
        keys = (c_void_p * n)()
        values = (c_void_p * n)()
        coref.CFDictionaryGetKeysAndValues(self, keys, values)
        d = dict()
        for i in xrange(n):
            d[CFType.from_handle(keys[i])] = CFType.from_handle(values[i])
        return d

    def __getitem__(self, key):
        return CFType.from_handle(coref.CFDictionaryGetValue(self, key))

    def __len__(self):
        return coref.CFDictionaryGetCount(self)


class CFDistributedNotificationCenter(CFType):
    """CoreFoundation distributed notification center type.

    :see: http://developer.apple.com/documentation/CoreFoundation/Reference/CFNotificationCenterRef/
    """

    CFNOTIFICATIONCALLBACK = CFUNCTYPE(None, c_void_p, c_void_p, c_void_p, c_void_p, c_void_p)

    def __init__(self):
        CFType.__init__(self, c_void_p(coref.CFNotificationCenterGetDistributedCenter()))
        # there is only one distributed notification center per application
        self.owner = False
        self.callbacks = {}
        self._c_callback = self.CFNOTIFICATIONCALLBACK(self._callback)

    def _callback(self, center, observer, name, obj, userInfo):
        observer = CFString.from_handle(observer)
        name = CFString.from_handle(name)
        if obj:
            obj = CFString.from_handle(obj)
        userInfo = CFDictionary.from_handle(userInfo)
        callback = self.callbacks[(unicode(observer), unicode(name))]
        callback(self, observer, name, obj, userInfo)

    def add_observer(self, observer, callback, name=None, obj=None,
            drop=False, coalesce=False, hold=False, immediate=False):
        if not callable(callback):
            raise TypeError('callback must be callable')
        observer = CFString(observer)
        self.callbacks[(unicode(observer), unicode(name))] = callback
        if name is not None:
            name = CFString(name)
        if obj is not None:
            obj = CFString(obj)
        if drop:
            behaviour = 1
        elif coalesce:
            behaviour = 2
        elif hold:
            behaviour = 3
        elif immediate:
            behaviour = 4
        else:
            behaviour = 0
        coref.CFNotificationCenterAddObserver(self, observer,
                self._c_callback, name, obj, behaviour)

    def remove_observer(self, observer, name=None, obj=None):
        observer = CFString(observer)
        if name is not None:
            name = CFString(name)
        if obj is not None:
            obj = CFString(obj)
        coref.CFNotificationCenterRemoveObserver(self, observer, name, obj)
        try:
            del self.callbacks[(unicode(observer), unicode(name))]
        except KeyError:
            pass

    def post_notification(self, name, obj=None, userInfo=None, immediate=False):
        name = CFString(name)
        if obj is not None:
            obj = CFString(obj)
        if userInfo is not None:
            userInfo = CFDictionary(userInfo)
        coref.CFNotificationCenterPostNotification(self, name, obj, userInfo, immediate)


class EventLoop(object):
    """Carbon event loop object for the current thread.
    
    The Carbon reference documentation seems to be gone from developer.apple.com, the following
    link points to a mirror I found. I don't know how long until this one is gone too.
    
    :see: http://www.monen.nl/DevDoc/documentation/Carbon/Reference/Carbon_Event_Manager_Ref/index.html
    """
    
    def __init__(self):
        self.handle = c_void_p(carbon.GetCurrentEventLoop())

    @staticmethod
    def run(timeout=-1):
        # Timeout is expressed in seconds (float), -1 means forever.
        # Returns True if aborted (eventLoopQuitErr).
        return (carbon.RunCurrentEventLoop(timeout) == -9876)

    def stop(self):
        carbon.QuitEventLoop(self.handle)


# load the Carbon and CoreFoundation frameworks
# (only if not building the docs)
if not getattr(sys, 'skype4py_setup', False):

    path = find_library('Carbon')
    if path is None:
        raise ImportError('Could not find Carbon.framework')
    carbon = cdll.LoadLibrary(path)
    carbon.RunCurrentEventLoop.argtypes = (c_double,)

    path = find_library('CoreFoundation')
    if path is None:
        raise ImportError('Could not find CoreFoundation.framework')
    coref = cdll.LoadLibrary(path)


class SkypeAPI(SkypeAPIBase):
    """
    :note: Code based on Pidgin Skype Plugin source
           (http://code.google.com/p/skype4pidgin/).
           Permission to use granted by the author.
    """

    def __init__(self, opts):
        self.logger = logging.getLogger('Skype4Py.api.darwin.SkypeAPI')
        SkypeAPIBase.__init__(self)
        self.run_main_loop = opts.pop('RunMainLoop', True)
        finalize_opts(opts)
        self.center = CFDistributedNotificationCenter()
        self.is_available = False
        self.client_id = -1
        self.thread_started = False

    def start(self):
        """
        Start the thread associated with this API object.
        Ensure that the call is made no more than once,
        to avoid raising a RuntimeError.
        """
        if not self.thread_started:
            super(SkypeAPI, self).start()
            self.thread_started = True

    def run(self):
        self.logger.info('thread started')
        if self.run_main_loop:
            self.loop = EventLoop()
            EventLoop.run()
        self.logger.info('thread finished')

    def close(self):
        if hasattr(self, 'loop'):
            self.loop.stop()
            self.client_id = -1
        SkypeAPIBase.close(self)

    def set_friendly_name(self, friendly_name):
        SkypeAPIBase.set_friendly_name(self, friendly_name)
        if self.attachment_status == apiAttachSuccess:
            # reattach with the new name
            self.set_attachment_status(apiAttachUnknown)
            self.attach()

    def attach(self, timeout, wait=True):
        if self.attachment_status in (apiAttachPendingAuthorization, apiAttachSuccess):
            return
        self.acquire()
        try:
            try:
                self.start()
            except AssertionError:
                pass
            t = threading.Timer(timeout2float(timeout), lambda: setattr(self, 'wait', False))
            try:
                self.init_observer()
                self.client_id = -1
                self.set_attachment_status(apiAttachPendingAuthorization)
                self.post('SKSkypeAPIAttachRequest')
                self.wait = True
                if wait:
                    t.start()
                while self.wait and self.attachment_status == apiAttachPendingAuthorization:
                    if self.run_main_loop:
                        time.sleep(1.0)
                    else:
                        EventLoop.run(1.0)
            finally:
                t.cancel()
            if not self.wait:
                self.set_attachment_status(apiAttachUnknown)
                raise SkypeAPIError('Skype attach timeout')
        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.start()
        except AssertionError:
            pass
        self.init_observer()
        self.is_available = False
        self.post('SKSkypeAPIAvailabilityRequest')
        time.sleep(1.0)
        return self.is_available

    def startup(self, minimized, nosplash):
        if not self.is_running():
            from subprocess import Popen
            nul = file('/dev/null')
            Popen(['/Applications/Skype.app/Contents/MacOS/Skype'], stdin=nul, stdout=nul, stderr=nul)

    def send_command(self, command):
        if not self.attachment_status == apiAttachSuccess:
            self.attach(command.Timeout)
        self.push_command(command)
        self.notifier.sending_command(command)
        cmd = u'#%d %s' % (command.Id, command.Command)
        if command.Blocking:
            if self.run_main_loop:
                command._event = event = threading.Event()
            else:
                command._loop = EventLoop()
        else:
            command._timer = timer = threading.Timer(command.timeout2float(), self.pop_command, (command.Id,))

        self.logger.debug('sending %s', repr(cmd))
        userInfo = CFDictionary({CFString('SKYPE_API_COMMAND'): CFString(cmd),
                                 CFString('SKYPE_API_CLIENT_ID'): CFNumber(self.client_id)})
        self.post('SKSkypeAPICommand', userInfo)

        if command.Blocking:
            if self.run_main_loop:
                event.wait(command.timeout2float())
                if not event.isSet():
                    raise SkypeAPIError('Skype command timeout')
            else:
                if not EventLoop.run(command.timeout2float()):
                    raise SkypeAPIError('Skype command timeout')
        else:
            timer.start()

    def init_observer(self):
        if self.has_observer():
            self.delete_observer()
        self.observer = CFString(self.friendly_name)
        self.center.add_observer(self.observer, self.SKSkypeAPINotification, 'SKSkypeAPINotification', immediate=True)
        self.center.add_observer(self.observer, self.SKSkypeWillQuit, 'SKSkypeWillQuit', immediate=True)
        self.center.add_observer(self.observer, self.SKSkypeBecameAvailable, 'SKSkypeBecameAvailable', immediate=True)
        self.center.add_observer(self.observer, self.SKAvailabilityUpdate, 'SKAvailabilityUpdate', immediate=True)
        self.center.add_observer(self.observer, self.SKSkypeAttachResponse, 'SKSkypeAttachResponse', immediate=True)

    def delete_observer(self):
        if not self.has_observer():
            return
        self.center.remove_observer(self.observer, 'SKSkypeAPINotification')
        self.center.remove_observer(self.observer, 'SKSkypeWillQuit')
        self.center.remove_observer(self.observer, 'SKSkypeBecameAvailable')
        self.center.remove_observer(self.observer, 'SKAvailabilityUpdate')
        self.center.remove_observer(self.observer, 'SKSkypeAttachResponse')
        del self.observer

    def has_observer(self):
        return hasattr(self, 'observer')

    def post(self, name, userInfo=None):
        if not self.has_observer():
            self.init_observer()
        self.center.post_notification(name, self.observer, userInfo, immediate=True)

    def SKSkypeAPINotification(self, center, observer, name, obj, userInfo):
        client_id = int(CFNumber(userInfo[CFString('SKYPE_API_CLIENT_ID')]))
        if client_id != 999 and (client_id == 0 or client_id != self.client_id):
            return
        cmd = unicode(CFString(userInfo[CFString('SKYPE_API_NOTIFICATION_STRING')]))
        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._loop.stop()
                else:
                    command._timer.cancel()
                self.notifier.reply_received(command)
            else:
                self.notifier.notification_received(cmd[p + 1:])
        else:
            self.notifier.notification_received(cmd)

    def SKSkypeWillQuit(self, center, observer, name, obj, userInfo):
        self.logger.debug('received SKSkypeWillQuit')
        self.set_attachment_status(apiAttachNotAvailable)

    def SKSkypeBecameAvailable(self, center, observer, name, obj, userInfo):
        self.logger.debug('received SKSkypeBecameAvailable')
        self.set_attachment_status(apiAttachAvailable)

    def SKAvailabilityUpdate(self, center, observer, name, obj, userInfo):
        self.logger.debug('received SKAvailabilityUpdate')
        self.is_available = not not int(CFNumber(userInfo[CFString('SKYPE_API_AVAILABILITY')]))

    def SKSkypeAttachResponse(self, center, observer, name, obj, userInfo):
        self.logger.debug('received SKSkypeAttachResponse')
        # It seems that this notification is not called if the access is refused. Therefore we can't
        # distinguish between attach timeout and access refuse.
        if unicode(CFString(userInfo[CFString('SKYPE_API_CLIENT_NAME')])) == self.friendly_name:
            response = int(CFNumber(userInfo[CFString('SKYPE_API_ATTACH_RESPONSE')]))
            if response and self.client_id == -1:
                self.client_id = response
                self.set_attachment_status(apiAttachSuccess)
