from Tkinter import *
import os
import sys
import dbus
import dbus.service
import dbus.mainloop.glib
from bluetooth import *
import xml.etree.ElementTree as ET

import gtk
from dbus.mainloop.glib import DBusGMainLoop
from Queue import Empty

import subprocess
import multiprocessing

from keyboard.keyboard_client import Keyboard
from mouse.mouse_client import Mouse

#####################################################################################################

#
# define a bluez 5 profile object for our keyboard/mouse
#
class BluetoothBluezProfile(dbus.service.Object):
    fd = -1

    @dbus.service.method("org.bluez.Profile1",
                         in_signature="", out_signature="")
    def Release(self):
        print("Release")
        mainloop.quit()

    @dbus.service.method("org.bluez.Profile1",
                         in_signature="", out_signature="")
    def Cancel(self):
        print("Cancel")

    @dbus.service.method("org.bluez.Profile1", in_signature="oha{sv}", out_signature="")
    def NewConnection(self, path, fd, properties):
        self.fd = fd.take()
        print("NewConnection(%s, %d)" % (path, self.fd))
        for key in properties.keys():
            if key == "Version" or key == "Features":
                print("  %s = 0x%04x" % (key, properties[key]))
            else:
                print("  %s = %s" % (key, properties[key]))

    @dbus.service.method("org.bluez.Profile1", in_signature="o", out_signature="")
    def RequestDisconnection(self, path):
        print("RequestDisconnection(%s)" % (path))

        if (self.fd > 0):
            os.close(self.fd)
            self.fd = -1

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


#
# create a bluetooth device to emulate a HID keyboard/mouse,
# advertize a SDP record using our bluez profile class
#
class BluetoothDevice():
    # change these constants
    MY_ADDRESS = "B8:27:EB:B6:8C:21"
    MY_DEV_NAME = "Bluetooth_Keyboard/Mouse"

    # define some constants
    P_CTRL = 17  # Service port - must match port configured in SDP record
    P_INTR = 19  # Service port - must match port configured in SDP record #Interrrupt port
    PROFILE_DBUS_PATH = "/bluez/upwork/hidbluetooth_profile"  # dbus path of  the bluez profile we will create
    SDP_RECORD_PATH = sys.path[0] + "/sdp_record.xml"  # file path of the sdp record to load
    UUID = "00001124-0000-1000-8000-00805f9b34fb"

    def __init__(self):

        print("Setting up Bluetooth device")

        self.init_bt_device()
        self.init_bluez_profile()

    # configure the bluetooth hardware device
    def init_bt_device(self):

        print("Configuring for name " + BluetoothDevice.MY_DEV_NAME)

        # set the device class to a keybord/mouse combo and set the name
        # os.system("sudo hciconfig hcio class 0x25C0") # Keyboard/Mouse Combo in Limited Discoverable Mode
        os.system("sudo hciconfig hcio class 0x05C0")  # Keyboard/Mouse Combo in General Discoverable Mode
        os.system("sudo hciconfig hcio name " + BluetoothDevice.MY_DEV_NAME)

        # make the device discoverable
        os.system("sudo hciconfig hcio piscan")

    # set up a bluez profile to advertise device capabilities from a loaded service record
    def init_bluez_profile(self):

        print("Configuring Bluez Profile")

        # read service record from a file
        print("Reading service record")

        with open(BluetoothDevice.SDP_RECORD_PATH, "r") as fh:
            service_record = fh.read()

        if not service_record:
            sys.exit("Could not open the sdp record. Exiting...")

        # setup profile options
        opts = {
            "ServiceRecord": service_record,
            "Role": "server",
            "RequireAuthentication": False,
            "RequireAuthorization": False
        }

        # retrieve a proxy for the bluez profile interface
        bus = dbus.SystemBus()
        manager = dbus.Interface(bus.get_object("org.bluez", "/org/bluez"), "org.bluez.ProfileManager1")

        profile = BluetoothBluezProfile(bus, BluetoothDevice.PROFILE_DBUS_PATH)

        manager.RegisterProfile(BluetoothDevice.PROFILE_DBUS_PATH, BluetoothDevice.UUID, opts)

        print("Profile registered ")

    # listen for incoming client connections

    # ideally this would be handled by the Bluez 5 profile
    # but that didn't seem to work
    def listen(self):

        print("Waiting for connections")
        self.scontrol = BluetoothSocket(L2CAP)
        self.sinterrupt = BluetoothSocket(L2CAP)

        # bind these sockets to a port - port zero to select next available
        self.scontrol.bind((self.MY_ADDRESS, self.P_CTRL))
        self.sinterrupt.bind((self.MY_ADDRESS, self.P_INTR))

        # Start listening on the server sockets
        self.scontrol.listen(1)  # Limit of 1 connection
        self.sinterrupt.listen(1)

        self.ccontrol, cinfo = self.scontrol.accept()
        print("Got a connection on the control channel from " + cinfo[0])

        self.cinterrupt, cinfo = self.sinterrupt.accept()
        print("Got a connection on the interrupt channel from " + cinfo[0])

        global connection_status_queue
        connection_status_queue.put("Connected")

    # send a string to the bluetooth host machine
    def send_string(self, message):
        try:
            self.cinterrupt.send(message)
        except:
            self.close()
            self.listen()

    def close(self):
        global connection_status_queue
        connection_status_queue.put("Disconnected")
        self.scontrol.close()
        self.sinterrupt.close()


# define a dbus service that emulates a bluetooth keyboard and mouse
# this will enable different clients to connect to and use
# the service
class BluetoothService(dbus.service.Object):
    def __init__(self):

        print("Setting up service")

        # set up as a dbus service
        bus_name = dbus.service.BusName("org.upwork.HidBluetoothService", bus=dbus.SystemBus())
        dbus.service.Object.__init__(self, bus_name, "/org/upwork/HidBluetoothService")

        # create and setup our device
        self.device = BluetoothDevice()

        # start listening for connections
        self.device.listen()

    @dbus.service.method('org.upwork.HidBluetoothService', in_signature='yay')
    def send_keys(self, modifier_byte, keys):

        cmd_str = ""
        cmd_str += chr(0xA1)
        cmd_str += chr(0x01)
        cmd_str += chr(modifier_byte)
        cmd_str += chr(0x00)

        count = 0
        for key_code in keys:
            if count < 6:
                cmd_str += chr(key_code)
            count += 1

        self.device.send_string(cmd_str)

    @dbus.service.method('org.upwork.HidBluetoothService', in_signature='iai')
    def send_mouse(self, buttons, rel_move):

        cmd_str = ""
        cmd_str += chr(0xA1)
        cmd_str += chr(0x02)
        cmd_str += chr(buttons)
        cmd_str += chr(rel_move[0])
        cmd_str += chr(rel_move[1])
        cmd_str += chr(rel_move[2])

        self.device.send_string(cmd_str)

    @dbus.service.method('org.freedesktop.DBus.Introspectable', out_signature='s')
    def Introspect(self):
        return ET.tostring(ET.parse(os.getcwd() + '/org.upwork.hidbluetooth.introspection').getroot(), encoding='utf8',
                           method='xml')

    def close(self):
        try:
            self.device.close()
        except:
            pass


#####################################################################################################


class App(Frame):
    """
    GUI Class
    """

    def __init__(self, master=None, width=200, height=400, background="white"):
        Frame.__init__(self, master, width=width, height=height, bg=background)
        self.pack(side="top", fill=BOTH, expand=True)

        self.buttons_frame = Frame(self, bg="grey")
        self.buttons_frame.pack(side="top", fill=X, expand=False)

        self.buttons_variable = IntVar()
        self.buttons_variable.set(0)

        self.radio_buttons = []

        for i in xrange(4):
            self.radio_buttons.append(
                Radiobutton(self.buttons_frame, variable=self.buttons_variable, activebackground="green", bg="white",
                            selectcolor="green", relief="sunken", command=self.change_screen, value=i, indicatoron=0))
            self.radio_buttons[i].pack(fill=X, expand=True, side=LEFT, padx=10, pady=10)

        self.inner_frame = Frame(self, bg=background)
        self.inner_frame.pack(side="top", fill=X, expand=True)
        self.inner_frame.grid_rowconfigure(0, weight=1)
        self.inner_frame.grid_columnconfigure(0, weight=1)

        self.pageOne = PageOne(self.inner_frame)
        self.pageOne.grid(row=0, column=0, sticky="nsew")

        self.pageTwo = PageTwo(self.inner_frame)
        self.pageTwo.grid(row=0, column=0, sticky="nsew")

        self.change_screen()

    def change_screen(self):
        index = self.buttons_variable.get()
        if index == 0:
            self.pageOne.tkraise()
        elif index == 1:
            self.pageTwo.tkraise()


#############################################################################################################


class BluetoothStatusLabel(Label):
    def __init__(self, master, bg="red"):
        Label.__init__(self, master, bg=bg)
        self.update_text()

    def update_text(self):
        if "UP" in subprocess.check_output("hciconfig hci0 | grep UP", shell=True):
            self.configure(bg="green", text="Enabled")
        else:
            self.configure(bg="red", text="Disabled")
        self.after(1000, self.update_text)


class ConnectionStatusLabel(Label):
    def __init__(self, master, bg="red"):
        Label.__init__(self, master, bg=bg)
        self.text = "Disconnected"
        self.update_text()

    def update_text(self):
        try:
            global connection_status_queue
            self.text = connection_status_queue.get(True, 0.1)
        except Empty:
            pass
        self.configure(text=self.text)
        if self.text == "Connected":
            self.configure(bg="green", text="Connected")
        else:
            self.configure(bg="red", text="Disconnected")
        self.after(1000, self.update_text)


class PageOne(Frame):
    def __init__(self, master, background="white"):
        Frame.__init__(self, master, bg=background)

        self.bluetooth_status = StringVar()
        self.connection_status = StringVar()

        self.bluetooth_status.set("Disabled")
        self.connection_status.set("Disconnected")

        self.frame1 = Frame(self, bg=background)
        self.frame1.pack(side="top", fill="both", expand=True)

        Label(self.frame1, text="Bluetooth Status: ", bg=background).pack(side=LEFT, padx=(10, 20), pady=10)

        BluetoothStatusLabel(self.frame1, bg="red").pack(fill=X,
                                                         expand=True,
                                                         side=LEFT,
                                                         padx=10,
                                                         pady=10)

        self.frame2 = Frame(self, bg=background)
        self.frame2.pack(side="top", fill="both", expand=True)

        Label(self.frame2, text="Connection Status: ", bg=background).pack(side=LEFT, padx=10, pady=10)

        ConnectionStatusLabel(self.frame2, bg="red").pack(fill=X,
                                                          expand=True,
                                                          side=LEFT,
                                                          padx=10,
                                                          pady=10)


##########################################################################################################################


class PageTwo(Frame):
    def __init__(self, master, background="white"):
        Frame.__init__(self, master, bg=background)

        self.container = Frame(self, bg=background)
        self.container.pack(side="top")

        self.buttons = []

        for i in xrange(3):
            for j in xrange(3):
                self.buttons.append(Button(self.container, text=str(i * 3 + j + 1)))
                self.buttons[i * 3 + j].bind("<ButtonPress-1>", self.on_press(i, j))
                self.buttons[i * 3 + j].bind("<ButtonRelease-1>", self.on_release)
                self.buttons[i * 3 + j].grid(row=i, column=j, padx=20, pady=20)

        self.bus = dbus.SystemBus()
        self.bluetoothservice = self.bus.get_object('org.upwork.HidBluetoothService', "/org/upwork/HidBluetoothService")
        self.iface = dbus.Interface(self.bluetoothservice, 'org.upwork.HidBluetoothService')

    def on_press(self, row, column):
        button_id = row * 3 + column + 1
        shift = 29

        def sender(event):
            print("button " + str(button_id) + " was pressed")
            self.iface.send_keys(0x00, [button_id + shift, 0x00, 0x00, 0x00, 0x00, 0x00])

        return sender

    def on_release(self, event):
        print("button was released")
        self.iface.send_keys(0x00, [0x00, 0x00, 0x00, 0x00, 0x00, 0x00])


def create_bluetooth_server_process():
    try:
        DBusGMainLoop(set_as_default=True)
        BluetoothService()
        gtk.main()
    finally:
        return

if __name__ == "__main__":
    # Variable used to update the user interface's Connection Status Label
    connection_status_queue = multiprocessing.Manager().Queue()
    connection_status_queue.put("Disconnected")

    # Bluetooth DBus Service that will receive inputs from Keyboard and Mouse and send them via Bluetooth
    bluetoothProcess = multiprocessing.Process(target=create_bluetooth_server_process)
    bluetoothProcess.start()

    # Keyboard process
    keyboardProcess = multiprocessing.Process(target=Keyboard)
    keyboardProcess.start()

    # Mouse process
    mouseProcess = multiprocessing.Process(target=Mouse)
    mouseProcess.start()

    # User Interface
    root = Tk()
    root.minsize(300, 400)
    root.maxsize(300, 400)
    main_application = App(root)

    print("Starting user interface main loop")
    main_application.mainloop()
    print("Exiting user interface main loop")

    # Terminate all processes
    keyboardProcess.terminate()
    mouseProcess.terminate()
    bluetoothProcess.terminate()

    # Wait for the processes to be terminated
    keyboardProcess.join()
    mouseProcess.join()
    bluetoothProcess.join()

    print("Closing Application")
