#!/usr/bin/python3

# this script is a mix between simple-agent from bluez5 and bt-pan from
# http://blog.fraggod.net/2015/03/28/bluetooth-pan-network-setup-with-bluez-5x.html

from optparse import OptionParser
import os
import sys
import signal
import dbus
import dbus.service
import dbus.mainloop.glib
import gobject as GObject

DEBUG = bool(os.getenv("BTPAN_DEBUG"))

iface_base    = 'org.bluez'
iface_adapter = '{}.Adapter1'.format(iface_base)
iface_agent   = '{}.Agent1'.format(iface_base)
iface_dev     = '{}.Device1'.format(iface_base)
iface_props   = 'org.freedesktop.DBus.Properties'

connected_status_file = '/tmp/bluetooth-status'

class BTError(Exception): pass

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

def get_bus():
    bus = getattr(get_bus, 'cached_obj', None)
    if not bus:
        bus = get_bus.cached_obj = dbus.SystemBus()
    return bus

def get_manager():
    manager = getattr(get_manager, 'cached_obj', None)
    if not manager:
        manager = get_manager.cached_obj = dbus.Interface(get_bus().get_object(iface_base, '/'),
                                                          'org.freedesktop.DBus.ObjectManager' )
    return manager

def prop_get(obj, k, iface=None):
    if iface is None:
        iface = obj.dbus_interface
    return obj.Get(iface, k, dbus_interface=iface_props)

def prop_set(obj, k, v, iface=None):
    if iface is None:
        iface = obj.dbus_interface
    return obj.Set(iface, k, v, dbus_interface=iface_props)

def find_adapter_in_objects(objects, pattern=None):
    bus = get_bus()
    for path, ifaces in objects.items():
        adapter = ifaces.get(iface_adapter)
        if adapter is None:
            continue
        if not pattern or pattern == adapter['Address'] or path.endswith(pattern):
            obj = bus.get_object(iface_base, path)
            return dbus.Interface(obj, iface_adapter)

    raise BTError('Bluetooth adapter not found')

def find_adapter(pattern=None):
    return find_adapter_in_objects(get_manager().GetManagedObjects(), pattern)

def find_device_in_objects(objects, device_address, adapter_pattern=None):
    bus = get_bus()
    path_prefix = ''

    if adapter_pattern:
        if not isinstance(adapter_pattern, str):
            adapter = adapter_pattern
        else:
            adapter = find_adapter_in_objects(objects, adapter_pattern)
        path_prefix = adapter.object_path

    for path, ifaces in objects.items():
        device = ifaces.get(iface_dev)
        if device is None:
            continue
        if device['Address'] == device_address and path.startswith(path_prefix):
            obj = bus.get_object(iface_base, path)
            return dbus.Interface(obj, iface_dev)

    raise BTError('Bluetooth device not found')

def find_device(device_address, adapter_pattern=None):
    return find_device_in_objects(get_manager().GetManagedObjects(), device_address, adapter_pattern)

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

class Rejected(dbus.DBusException):
    _dbus_error_name = "org.bluez.Error.Rejected"

class Agent(dbus.service.Object):
    exit_on_release = False

    def __init__(self, bus, path):
        dbus.service.Object.__init__(self, bus, path)

        self.bus  = bus
        self.path = path

        self.connectedDevs = []
        self.discoverable = True

        # check if last change was trust.
        # trust => (modalias + paired) means paired
        self.trustedBefore = False

        bus.add_signal_receiver(self.PropertiesChanged,
                                dbus_interface="org.freedesktop.DBus.Properties",
                                signal_name="PropertiesChanged",
                                path_keyword="path")

    @dbus.service.method(iface_agent, in_signature="", out_signature="")
    def Release(self):
        if DEBUG: print("Release")
        if self.exit_on_release:
            mainloop.quit()

    @dbus.service.method(iface_agent, in_signature="os", out_signature="")
    def AuthorizeService(self, device, uuid):
        if DEBUG: print("AuthorizeService", device, uuid)
        self.set_trusted(device)

    @dbus.service.method(iface_agent, in_signature="o", out_signature="s")
    def RequestPinCode(self, device):
        if DEBUG: print("RequestPinCode", device)
        self.set_trusted(device)
        return "0000"

    @dbus.service.method(iface_agent, in_signature="o", out_signature="u")
    def RequestPasskey(self, device):
        if DEBUG: print("RequestPasskey", device)
        self.set_trusted(device)
        return dbus.UInt32("0000")

    @dbus.service.method(iface_agent, in_signature="ouq", out_signature="")
    def DisplayPasskey(self, device, passkey, entered):
        if DEBUG: print("DisplayPasskey (%s, %06u entered %u)" % (device, passkey, entered))

    @dbus.service.method(iface_agent, in_signature="os", out_signature="")
    def DisplayPinCode(self, device, pincode):
        if DEBUG: print("DisplayPinCode (%s, %s)" % (device, pincode))

    @dbus.service.method(iface_agent, in_signature="ou", out_signature="")
    def RequestConfirmation(self, device, passkey):
        if DEBUG: print("RequestConfirmation", device)
        self.set_trusted(device)

    @dbus.service.method(iface_agent, in_signature="o", out_signature="")
    def RequestAuthorization(self, device):
        if DEBUG: print("RequestAuthorization", device)
        self.set_trusted(device)

    @dbus.service.method(iface_agent, in_signature="", out_signature="")
    def Cancel(self):
        if DEBUG: print("Cancel")

    def PropertiesChanged(self, interface, changed, invalidated, path):
        if DEBUG: print("PropertiesChanged", interface, changed, path)

        if interface == iface_adapter:
            if "Discoverable" not in changed.keys():
                return

            self.discoverable = bool(changed["Discoverable"])

        elif interface == iface_dev:
            keys = changed.keys()

            if "Trusted" in keys:
                self.trustedBefore = bool(changed["Trusted"])
                return

            if self.trustedBefore and "Modalias" in keys and "Paired" in keys and "UUIDs" in keys and changed["Paired"]:
                self.trustedBefore = False
                # keep going
                changed["Connected"] = True

            if "Connected" not in keys:
                return

            if changed["Connected"]:
                if path not in self.connectedDevs:
                    self.connectedDevs.append(path)
            elif path in self.connectedDevs:
                self.connectedDevs.remove(path)

        else:
            return

        if len(self.connectedDevs) > 0:
            status = "Connected"
            # NOTE: python threading module does not work, had to resort to this
            os.system("/sbin/bt-pan-restart-helper &")
        elif self.discoverable:
            status = "Discoverable"
        else:
            status = "Disconnected"

        if DEBUG: print(status)
        with open(connected_status_file, 'w') as fh:
            fh.write(status+"\n")

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

    def ask(self, prompt):
        if DEBUG: print("ask", prompt)
        return input(prompt)

    def set_trusted(self, path):
        if DEBUG: print("set_trusted", path)
        props = dbus.Interface(self.bus.get_object(iface_base, path), "org.freedesktop.DBus.Properties")
        props.Set(iface_dev, "Trusted", True)

    def dev_connect(self, path):
        if DEBUG: print("dev_connect", path)
        dev = dbus.Interface(self.bus.get_object(iface_base, path), iface_dev)
        dev.Connect()

    def set_exit_on_release(self, exit_on_release):
        self.exit_on_release = exit_on_release

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

if __name__ == '__main__':
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    signal.signal(signal.SIGTERM, lambda sig,frm: sys.exit(0))

    dev_local = find_adapter()
    prop_set(dev_local, 'Powered', True)
    #prop_set(dev_local, 'PairableTimeout', dbus.UInt32(0))
    prop_set(dev_local, 'Pairable', True)
    prop_set(dev_local, 'DiscoverableTimeout', dbus.UInt32(60*2))
    prop_set(dev_local, 'Discoverable', False)

    server = dbus.Interface(dev_local, 'org.bluez.NetworkServer1')
    server.Register("nap", "bnep")

    bus = dbus.SystemBus()

    path = "/test/agent"
    agent = Agent(bus, path)

    mainloop = GObject.MainLoop()

    obj = bus.get_object(iface_base, "/org/bluez");

    manager = dbus.Interface(obj, "org.bluez.AgentManager1")
    manager.RegisterAgent(path, "NoInputNoOutput")
    manager.RequestDefaultAgent(path)

    with open(connected_status_file, 'w') as fh:
        fh.write("Disconnected\n")

    # wtf, registering the dbus server seems to change the BT class..
    os.system("hciconfig hci0 class 0x822310")

    mainloop.run()

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