# Copyright (c) 2009 Bea Lam. All rights reserved.
#
# This file is part of LightBlue.
#
# LightBlue is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# LightBlue is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with LightBlue.  If not, see <http://www.gnu.org/licenses/>.

# Mac OS X main module implementation.

import types
import warnings

import Foundation
import AppKit
import objc

import _IOBluetooth
import _LightAquaBlue
import _lightbluecommon
import _macutil
import _bluetoothsockets


# public attributes
__all__ = ("finddevices", "findservices", "finddevicename", 
           "selectdevice", "selectservice",
           "gethostaddr", "gethostclass",
           "socket", 
           "advertise", "stopadvertise",
           "pair", "unpair")

# details of advertised services
__advertised = {}

def pair(addr):
    """
    pair(addr) -> (pairing_code, error)
    
    Initiates a pairing procedure with a device based its 
    device address, e.g. '00:18:33:5B:34:24'.

    Returns a tuple (pairing code, potential error code or None).
    """
    unpair(addr)
    try:
        p = _AsyncPair.alloc().initWithAddress_(addr)
        return p.start()
    except:
        return (0, -1)

def unpair(addr):
    device = _IOBluetooth.IOBluetoothDevice.withAddressString_(addr)
    if (device):
        device.remove() # Note: -remove is a private method...

def finddevices(getnames=True, timeout=10):
    inquiry = _SyncDeviceInquiry()
    inquiry.run(getnames, timeout)
    devices = inquiry.getfounddevices()
    return devices


def findservices(addr=None, name=None, servicetype=None):
    if servicetype not in (_lightbluecommon.RFCOMM, _lightbluecommon.OBEX, None):
        raise ValueError("servicetype must be RFCOMM, OBEX or None, was %s" % \
            servicetype)

    if addr is None:
        try:
            founddevices = finddevices()
        except _lightbluecommon.BluetoothError, e:
            msg = "findservices() failed, " +\
                    "error while finding devices: " + str(e)
            raise _lightbluecommon.BluetoothError(msg)
            
        #print founddevices
        addresses = [dev[0] for dev in founddevices]
    else:
        addresses = [addr]

    services = []
    for devaddr in addresses:
        iobtdevice = _IOBluetooth.IOBluetoothDevice.withAddressString_(devaddr)
            
        try:
            lastseen = iobtdevice.getLastServicesUpdate()
            if lastseen is None or lastseen.timeIntervalSinceNow() < -2:
                # perform SDP query to update known services.
                # wait at least a few seconds between service discovery cos 
                # sometimes it doesn't work if doing updates too often.
                # In future should have option to not do updates.
                serviceupdater = _SDPQueryRunner.alloc().init()
                try:
                    serviceupdater.query(iobtdevice)  # blocks until updated
                except _lightbluecommon.BluetoothError, e:
                    msg = "findservices() couldn't get services for %s: %s" % \
                        (iobtdevice.getNameOrAddress(), str(e))
                    warnings.warn(msg)
                    # or should I use cached services instead of warning?
                    # but sometimes the cached ones are totally wrong.
                    
            # if searching for RFCOMM, exclude OBEX services
            if servicetype == _lightbluecommon.RFCOMM:
                uuidbad = _macutil.PROTO_UUIDS.get(_lightbluecommon.OBEX)
            else:
                uuidbad = None
                    
            filtered = _searchservices(iobtdevice, name=name, 
                uuid=_macutil.PROTO_UUIDS.get(servicetype), 
                uuidbad=uuidbad)
            
            #print "unfiltered:", iobtdevice.getServices()
            services.extend([_getservicetuple(s) for s in filtered])
        finally:            
            # close baseband connection (not sure if this is necessary, but 
            # sometimes the transport connection seems to stay open?)
            iobtdevice.closeConnection()
        
    return services


def finddevicename(address, usecache=True):
    if not _lightbluecommon._isbtaddr(address):
        raise TypeError("%s is not a valid bluetooth address" % str(address))

    if address == gethostaddr():
        return _gethostname()

    device = _IOBluetooth.IOBluetoothDevice.withAddressString_(address)
    if usecache:
        name = device.getName()
        if name is not None:
            return name
    
    # do name request with timeout of 10 seconds    
    result = device.remoteNameRequest_withPageTimeout_(None, 10000)
    if result == _macutil.kIOReturnSuccess:
        return device.getName()
    raise _lightbluecommon.BluetoothError(
        "Could not find device name for %s" % address)      


### local device ###

def gethostaddr():
    addr = _LightAquaBlue.BBLocalDevice.getAddressString()
    if addr is not None:
        # PyObjC returns all strings as unicode, but the address doesn
't need 
        # to be unicode cos it's just hex values
        return _macutil.formatdevaddr(addr)
    raise _lightbluecommon.BluetoothError("Cannot read local device address")                            


def gethostclass():
    cod = _LightAquaBlue.BBLocalDevice.getClassOfDevice()
    if cod != -1:
        return int(cod)
  
  raise _lightbluecommon.BluetoothError("Cannot read local device class")                            


def _gethostname():
    name = _LightAquaBlue.BBLocalDevice.getName()
    if name is not None:
        return name
    raise _lightbluecommon.BluetoothError("Cannot read local device name")                            
    
        
### socket ###

def socket(proto=_lightbluecommon.RFCOMM):
    return _bluetoothsockets._getsocketobject(proto)
        
### advertising services ###


def advertise(name, sock, servicetype):
    if not isinstance(name, types.StringTypes):
        raise TypeError("name must be string, was %s" % \
            type(name))
        
    # raises exception if socket is not bound
    boundchannelID = sock._getport()
    
    # advertise the service
    if servicetype == _lightbluecommon.RFCOMM:
        try:
            result, finalchannelID, servicerecordhandle = _LightAquaBlue.BBServiceAdvertiser.addRFCOMMServiceDictionary_withName_UUID_channelID_serviceRecordHandle_(
                _LightAquaBlue.BBServiceAdvertiser.serialPortProfileDictionary(), 
                name, 
                None, None, None)
        except:
            result, finalchannelID, servicerecordhandle = _LightAquaBlue.BBServiceAdvertiser.addRFCOMMServiceDictionary_withName_UUID_channelID_serviceRecordHandle_(
                _LightAquaBlue.BBServiceAdvertiser.serialPortProfileDictionary(), 
                name, 
                None)       
    elif servicetype == _lightbluecommon.OBEX:
        try:
            result, finalchannelID, servicerecordhandle = _LightAquaBlue.BBServiceAdvertiser.addRFCOMMServiceDictionary_withName_UUID_channelID_serviceRecordHandle_(
                _LightAquaBlue.BBServiceAdvertiser.objectPushProfileDictionary(), 
                name, 
                None, None, None)
        except:
            result, finalchannelID, servicerecordhandle = _LightAquaBlue.BBServiceAdvertiser.addRFCOMMServiceDictionary_withName_UUID_channelID_serviceRecordHandle_(
                _LightAquaBlue.BBServiceAdvertiser.objectPushProfileDictionary(), 
                name, 
                None)       
    else:
        raise ValueError("servicetype must be either RFCOMM or OBEX")

    if result != _macutil.kIOReturnSuccess:
        raise _lightbluecommon.BluetoothError(
            result, "Error 