# Copyright (c) 2006 Bea Lam. All rights reserved.
# 
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# Mac OS X bluetooth sockets implementation.
#
# To-do:
# - allow socket options
#
# if doing security AUTH, should set bool arg when calling
#   openConnection_withPageTimeout_authenticationRequired_() in connect()


import time
import socket as _socket
import threading
import os
import errno
import types

import objc
import Foundation

import _IOBluetooth
import _lightbluecommon
import _macutil
from _LightAquaBlue import BBServiceAdvertiser, BBBluetoothChannelDelegate

import sets     # python 2.3

try:
    SHUT_RD, SHUT_WR, SHUT_RDWR = \
        _socket.SHUT_RD, _socket.SHUT_WR, _socket.SHUT_RDWR
except AttributeError:
    # python 2.3
    SHUT_RD, SHUT_WR, SHUT_RDWR = (0, 1, 2)


def _getavailableport(proto):
    # Just advertise a service and see what channel it was assigned, then
    # stop advertising the service and return the channel.
    # It's a hacky way of doing it, but IOBluetooth doesn't seem to provide
    # functionality for just getting an available channel.

    if proto == _lightbluecommon.RFCOMM:
        try:
            result, channelID, servicerecordhandle = BBServiceAdvertiser.addRFCOMMServiceDictionary_withName_UUID_channelID_serviceRecordHandle_(BBServiceAdvertiser.serialPortProfileDictionary(), "DummyService", None, None, None)
        except:
            result, channelID, servicerecordhandle = BBServiceAdvertiser.addRFCOMMServiceDictionary_withName_UUID_channelID_serviceRecordHandle_(BBServiceAdvertiser.serialPortProfileDictionary(), "DummyService", None)
        if result != _macutil.kIOReturnSuccess:
            raise _lightbluecommon.BluetoothError(result, \
                "Could not retrieve an available service channel")
        result = BBServiceAdvertiser.removeService_(servicerecordhandle)
        if result != _macutil.kIOReturnSuccess:
            raise _lightbluecommon.BluetoothError(result, \
                "Could not retrieve an available service channel")
        return channelID
    
    else:
        raise NotImplementedError("L2CAP server sockets not currently supported")


def _checkaddrpair(address, checkbtaddr=True):
    # will want checkbtaddr=False if the address might be empty string 
    # (for binding to a server address)
    
    if not isinstance(address, tuple):
        raise TypeError("address must be (address, port) tuple, was %s" % \
            type(address))
    
    if len(address) != 2:
        raise TypeError("address tuple must have 2 items (has %d)" % \
            len(address))
            
    if not isinstance(address[0], types.StringTypes):
        raise TypeError("address host value must be string, was %s" % \
            type(address[0]))
        
    if checkbtaddr:
        if not _lightbluecommon._isbtaddr(address[0]):
            raise TypeError("address '%s' is not a bluetooth address" % \
                address[0])
    
    if not isinstance(address[1], int):
        raise TypeError("address port value must be int, was %s" % \
            type(address[1]))              


# from std lib socket module
class _closedsocket(object):
    __slots__ = []
    def _dummy(*args):
        raise _socket.error(errno.EBADF, 'Bad file descriptor')
    send = recv = sendto = recvfrom = __getattr__ = _dummy


# Thanks to Simon Wittber for string queue recipe 
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/426060 
# (this is a modified version)
class _StringQueue(object):
    def __init__(self):
        self.l_buffer = []
        self.s_buffer = ""
        self.lock = threading.RLock()
        self.bufempty = True

    def empty(self):
        return self.bufempty

    def write(self, data):
        # no type check, and assumes data is not empty!
        #append data to list, no need to "".join just yet.
        self.lock.acquire()
        try:
            self.l_buffer.append(data)
            self.bufempty = False
        finally:
            self.lock.release()

    def _build_str(self):
        #build a new string out of list
        new_string = "".join(self.l_buffer)
        #join string buffer and new string
        self.s_buffer = "".join((self.s_buffer, new_string))
        #clear list
        self.l_buffer = []

    def __len__(self):
        #calculate length without needing to _build_str
        return sum([len(i) for i in self.l_buffer]) + len(self.s_buffer)

    def read(self, count):
        self.lock.acquire()
        try:
            #if string doesn't have enough chars to satisfy caller
            if count > len(self.s_buffer): 
                self._build_str()           
            #get data requested by caller
            result = self.s_buffer[:count]
            #remove requested data from string buffer
            self.s_buffer = self.s_buffer[len(result):]
            self.bufempty = (len(self.s_buffer) == 0)   
        finally:
            self.lock.release()         
        return result



#class _SocketWrapper(_socket._socketobject):
class _SocketWrapper(object):
    """
    A Bluetooth socket object has the same interface as a socket object from 
    the Python standard library <socket> module. It also uses the same 
    exceptions, raising socket.error for general errors and socket.timeout for 
    timeout errors.
    
    Note that L2CAP sockets are not available on Python For Series 60, and
    only L2CAP client sockets are supported on Mac OS X and Linux.
    
    A simple client socket example:
        >>> from lightblue import *
        >>> s = socket()        # or socket(L2CAP) to create an L2CAP socket
        >>> s.connect(("00:12:2c:45:8a:7b", 5))
        >>> s.send("hello")
        5
        >>> s.close()
        
    A simple server socket example:
        >>> from lightblue import *    
        >>> s = socket()
        >>> s.bind(("", 0))
        >>> s.listen(1)
        >>> advertise("My RFCOMM Service", s, RFCOMM)
        >>> conn, addr = s.accept()
        >>> print "Connected by", addr
        Connected by ('00:0D:93:19:C8:68', 5)
        >>> conn.recv(1024)
        "hello"
        >>> conn.close()
        >>> s.close()    
    """

    def __init__(self, sock):
        self._sock = sock

    def accept(self):
        sock, addr = self._sock.accept()
        return _SocketWrapper(sock), addr
    accept.__doc__ = _lightbluecommon._socketdocs["accept"]
        
    def dup(self):
        return _SocketWrapper(self._sock)        
    dup.__doc__ = _lightbluecommon._socketdocs["dup"]

    def close(self):        
        self._sock.close()
        
        self._sock = _closedsocket()
        self.send = self.recv = self.sendto = self.recvfrom = self._sock._dummy
        
        try:
            import lightblue
            lightblue.stopadvertise(self)
        except:
            pass
    close.__doc__ = _lightbluecommon._socketdocs["close"]
        
    def makefile(self, mode='r', bufsize=-1):
        # use std lib socket's _fileobject
        return _socket._fileobject(self._sock, mode, bufsize)
    makefile.__doc__ = _lightbluecommon._socketdocs["makefile"]        

    # delegate all other method calls to internal sock obj
    def __getattr__(self, attr):
        return getattr(self._sock, attr)


# internal _sock object for RFCOMM and L2CAP sockets
class _BluetoothSocket(object):

    _boundports = { _lightbluecommon.L2CAP: sets.Set(),
                    _lightbluecommon.RFCOMM: sets.Set() }

    # conn is the associated _RFCOMMConnection or _L2CAPConnection
    def __init__(self, conn):    
        self.__conn = conn
        
        if conn is not None and conn.channel is not None:
            self.__remotedevice = conn.channel.getDevice()
        else:
            self.__remotedevice = None
        
        # timeout=None cos sockets default to blocking mode
        self.__timeout = None
        
        #self.__isserverspawned = (conn.channel is not None)
        self.__port = 0
        self.__eventlistener = None
        self.__closed = False
        self.__maxqueuedconns = 0
        self.__incomingdata = _StringQueue()
        self.__queuedchannels = []
        self.__queuedchannels_lock = threading.RLock()
        
        # whether send or recv has been shut down 
        # set initial value to be other than SHUT_WR/SHUT_RD/SHUT_RDWR
        self.__commstate = -1 
        
    def accept(self):
        if not self.__isbound():
            raise _socket.error('Socket not bound')
        if not self.__islistening():
            raise _socket.error('Socket must be listening first')
        
        def clientconnected():
            return len(self.__queuedchannels) > 0
        if not clientconnected():
            self.__waituntil(clientconnected, "accept timed out")
        
        self.__queuedchannels_lock.acquire()
        try:
            newchannel = self.__queuedchannels.pop(0)
        finally:    
            self.__queuedchannels_lock.release()      
        
        # return (new-socket, addr) pair using the new channel
        newconn = _SOCKET_CLASSES[self.__conn.proto](newchannel)
        sock = _SocketWrapper(_BluetoothSocket(newconn))
        sock.__startevents()
        return (sock, sock.getpeername())
        
    def bind(self, address):
        _checkaddrpair(address, False)
        if self.__isbound():
            raise _socket.error('Socket is already bound')
        elif self.__isconnected():
            raise _socket.error("Socket is already connected, cannot be bound")
            
        if self.__conn.proto == _lightbluecommon.L2CAP:
            raise NotImplementedError("L2CAP server sockets not currently supported")
            
        if address[1] != 0:
            raise _socket.error("must bind to port 0, other ports not supported on Mac OS X")
        address = (address[0], _getavailableport(self.__conn.proto))
            
        # address must be either empty string or local device address
        if address[0] != "":
            try:
                import lightblue            
                localaddr = lightblue.gethostaddr()
            except:
                localaddr = None
            if localaddr is None or address[0] != localaddr:
                raise _socket.error(
                    errno.EADDRNOTAVAIL, os.strerror(errno.EADDRNOTAVAIL))            
                
        # is this port already in use?
        if address[1] in self._boundports[self.__conn.proto]:
            raise _socket.error(errno.EADDRINUSE, os.strerror(errno.EADDRINUSE))
    
        self._boundports[self.__conn.proto].add(address[1])
        self.__port = address[1]
        
    def close(self):
        wasconnected = self.__isconnected() or self.__isbound()
        self.__stopevents()
        
        if self.__conn is not None:
            if self.__isbound():
                self._boundports[self.__conn.proto].discard(self.__port)
            else:
                if self.__conn.channel is not None:
                    self.__conn.channel.setDelegate_(None)
                    self.__conn.channel.closeChannel()    
                
        # disconnect the baseband connection.
        # This will fail if other RFCOMM channels to the remote device are 
        # still open (which is what we want, cos we don't know if another 
        # process is talking to the device)
        if self.__remotedevice is not None: 
            self.__remotedevice.closeConnection()   # returns err code
            
        # if you don't run the event loop a little here, it's likely you won't
        # be able to reconnect to the same remote device later
        if wasconnected:
            _macutil.waitfor(0.5)

            
    def connect(self, address):
        if self.__isbound():
            raise _socket.error("Can't connect, socket has been bound")
        elif self.__isconnected():
            raise _socket.error("Socket is already connected")
        _checkaddrpair(address)
        
        # open a connection to device
        self.__remotedevice = _IOBluetooth.IOBluetoothDevice.withAddress_(
                _macutil.createbtdevaddr(address[0]))
        if not self.__remotedevice.isConnected():
            if self.__timeout is None:
                result = self.__remotedevice.openConnection()
            else:
                result = self.__remotedevice.openConnection_withPageTimeout_authenticationRequired_(
                    None, self.__timeout*1000, False)
                
            if result != _macutil.kIOReturnSuccess:
                if result == _macutil.kBluetoothHCIErrorPageTimeout:
                    if self.__timeout == 0:
                        raise _socket.error(errno.EAGAIN, 
                            "Resource temporarily unavailable")
                    else:
                        raise _socket.timeout("connect timed out")
                else: 
                    raise _socket.error(result, 
                        "Cannot connect to %s, can't open connection." \
                                                            % str(address[0]))
                                                            
        # open RFCOMM or L2CAP channel
        self.__eventlistener = self.__createlistener()
        result = self.__conn.connect(self.__remotedevice, address[1], 
                self.__eventlistener)   # pass listener as cocoa delegate
                
        if result != _macutil.kIOReturnSuccess:
            self.__remotedevice.closeConnection()
            self.__stopevents()
            self.__eventlistener = None
            raise _socket.error(result, 
                    "Cannot connect to %d on %s" % (address[1], address[0]))
            return
            
        # if you don't run the event loop a little here, it's likely you won't
        # be able to reconnect to the same remote device later
        _macutil.waitfor(0.5)

    def connect_ex(self, address):
        try:
            self.connect(address)
        except _socket.error, err:
            if len(err.args) > 1:
                return err.args[0]
            else:
                # there's no error code, just a message, so this error wasn't
                # from a system call -- so re-raise the exception
                raise _socket.error(err)
        return 0
        
    def getpeername(self):
        self.__checkconnected()
        addr = _macutil.formatdevaddr(self.__remotedevice.getAddressString())   
        return (addr, self._getport())
            
    def getsockname(self):
        if self.__isbound() or self.__isconnected():
            import lightblue
            return (lightblue.gethostaddr(), self._getport())
        else:
            return ("00:00:00:00:00:00", 0)

    def listen(self, backlog):
        if self.__islistening():
            return

        if not self.__isbound():
            raise _socket.error('Socket not bound')
        if not isinstance(backlog, int):
            raise TypeError("backlog must be int, was %s" % type(backlog))
        if backlog < 0: 
            raise ValueError("backlog cannot be negative, was %d" % backlog)
                    
        self.__maxqueuedconns = backlog
                    
        # start listening for client connections
        self.__startevents()
        
        
    def _isclosed(self):
        # isOpen() check doesn't work for incoming (server-spawned) channels            
        if (self.__conn.proto == _lightbluecommon.RFCOMM and 
                self.__conn.channel is not None and
                not self.__conn.channel.isIncoming()):
            return not self.__conn.channel.isOpen()    
        return self.__closed


    def recv(self, bufsize, flags=0):
        if self.__commstate in (SHUT_RD, SHUT_RDWR):
            return ""
        self.__checkconnected()

        if not isinstance(bufsize, int):
            raise TypeError("buffer size must be int, was %s" % type(bufsize))
        if bufsize < 0: 
            raise ValueError("negative buffersize in recv") # as for tcp 
        if bufsize == 0: 
            return ""
            
        # need this to ensure the _isclosed() check is up-to-date
        _macutil.looponce()
        
        if self._isclosed():
            if len(self.__incomingdata) == 0:
                raise _socket.error(errno.ECONNRESET,         
                                    os.strerror(errno.ECONNRESET))
            return self.__incomingdata.read(bufsize)
    
        # if incoming data buffer is empty, wait until data is available or
        # channel is closed
        def gotdata():
            return not self.__incomingdata.empty() or self._isclosed()
        if not gotdata():
            self.__waituntil(gotdata, "recv timed out")

        # other side closed connection while waiting?
        if self._isclosed() and len(self.__incomingdata) == 0:    
            raise _socket.error(errno.ECONNRESET, os.strerror(errno.ECONNRESET))
            
        return self.__incomingdata.read(bufsize)
        
        
    # recvfrom() is really for datagram sockets not stream sockets but it 
    # can be implemented anyway.
    def recvfrom(self, bufsize, flags=0):
        # stream sockets return None, instead of address
        return (self.recv(bufsize, flags), None)
        
    def sendall(self, data, flags=0):
        sentbytescount = self.send(data, flags)
        while sentbytescount < len(data):
            sentbytescount += self.send(data[sentbytescount:], flags)
        return None
        
    def send(self, data, flags=0):
        if not isinstance(data, types.StringTypes):
            raise TypeError("data must be string, was %s" % type(data))
        if self.__commstate in (SHUT_WR, SHUT_RDWR):
            raise _socket.error(errno.EPIPE, os.strerror(errno.EPIPE))
        self.__checkconnected()
        
        # do setup for if sock is in non-blocking mode
        if self.__timeout is not None:
            if self.__timeout == 0:
                # in non-blocking mode
                # isTransmissionPaused() is not available for L2CAP sockets,
                # what to do for that?
                if self.__conn.proto == _lightbluecommon.RFCOMM and \
                        self.__conn.channel.isTransmissionPaused():
                    # sending data now will block
                    raise _socket.error(errno.EAGAIN, 
                        "Resource temporarily unavailable")
            elif self.__timeout > 0:
                # non-blocking with timeout
                starttime = time.time()
        
        # loop until all data is sent
        writebuf = data
        bytesleft = len(data)
        mtu = self.__conn.getwritemtu()
        while bytesleft > 0:
            if self.__timeout is not None and self.__timeout > 0:
                if time.time() - starttime > self.__timeout:
                    raise _socket.timeout("send timed out")
            
            # write the data to the channel (only the allowed amount)
            # the method/selector is the same for L2CAP and RFCOMM channels
            if bytesleft > mtu: 
                sendbytecount = mtu
            else: 
                sendbytecount = bytesleft            
            #result = self.__conn.channel.writeSync_length_(
            #        writebuf[:sendbytecount], sendbytecount)
            result = self.__conn.write(writebuf[:sendbytecount])

            # normal tcp sockets don't seem to actually error on the first 
            # send() after a connection has broken; if you try a second time,
            # then you get the (32, 'Broken pipe') socket.error
            if result != _macutil.kIOReturnSuccess:
                raise _socket.error(result, "Error sending data")
            
            bytesleft -= sendbytecount
            writebuf = writebuf[sendbytecount:] # remove the data just sent
            
        return len(data) - bytesleft     
        
    # sendto args may be one of:
    # - data, address
    # - data, flags, address
    #
    # The standard behaviour seems to be to ignore the given address if already
    # connected.
    # sendto() is really for datagram sockets not stream sockets but it 
    # can be implemented anyway.
    def sendto(self, data, *args):
        if len(args) == 1:
            address = args[0]
            flags = 0
        elif len(args) == 2:
            flags, address = args
        else:
            raise TypeError("sendto takes at most 3 arguments (%d given)" % \
                (len(args) + 1))
        _checkaddrpair(address)
            
        # must already be connected, cos this is stream socket
        self.__checkconnected()
        return self.send(data, flags)        
        
    def fileno(self):
        raise NotImplementedError

    def getsockopt(self, level, optname, buflen=0):
        # see what options on Linux+s60
        # possibly have socket security option.
        raise _socket.error(
            errno.ENOPROTOOPT, os.strerror(errno.ENOPROTOOPT))   
        
    def setsockopt(self, level, optname, value):
        # see what options on Linux+s60    
        # possibly have socket security option.        
        raise _socket.error(
            errno.ENOPROTOOPT, os.strerror(errno.ENOPROTOOPT))   
        
    def setblocking(self, flag):
        if flag == 0:
            self.__timeout = 0      # non-blocking
        else:
            self.__timeout = None   # blocking
        
    def gettimeout(self):
        return self.__timeout
        
    def settimeout(self, value):
        if value is not None and not isinstance(value, (float, int)):
            msg = "timeout value must be a number or None, was %s" % \
                type(value)
            raise TypeError(msg)
        if value < 0:
            msg = "timeout value cannot be negative, was %d" % value
            raise ValueError(msg)
        self.__timeout = value
        
    def shutdown(self, how):
        if how not in (SHUT_RD, SHUT_WR, SHUT_RDWR):
            raise _socket.error(22, "Invalid argument")
        self.__commstate = how
        
    # This method is called from outside this file.
    def _getport(self):
        if self.__isconnected():
            return self.__conn.getport()
        if self.__isbound():
            return self.__port
        raise _lightbluecommon.BluetoothError("socket is neither connected nor bound")

    # This method is called from outside this file.            
    def _getchannel(self):
        if self.__conn is None:
            return None
        return self.__conn.channel
        
    # Called by the event listener when data is available
    # 'channel' is IOBluetoothRFCOMMChannel or IOBluetoothL2CAPChannel object
    def _handle_channeldata(self, channel, data):
        self.__incomingdata.write(data)
        _macutil.interruptwait()

    # Called by the event listener when a client connects to a server socket        
    def _handle_channelopened(self, channel):
        # put new channels into a queue, which 'accept' can then pull out
        self.__queuedchannels_lock.acquire()
        try:
            # need to implement max connections
            #if len(self.__queuedchannels) < self.__maxqueuedconns:
            self.__queuedchannels.append(channel)
            _macutil.interruptwait()
        finally:
            self.__queuedchannels_lock.release()
            
    # Called by the event listener when the channel is closed.
    def _handle_channelclosed(self, channel):
        # beware that this value won't actually be set until the event loop
        # has been driven so that this method is actually called
        self.__closed = True
        _macutil.interruptwait()
            
    def __waituntil(self, stopwaiting, timeoutmsg):
        """
        Waits until stopwaiting() returns True, or until the wait times out
        (according to the self.__timeout value).
        
        This is to make a function wait until a buffer has been filled. i.e.
        stopwaiting() should return True when the buffer is no longer empty.
        """
        if not stopwaiting():
            if self.__timeout == 0:
                # in non-blocking mode (immediate timeout)
                # push event loop to really be sure there is no data available
                _macutil.looponce()
                if not stopwaiting():
                    # trying to perform operation now would block
                    raise _socket.error(errno.EAGAIN, os.strerror(errno.EAGAIN))
            else:
                # block and wait until we get data, or time out
                if not _macutil.waituntil(stopwaiting, self.__timeout):
                    raise _socket.timeout(timeoutmsg)
                    
    def __createlistener(self):
        if self.__isbound():
            return _ChannelServerEventListener.alloc().initWithDelegate_port_protocol_(self, 
                    self._getport(), self.__conn.proto)
        else:
            listener = _ChannelEventListener.alloc().initWithDelegate_(self)
            if self.__conn.channel is not None:
                self.__conn.channel.setDelegate_(listener.delegate())
                listener.registerclosenotif(self.__conn.channel)
            return listener
        
    # should not call this if connect() has been called to connect this socket
    def __startevents(self):
        if self.__eventlistener is not None:
            raise _lightbluecommon.BluetoothError("socket already listening")
        self.__eventlistener = self.__createlistener()
        
    def __stopevents(self):
        if self.__eventlistener is not None:
            self.__eventlistener.close()
            
    def __islistening(self):
        return self.__eventlistener is not None            
                
    def __checkconnected(self):
        if not self._sock.isconnected():  # i.e. is connected, non-server socket
            # not connected, raise "socket not connected"
            raise _socket.error(errno.ENOTCONN, os.strerror(errno.ENOTCONN))                
                
    # returns whether socket is a bound server socket
    def __isbound(self):
        return self.__port != 0
        
    def __isconnected(self):
        return self.__conn.channel is not None                
        
    def __checkconnected(self):
        if not self.__isconnected():
            # not connected, raise "socket not connected"
            raise _socket.error(errno.ENOTCONN, os.strerror(errno.ENOTCONN))
            
    # set method docstrings
    definedmethods = locals()   # i.e. defined methods in _SocketWrapper
    for name, doc in _lightbluecommon._socketdocs.items():
        try:
            definedmethods[name].__doc__ = doc
        except KeyError:
            pass
                
                
class _RFCOMMConnection(object):

    proto = _lightbluecommon.RFCOMM

    def __init__(self, channel=None):
        # self.channel is accessed by _BluetoothSocket parent
        self.channel = channel
    
    def connect(self, device, port, listener):
        # open RFCOMM channel (should timeout actually apply to opening of
        # channel as well? if so need to do timeout with async callbacks)
        try:
            # pyobjc 2.0
            result, self.channel = device.openRFCOMMChannelSync_withChannelID_delegate_(None, port, listener.delegate())
        except TypeError:
            result, self.channel = device.openRFCOMMChannelSync_withChannelID_delegate_(port, listener.delegate())        
        if result == _macutil.kIOReturnSuccess:
            self.channel.setDelegate_(listener.delegate())
            listener.registerclosenotif(self.channel)
        else:
            self.channel = None
        return result
        
    def write(self, data):
        if self.channel is None:
            raise _socket.error("socket not connected")
        return \
            BBBluetoothChannelDelegate.synchronouslyWriteData_toRFCOMMChannel_(
                buffer(data), self.channel)
        
    def getwritemtu(self):
        return self.channel.getMTU()
        
    def getport(self):
        return self.channel.getChannelID()
        
        
class _L2CAPConnection(object):

    proto = _lightbluecommon.L2CAP
    
    def __init__(self, channel=None):
        # self.channel is accessed by _BluetoothSocket parent
        self.channel = channel    

    def connect(self, device, port, listener):
        try:
            # pyobjc 2.0
            result, self.channel = device.openL2CAPChannelSync_withPSM_delegate_(None, port, listener.delegate())
        except TypeError:
            result, self.channel = device.openL2CAPChannelSync_withPSM_delegate_(port, listener.delegate())            
        if result == _macutil.kIOReturnSuccess:
            self.channel.setDelegate_(listener.delegate())
            listener.registerclosenotif(self.channel)
        else:
            self.channel = None
        return result
        
    def write(self, data):
        if self.channel is None:
            raise _socket.error("socket not connected")
        return \
            BBBluetoothChannelDelegate.synchronouslyWriteData_toL2CAPChannel_(
                buffer(data), self.channel)        
        
    def getwritemtu(self):
        return self.channel.getOutgoingMTU()        

    def getport(self):
        return self.channel.getPSM()
        
                                
class _ChannelEventListener(Foundation.NSObject):
    """
    Uses a BBBluetoothChannelDelegate to listen for events on an 
    IOBluetoothRFCOMMChannel or IOBluetoothL2CAPChannel, and makes callbacks to
    a specified object when events occur.
    """

    # note this is a NSObject "init", not a python object "__init__"
    def initWithDelegate_(self, cb_obj):
        """
        Arguments:
        - cb_obj: An object that receives callbacks when events occur. This
          object should have:
            - a method '_handle_channeldata' which takes the related channel (a 
              IOBluetoothRFCOMMChannel or IOBluetoothL2CAPChannel) and the new
              data (a string) as the arguments.
            - a method '_handle_channelclosed' which takes the related channel
              as the argument.
        
        If this listener's delegate is passed to the openRFCOMMChannel... or
        openL2CAPChannel... selectors as the delegate, the delegate (and
        therefore this listener) will automatically start receiving events. 
        
        Otherwise, call setDelegate_() on the channel with this listener's
        delegate as the argument to allow this listener to start receiving
        channel events. (This is the only option for server-spawned sockets.)
        """
        self = super(_ChannelEventListener, self).init()    
        if cb_obj is None:
            raise TypeError("callback object is None")
        self.__cb_obj = cb_obj
        self.__closenotif = None
        self.__channelDelegate = \
                BBBluetoothChannelDelegate.alloc().initWithDelegate_(self)
        return self
    initWithDelegate_ = objc.selector(initWithDelegate_, signature="@@:@")
        
    def delegate(self):
        return self.__channelDelegate
    
    def registerclosenotif(self, channel):
        # oddly enough, sometimes the channelClosed: selector doesn't get called
        # (maybe if there's a lot of data being passed?) but this seems to work
        notif = channel.registerForChannelCloseNotification_selector_(self, 
                "channelClosedEvent:channel:")
        if notif is not None:
            self.__closenotif = notif
    
    def close(self):
        if self.__closenotif is not None:
            self.__closenotif.unregister()
        
    def channelClosedEvent_channel_(self, notif, channel):
        if hasattr(self.__cb_obj, '_handle_channelclosed'):
            self.__cb_obj._handle_channelclosed(channel)        
    channelClosedEvent_channel_ = objc.selector(
            channelClosedEvent_channel_, signature="v@:@@")            
    
    # implement method from BBBluetoothChannelDelegateObserver protocol:
    # - (void)channelData:(id)channel data:(NSData *)data;
    def channelData_data_(self, channel, data):
        if hasattr(self.__cb_obj, '_handle_channeldata'):
            self.__cb_obj._handle_channeldata(channel, data[:])
    channelData_data_ = objc.selector(channelData_data_, signature="v@:@@")

    # implement method from BBBluetoothChannelDelegateObserver protocol:        
    # - (void)channelClosed:(id)channel;
    def channelClosed_(self, channel):
        if hasattr(self.__cb_obj, '_handle_channelclosed'):
            self.__cb_obj._handle_channelclosed(channel)
    channelClosed_ = objc.selector(channelClosed_, signature="v@:@")            
    
    
class _ChannelServerEventListener(Foundation.NSObject):
    """
    Listens for server-specific events on a RFCOMM or L2CAP channel (i.e. when a
    client connects) and makes callbacks to a specified object when events
    occur.
    """
    
    # note this is a NSObject "init", not a python object "__init__"
    def initWithDelegate_port_protocol_(self, cb_obj, port, proto):
        """
        Arguments:
        - cb_obj: to receive callbacks when a client connects to 
          to the channel, the callback object should have a method 
          '_handle_channelopened' which takes the newly opened 
          IOBluetoothRFCOMMChannel or IOBluetoothL2CAPChannel as its argument.
        - port: the channel or PSM that the server is listening on
        - proto: L2CAP or RFCOMM.
        """        
        self = super(_ChannelServerEventListener, self).init()            
        if cb_obj is None:
            raise TypeError("callback object is None")
        self.__cb_obj = cb_obj            
        self.__usernotif = None        

        if proto == _lightbluecommon.RFCOMM:
            usernotif = _IOBluetooth.IOBluetoothRFCOMMChannel.registerForChannelOpenNotifications_selector_withChannelID_direction_(self, "newChannelOpened:channel:", port, _macutil.kIOBluetoothUserNotificationChannelDirectionIncoming)      
        elif proto == _lightbluecommon.L2CAP:
            usernotif = _IOBluetooth.IOBluetoothL2CAPChannel.registerForChannelOpenNotifications_selector_withPSM_direction_(self, "newChannelOpened:channel:", port, _macutil.kIOBluetoothUserNotificationChannelDirectionIncoming)             
            
        if usernotif is None:
            raise _socket.error("Unable to register for channel-" + \
                "opened notifications on server socket on channel/PSM %d" % \
                port)
        self.__usernotif = usernotif
        return self
    initWithDelegate_port_protocol_ = objc.selector(
        initWithDelegate_port_protocol_, signature="@@:@ii")

    def close(self):
        if self.__usernotif is not None:
            self.__usernotif.unregister()   
        
    def newChannelOpened_channel_(self, notif, newChannel):
        """ 
        Handle when a client connects to the server channel.
        (This method is called for both RFCOMM and L2CAP channels.)
        """
        if newChannel is not None and newChannel.isIncoming():
            # not sure if delegate really needs to be set
            newChannel.setDelegate_(self) 

            if hasattr(self.__cb_obj, '_handle_channelopened'):          
                self.__cb_obj._handle_channelopened(newChannel)
    # makes this method receive notif and channel as objects                
    newChannelOpened_channel_ = objc.selector(
            newChannelOpened_channel_, signature="v@:@@")


# -----------------------------------------------------------

# protocol-specific classes
_SOCKET_CLASSES = { _lightbluecommon.RFCOMM: _RFCOMMConnection,
                    _lightbluecommon.L2CAP:  _L2CAPConnection }

def _getsocketobject(proto):
    if proto not in _SOCKET_CLASSES.keys():
        raise ValueError("Unknown socket protocol, must be L2CAP or RFCOMM")
    return _SocketWrapper(_BluetoothSocket(_SOCKET_CLASSES[proto]()))
