# 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.

import datetime
import time
import types

import objc
from Foundation import NSObject, NSDate

from _IOBluetooth import OBEXSession, IOBluetoothDevice, \
        IOBluetoothRFCOMMChannel
from _LightAquaBlue import BBBluetoothOBEXClient, BBBluetoothOBEXServer, \
        BBStreamingInputStream, BBStreamingOutputStream, \
        BBMutableOBEXHeaderSet, \
        BBLocalDevice
import _lightbluecommon
import _obexcommon
import _macutil

from _obexcommon import OBEXError

# from <IOBluetooth/OBEX.h>
_kOBEXSuccess = 0
_kOBEXGeneralError = -21850
_kOBEXSessionNotConnectedError = -21876
_kOBEXSessionAlreadyConnectedError = -21882
_kOBEXSessionNoTransportError = -21879
_kOBEXSessionTransportDiedError = -21880
_OBEX_FINAL_MASK = 0x80

_HEADER_MASK = 0xc0
_HEADER_UNICODE = 0x00
_HEADER_BYTE_SEQ = 0x40
_HEADER_1BYTE = 0x80
_HEADER_4BYTE = 0xc0

# public attributes
__all__ = ("OBEXClient", "sendfile", "recvfile")


_obexerrorcodes = { 0: "no error", -21850: "general error", -21851: "no resources", -21852: "operation not supported", -21853: "internal error", -21854: "bad argument", -21855: "timeout", -21856: "bad request", -21857: "cancelled", -21875: "session is busy", -21876: "OBEX session not connected", -21877: "bad request in OBEX session", -21878: "bad response from other party", -21879: "Bluetooth transport not available", -21880: "Bluetooth transport connection died", -21881: "OBEX session timed out", -21882: "OBEX session already connected" }

def errdesc(errorcode):
    return _obexerrorcodes.get(errorcode, str(errorcode))

    
# OBEXSession provides response codes with the final bit set, but OBEXResponse 
# class expects the response code to not have the final bit set.
def _cutresponsefinalbit(responsecode):
    return (responsecode & ~_OBEX_FINAL_MASK)
    
    
def _headersdicttoset(headers):
    headerset = BBMutableOBEXHeaderSet.alloc().init()
    for header, value in headers.items():
        if isinstance(header, types.StringTypes):
            hid = _obexcommon._HEADER_STRINGS_TO_IDS.get(header.lower())
        else:
            hid = header
        if hid is None:
            raise ValueError("unknown header '%s'" % header)
        if isinstance(value, datetime.datetime):
            value = value.strftime("%Y%m%dT%H%M%S")
        mask = hid & _HEADER_MASK
        if mask == _HEADER_UNICODE:
            if not isinstance(value, types.StringTypes):
                raise TypeError("value for '%s' must be string, was %s" %
                    (str(header), type(value)))
            headerset.setValue_forUnicodeHeader_(value, hid)
        elif mask == _HEADER_BYTE_SEQ:
            try:
                value = buffer(value)
            except:
                raise TypeError("value for '%s' must be string, array or other buffer type, was %s" % (str(header), type(value)))
            headerset.setValue_forByteSequenceHeader_(value, hid)
        elif mask == _HEADER_1BYTE:
            if not isinstance(value, int):
                raise TypeError("value for '%s' must be int, was %s" % 
                    (str(header), type(value)))
            headerset.setValue_for1ByteHeader_(value, hid)
        elif mask == _HEADER_4BYTE:
            if not isinstance(value, int) and not isinstance(value, long):
                raise TypeError("value for '%s' must be int, was %s" %
                    (str(header), type(value)))
            headerset.setValue_for4ByteHeader_(value, hid)
        if not headerset.containsValueForHeader_(hid):
            raise ValueError("cannot set OBEX header value for '%s'" % header)
    return headerset

    
# returns in { header-id: value } form.
def _headersettodict(headerset):
    headers = {}
    for number in headerset.allHeaders():
        hid = number.unsignedCharValue()
        mask = hid & _HEADER_MASK            
        if mask == _HEADER_UNICODE:
            value = headerset.valueForUnicodeHeader_(hid)
        elif mask == _HEADER_BYTE_SEQ:
            value = headerset.valueForByteSequenceHeader_(hid)[:]
            if hid == 0x42:     # type
                if len(value) > 0 and value[-1] == '\0':
                    value = value[:-1]  # remove null byte
            elif hid == 0x44:     # time iso-8601 string
                value = _obexcommon._datetimefromstring(value)
        elif mask == _HEADER_1BYTE:
            value = headerset.valueFor1ByteHeader_(hid)
        elif mask == _HEADER_4BYTE:
            value = headerset.valueFor4ByteHeader_(hid)
        headers[hid] = value
    return headers


class OBEXClient:
    __doc__ = _obexcommon._obexclientclassdoc
    
    def __init__(self, address, channel):
        if not _lightbluecommon._isbtaddr(address):  
            raise TypeError("address '%s' is not a valid bluetooth address"
                % address)
        if not type(channel) == int:
            raise TypeError("channel must be int, was %s" % type(channel))
        if channel < 0:
            raise ValueError("channel cannot be negative")
    
        self.__serveraddr = (address, channel)
        self.__busy = False    
        self.__client = None
        self.__obexsession = None   # for testing
        #BBBluetoothOBEXClient.setDebug_(True)
        
                    
    def connect(self, headers={}):
        if self.__client is not None:
            raise OBEXError(_kOBEXSessionAlreadyConnectedError, 
                    "session is already connected")
        if not BBLocalDevice.isPoweredOn():
            raise OBEXError(_kOBEXSessionNoTransportError, 
                    "Bluetooth device not available")                    

        self.__delegate = _BBOBEXClientDelegate.alloc().initWithCallback_(
                self._finishedrequest)
        self.__client = BBBluetoothOBEXClient.alloc().initWithRemoteDeviceAddress_channelID_delegate_(
                _macutil.btaddrtochars(self.__serveraddr[0]),
                    self.__serveraddr[1], self.__delegate)
        if self.__obexsession is not None:
            self.__client.performSelector_withObject_("setOBEXSession:",
                    self.__obexsession)
    
        self.__reset()
        headerset = _headersdicttoset(headers)        
        r = self.__client.sendConnectRequestWithHeaders_(headerset)
        if r != _kOBEXSuccess:
            self.__closetransport()
            raise OBEXError(r, "error starting Connect request (%s)" %
                    errdesc(r))
            
        _macutil.waituntil(self._done)
        if self.__error != _kOBEXSuccess:
            self.__closetransport()
            raise OBEXError(self.__error, "error during Connect request (%s)" % 
                    errdesc(self.__error))
               
        resp = self.__getresponse()               
        if resp.code != _obexcommon.OK:
            self.__closetransport()
        return resp
        
        
    def disconnect(self, headers={}):
        self.__checkconnected()
        self.__reset()
        try:
            headerset = _headersdicttoset(headers)       
            r = self.__client.sendDisconnectRequestWithHeaders_(headerset)
            if r != _kOBEXSuccess:
                raise OBEXError(r, "error starting Disconnect request (%s)" %
                        errdesc(r))
                
            _macutil.waituntil(self._done)
            if self.__error != _kOBEXSuccess:
                raise OBEXError(self.__error, 
                        "error during Disconnect request (%s)" % 
                        errdesc(self.__error))
        finally:
            # close channel regardless of disconnect result
            self.__closetransport()
        return self.__getresponse()


    def put(self, headers, fileobj):
        if not hasattr(fileobj, "read"):
            raise TypeError("file-like object must have read() method")       
        self.__checkconnected()            
        self.__reset()
        
        headerset = _headersdicttoset(headers)
        self.fileobj = fileobj
        self.__fileobjdelegate = _macutil.BBFileLikeObjectReader.alloc().initWithFileLikeObject_(fileobj)
        self.instream = BBStreamingInputStream.alloc().initWithDelegate_(self.__fileobjdelegate)
        self.instream.open()
        r = self.__client.sendPutRequestWithHeaders_readFromStream_(
                headerset, self.instream)
        if r != _kOBEXSuccess:
            raise OBEXError(r, "error starting Put request (%s)" % errdesc(r))
        _macutil.waituntil(self._done)
        if self.__error != _kOBEXSuccess:
            raise OBEXError(self.__error, "error during Put request (%s)" %
                    errdesc(self.__error))
        return self.__getresponse()
        
        
    def delete(self, headers):      
        self.__checkconnected()
        self.__reset()
        headerset = _headersdicttoset(headers)
        r = self.__client.sendPutRequestWithHeaders_readFromStream_(headerset,
                None)
        if r != _kOBEXSuccess:
            raise OBEXError(r, "error starting Delete request (%s)" %
                    errdesc(r))
            
        _macutil.waituntil(self._done)
        if self.__error != _kOBEXSuccess:
            raise OBEXError(self.__error, "error during Delete request (%s)" %
                    errdesc(self.__error))
        return self.__getresponse()
        
        
    def get(self, headers, fileobj):
        if not hasattr(fileobj, "write"):
            raise TypeError("file-like object must have write() method")
            
        self.__checkconnected()
        self.__reset()
        headerset = _headersdicttoset(headers)
        delegate = _macutil.BBFileLikeObjectWriter.alloc().initWithFileLikeObject_(fileobj)
        outstream = BBStreamingOutputStream.alloc().initWithDelegate_(delegate)
        outstream.open()            
        r = self.__client.sendGetRequestWithHeaders_writeToStream_(
                headerset, outstream)
        if r != _kOBEXSuccess:
            raise OBEXError(r, "error starting Get request (%s)" % errdesc(r))

        _macutil.waituntil(self._done)
        if self.__error != _kOBEXSuccess:
            raise OBEXError(self.__error, "error during Get request (%s)" %
                    errdesc(self.__error))
        return self.__getresponse()
        
        
    def setpath(self, headers, cdtoparent=False, createdirs=False):
        self.__checkconnected()
        self.__reset()
        headerset = _headersdicttoset(headers)       
        r = self.__client.sendSetPathRequestWithHeaders_changeToParentDirectoryFirst_createDirectoriesIfNeeded_(headerset, cdtoparent, createdirs)
        if r != _kOBEXSuccess:
            raise OBEXError(r, "error starting SetPath request (%s)" %
                    errdesc(r))

        _macutil.waituntil(self._done)
        if self.__error != _kOBEXSuccess:
            raise OBEXError(self.__error, "error during SetPath request (%s)" %
                    errdesc(self.__error))
        return self.__getresponse()        
                

    def _done(self):
        return not self.__busy
        
    def _finishedrequest(self, error, response):
        if error in (_kOBEXSessionNoTransportError,
                     _kOBEXSessionTransportDiedError):
            self.__closetransport()
        self.__error = error
        self.__response = response
        self.__busy = False
        _macutil.interruptwait()
        
    def _setobexsession(self, session):
        self.__obexsession = session
        
    # Note that OBEXSession returns kOBEXSessionNotConnectedError if you don't
    # send CONNECT before sending any other requests; this means the OBEXClient
    # must send connect() before other requests, so this restriction is enforced
    # in the Linux version as well, for consistency.
    def __checkconnected(self):
        if self.__client is None:
            raise OBEXError(_kOBEXSessionNotConnectedError, 
                    "must connect() before sending other requests")        

    def __closetransport(self):
        if self.__client is not None:
            try:
                self.__client.RFCOMMChannel().closeChannel()
                self.__client.RFCOMMChannel().getDevice().closeConnection()
            except:
                pass
        self.__client = None                
        
    def __reset(self):
        self.__busy = True
        self.__error = None
        self.__response = None

    def __getresponse(self):
        code = self.__response.responseCode()
        rawheaders = _headersettodict(self.__response.allHeaders())
        return _obexcommon.OBEXResponse(_cutresponsefinalbit(code), rawheaders)

    # set method docstrings
    definedmethods = locals()   # i.e. defined methods in OBEXClient
    for name, doc in _obexcommon._obexclientdocs.items():
        try:
            definedmethods[name].__doc__ = doc
        except KeyError:
            pass                                             
        

        
class _BBOBEXClientDelegate(NSObject):

    def initWithCallback_(self, cb_requestdone):
        self = super(_BBOBEXClientDelegate, self).init()
        self._cb_requestdone = cb_requestdone
        return self
    initWithCallback_ = objc.selector(initWithCallback_, signature="@@:@")

    #
    # Delegate methods follow. objc signatures for all methods must be set 
    # using objc.selector or else may get bus error.
    #        
        
    # - (void)client:(BBBluetoothOBEXClient *)client 
    #    didFinishConnectRequestWithError:(OBEXError)error 
    #       response:(BBOBEXResponse *)response;
    def client_didFinishConnectRequestWithError_response_(self, client, error,
            response):
        self._cb_requestdone(error, response)
    client_didFinishConnectRequestWithError_response_ = objc.selector(
        client_didFinishConnectRequestWithError_response_, signature="v@:@i@")
            
    # - (void)client:(BBBluetoothOBEXClient *)client 
    #    didFinishDisconnectRequestWithError:(OBEXError)error 
    #       response:(BBOBEXResponse *)response;
    def client_didFinishDisconnectRequestWithError_response_(self, client,
            error, response):
        self._cb_requestdone(error, response)
    client_didFinishDisconnectRequestWithError_response_ = objc.selector(
        client_didFinishDisconnectRequestWithError_response_,signature="v@:@i@")
            
    # - (void)client:(BBBluetoothOBEXClient *)client
    # didFinishPutRequestForStream:(NSInputStream *)inputStream
    #         error:(OBEXError)error
    #       response:(BBOBEXResponse *)response;
    def client_didFinishPutRequestForStream_error_response_(self, client,
            instream, error, response):
        self._cb_requestdone(error, response)
    client_didFinishPutRequestForStream_error_response_ = objc.selector(
        client_didFinishPutRequestForStream_error_response_,signature="v@:@@i@")
            
    # - (void)client:(BBBluetoothOBEXClient *)client
    # didFinishGetRequestForStream:(NSOutputStream *)outputStream
    #         error:(OBEXError)error
    #       response:(BBOBEXResponse *)response;
    def client_didFinishGetRequestForStream_error_response_(self, client,
            outstream, error, response):
        self._cb_requestdone(error, response)
    client_didFinishGetRequestForStream_error_response_ = objc.selector(
        client_didFinishGetRequestForStream_error_response_,signature="v@:@@i@")
            
    # - (void)client:(BBBluetoothOBEXClient *)client
    #    didFinishSetPathRequestWithError:(OBEXError)error 
    #       response:(BBOBEXResponse *)response;
    def client_didFinishSetPathRequestWithError_response_(self, client, error,
            response):
        self._cb_requestdone(error, response)
    client_didFinishSetPathRequestWithError_response_ = objc.selector(
        client_didFinishSetPathRequestWithError_response_, signature="v@:@i@")
            
    # client:didAbortRequestWithStream:error:response: not
    # implemented since OBEXClient does not allow abort requests
            
            
# ------------------------------------------------------------------
            
def sendfile(address, channel, source):
    if not _lightbluecommon._isbtaddr(address):  
        raise TypeError("address '%s' is not a valid bluetooth address" %
                address)
    if not isinstance(channel, int):
        raise TypeError("channel must be int, was %s" % type(channel))
    if not isinstance(source, types.StringTypes) and \
            not hasattr(source, "read"):
        raise TypeError("source must be string or file-like object with read() method")
              
    if isinstance(source, types.StringTypes):
        headers = {"name": source}
        fileobj = file(source, "rb")
        closefileobj = True                    
    else:
        if hasattr(source, "name"):
            headers = {"name": source.name}
        fileobj = source
        closefileobj = False                
        
    client = OBEXClient(address, channel)
    client.connect()        
    
    try:
        resp = client.put(headers, fileobj)
    finally:
        if closefileobj:
            fileobj.close()
        try:
            client.disconnect()
        except:
            pass    # always ignore disconnection errors                

    if resp.code != _obexcommon.OK:
        raise OBEXError("server denied the Put request")


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


class BBOBEXObjectPushServer(NSObject):

    def initWithChannel_fileLikeObject_(self, channel, fileobject):
        if not isinstance(channel, IOBluetoothRFCOMMChannel) and \
                not isinstance(channel, OBEXSession):
            raise TypeError("internal error, channel is of wrong type %s" %
                    type(channel))
        if not hasattr(fileobject, "write"):
            raise TypeError("fileobject must be file-like object with write() method")
    
        self = super(BBOBEXObjectPushServer, self).init()
        self.__fileobject = fileobject
        self.__server = BBBluetoothOBEXServer.alloc().initWithIncomingRFCOMMChannel_delegate_(channel, self)
        #BBBluetoothOBEXServer.setDebug_(True)
        
        self.__error = None
        self.__gotfile = False
        self.__gotdisconnect = False
        self.__disconnected = False
        
        # for internal testing
        if isinstance(channel, OBEXSession):
            self.__server.performSelector_withObject_("setOBEXSession:",
                    channel)
        return self
    initWithChannel_fileLikeObject_ = objc.selector(
        initWithChannel_fileLikeObject_, signature="@@:i@")
        
        
    def run(self):
        self.__server.run()
    
        # wait until client sends a file, or an error occurs
        _macutil.waituntil(lambda: self.__gotfile or self.__error is not None)
        
        # wait briefly for a disconnect request (client may have decided to just
        # close the connection without sending a disconnect request)
        if self.__error is None:
            ok = _macutil.waituntil(lambda: self.__gotdisconnect, 3)
            if ok:
                _macutil.waituntil(lambda: self.__disconnected)
            
        # only raise OBEXError if file was not received
        if not self.__gotfile:
            if self.__error is not None:
                raise OBEXError(self.__error[0], self.__error[1])
                
            # if client connected but didn't send PUT
            raise OBEXError(_kOBEXGeneralError, "client did not send a file")
        
    #
    # BBBluetoothOBEXClientDelegate methods follow.
    # These enable this class to get callbacks when some event occurs on the
    # server (e.g. got a new client request, or an error occurred, etc.).
    #
        
    # - (BOOL)server:(BBBluetoothOBEXServer *)server
    # shouldHandleConnectRequest:(BBOBEXHeaderSet *)requestHeaders;        
    def server_shouldHandleConnectRequest_(self, server, requestheaders):
        return True
    server_shouldHandleConnectRequest_ = objc.selector(   
        server_shouldHandleConnectRequest_, signature="c@:@@")        
    
    # - (BOOL)server:(BBBluetoothOBEXServer *)server
    # shouldHandleDisconnectRequest:(BBOBEXHeaderSet *)requestHeaders;    
    def server_shouldHandleDisconnectRequest_(self, server, requestheaders):
        self.__gotdisconnect = True    
        _macutil.interruptwait()
        return True
    server_shouldHandleDisconnectRequest_ = objc.selector(   
        server_shouldHandleDisconnectRequest_, signature="c@:@@")
        
    # - (void)serverDidHandleDisconnectRequest:(BBBluetoothOBEXServer *)server;
    def serverDidHandleDisconnectRequest_(self, server):
        self.__disconnected = True
        _macutil.interruptwait()
    serverDidHandleDisconnectRequest_ = objc.selector(
        serverDidHandleDisconnectRequest_, signature="v@:@")
        
    # - (NSOutputStream *)server:(BBBluetoothOBEXServer *)server
    # shouldHandlePutRequest:(BBOBEXHeaderSet *)requestHeaders;        
    def server_shouldHandlePutRequest_(self, server, requestheaders):
        #print "Incoming file:", requestHeaders.valueForNameHeader()
        self.delegate = _macutil.BBFileLikeObjectWriter.alloc().initWithFileLikeObject_(self.__fileobject)
        outstream = BBStreamingOutputStream.alloc().initWithDelegate_(self.delegate)
        outstream.open()
        return outstream
    server_shouldHandlePutRequest_ = objc.selector(   
        server_shouldHandlePutRequest_, signature="@@:@@")
        
    # - (void)server:(BBBluetoothOBEXServer *)server
    # didHandlePutRequestForStream:(NSOutputStream *)outputStream
    #   requestWasAborted:(BOOL)aborted;        
    def server_didHandlePutRequestForStream_requestWasAborted_(self, server,
            stream, aborted):
        if aborted:
            self.__error = (_kOBEXGeneralError, "client aborted file transfer")
        else:
            self.__gotfile = True
        _macutil.interruptwait()
    server_didHandlePutRequestForStream_requestWasAborted_ = objc.selector(
        server_didHandlePutRequestForStream_requestWasAborted_, 
        signature="v@:@@c")        
        
    # - (void)server:(BBBluetoothOBEXServer *)server
    # errorOccurred:(OBEXError)error
    #   description:(NSString *)description;        
    def server_errorOccurred_description_(self, server, error, desc):
        self.__error = (error, desc)
        _macutil.interruptwait()
    server_errorOccurred_description_ = objc.selector(
        server_errorOccurred_description_, signature="v@:@i@")        



# ------------------------------------------------------------------              
    
def recvfile(sock, dest):
    if sock is None:
        raise TypeError("Given socket is None")
    if not isinstance(dest, (types.StringTypes, types.FileType)):
        raise TypeError("dest must be string or file-like object with write() method")
        
    if isinstance(dest, types.StringTypes):
        fileobj = open(dest, "wb")
        closefileobj = True
    else:
        fileobj = dest
        closefileobj = False
        
    try:
        sock.listen(1)
        conn, addr = sock.accept()
        #print "A client connected:", addr
        server = BBOBEXObjectPushServer.alloc().initWithChannel_fileLikeObject_(
                conn._getchannel(), fileobj)            
        server.run()
        conn.close()
    finally:
        if closefileobj:
            fileobj.close()
