#!/usr/bin/python

# Bluetooth HCI Python library  (Experimental)
#
# Pure Python and standard library based module for interacting with the Bluetooth HCI.
# There is no dependency on the PyBluez Python/Native libraries, bluetoothd service or D-Bus.

# This can be considered to be a Pythonisation of the NodeJS NoBLE/ BLENo by Sandeep Mistry.

# Author:  Wayne Keenan
# email:   wayne@thebubbleworks.com
# Twitter: https://twitter.com/wkeenan


# Acknowledgements:

# Significant information taken from https://github.com/sandeepmistry/node-bluetooth-hci-socket
# With help from https://github.com/colin-guyon/py-bluetooth-utils and the BlueZ Python library.

import array
import struct
import fcntl
import socket
import threading
try:
    import thread
except ImportError:
    import _thread
from threading import Event
import select
import os
import sys

from .BluetoothSocket import BluetoothSocket

from .constants import *

OGF_HOST_CTL = 0x03
OCF_RESET = 0x0003

# -------------------------------------------------
# Socket HCI transport API

# This socket based to the Bluetooth HCI.

# Strong candidate for refactoring into factory pattern to support
# alternate transports (e.g. serial) and easier mocking for automated testing.

class BluetoothHCISocketProvider:

    def __init__(self, device_id=0):
        self.device_id = device_id
        self._keep_running = True
        self._socket = None
        self._socket_on_data_user_callback = None
        self._socket_on_started = None
        self._socket_poll_thread = None
        self._l2sockets = {}

        self._socket = BluetoothSocket(socket.AF_BLUETOOTH, socket.SOCK_RAW, socket.BTPROTO_HCI)
        
    
        #self._socket = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_RAW, socket.BTPROTO_HCI)
        #self._socket = BluetoothUserSocket()
        #self._socket = bluetooth.bluez._gethcisock(0)

        self._socket.setblocking(0)
        self.__r, self.__w = os.pipe()
        self._r = os.fdopen(self.__r, 'rU')
        self._w = os.fdopen(self.__w, 'w')

    def __del__(self):
        self._keep_running = False


    def open(self):

        # TODO: specify channel: HCI_CHANNEL_RAW, HCI_CHANNEL_USER, HCI_CHANNEL_CONTROL
        # https://www.spinics.net/lists/linux-bluetooth/msg37345.html
        # self._socket.bind((self.device_id,))
        
        HCI_CHANNEL_RAW = 0
        HCI_CHANNEL_USER = 1
        self._socket.bind_hci(self.device_id, HCI_CHANNEL_RAW)
        #self._socket2.bind_l2(0, "0B:D8:28:EB:27:B8", cid=ATT_CID, addr_type=1)
        #self._socket2.connect_l2(0, "0B:D8:28:EB:27:B8", cid=ATT_CID, addr_type=1)

        #self.reset()


        self._socket_poll_thread = threading.Thread(target=self._socket_poller, name='HCISocketPoller')
        self._socket_poll_thread.setDaemon(True)
        self._socket_poll_thread.start()

    def kernel_disconnect_workarounds(self, data):
        #print 'PRE KERNEL WORKAROUND %d' % len(data)
        
        def noop(value):
            return value
            
        if (sys.version_info > (3, 0)):
            ord = noop
        else:
            import __builtin__
            ord = __builtin__.ord
        
        if len(data) == 22 and [ord(elem) for elem in data[0:5]] == [0x04, 0x3e, 0x13, 0x01, 0x00]:
            handle = ord(data[5])
            # get address
            set = data[9:15]
            # get device info
            dev_info = self.get_device_info()
            raw_set = [ord(c) for c in set]
            raw_set.reverse()
            #addz = ''.join([hex(c) for c in set])
            #set.reverse()
            addz = "%02x:%02x:%02x:%02x:%02x:%02x" % struct.unpack("BBBBBB", array.array('B', raw_set))
            socket2 = BluetoothSocket(socket.AF_BLUETOOTH, socket.SOCK_SEQPACKET, socket.BTPROTO_L2CAP)
            
            socket2.bind_l2(0, dev_info['addr'], cid=ATT_CID, addr_type=0)#addr_type=dev_info['type'])
            
            self._l2sockets[handle] = socket2
            try:
                result = socket2.connect_l2(0, addz, cid=ATT_CID, addr_type=ord(data[8]) + 1)
            except:
                pass
        elif len(data) == 7 and [ord(elem) for elem in data[0:4]] == [0x04, 0x05, 0x04, 0x00]:
            handle = ord(data[4])
            
            socket2 = self._l2sockets[handle] if handle in self._l2sockets else None
            if socket2:
                # print 'GOT A SOCKET!'
                socket2.close()
                del self._l2sockets[handle]

    def reset(self):
        cmd = array.array('B', [0] * 4)
        
        # // header
        # cmd.writeUInt8(HCI_COMMAND_PKT, 0);
        # cmd.writeUInt16LE(OCF_RESET | OGF_HOST_CTL << 10, 1);
        
        # // length
        # cmd.writeUInt8(0x00, 3);
        
        struct.pack_into("<BHB", cmd, 0, HCI_COMMAND_PKT, OCF_RESET | OGF_HOST_CTL << 10, 0x00)
        
        #debug('reset');
        self.write_buffer(cmd);        
        
    def close(self):
        self._socket.close()

    def send_cmd(self, cmd, data):
        arr = array.array('B', data)
        fcntl.ioctl(self._socket.fileno(), cmd, arr)
        return arr

    def send_cmd_value(self, cmd, value):
        fcntl.ioctl(self._socket.fileno(), cmd, value)

    def write_buffer(self, data):
        self._socket.send(data)

    def set_filter(self, data):
        # flt = bluez.hci_filter_new()
        # bluez.hci_filter_all_events(flt)
        # bluez.hci_filter_set_ptype(flt, bluez.HCI_EVENT_PKT)
        self._socket.setsockopt( socket.SOL_HCI, socket.HCI_FILTER, data )   
        pass
        #self._socket.setsockopt(socket.SOL_HCI, socket.HCI_FILTER, data)

    def invoke(self, callback):
        
        event = Event()
        self._msg = (event, callback)
        self._w.write(" ")
        event.wait()

    def _socket_poller(self):
        if self._socket_on_started:
            self._socket_on_started()
            
        while self._keep_running:
            readable, writable, exceptional = select.select([self._socket, self._r], [], [])
            for s in readable:
                
                if s == self._r:
                    self._r.read(1)
                    self._msg[1]()
                    self._msg[0].set()
                    self._msg = None
                elif s == self._socket:
                    data = self._socket.recv(1024)         # blocking
                    self.kernel_disconnect_workarounds(data)
                    if self._socket_on_data_user_callback:
                        self._socket_on_data_user_callback(bytearray(data))

    def on_started(self, callback):
        self._socket_on_started = callback
        
    def on_data(self, callback):
        self._socket_on_data_user_callback = callback

    def get_device_info(self):

        # C hci_dev_info struct defined at https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/lib/hci.h#n2382
        hci_dev_info_struct = struct.Struct('=H 8s 6B L B 8B 3L 4I 10L')

        request_dta = hci_dev_info_struct.pack(
            self.device_id,
            b'',
            0, 0, 0, 0, 0, 0,
            0,
            0,
            0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

        response_data = self.send_cmd(HCIGETDEVINFO, request_dta)

        hci_dev_info = hci_dev_info_struct.unpack(response_data)

        # Just extract a few parts for now
        device_id = hci_dev_info[0]
        device_name = hci_dev_info[1].split(b'\0',1)[0]
        bd_addr = "%0x:%0x:%0x:%0x:%0x:%0x" % hci_dev_info[7:1:-1]
        type = hci_dev_info[4]
        
        return dict(id=device_id,
                    name=device_name,
                    addr=bd_addr,
                    type=type)

class BluetoothHCI:
    def __init__(self, device_id=0, auto_start = True):
        # TODO: be given a provider interface from a factory (e.g. socket, serial, mock)
        self.hci = BluetoothHCISocketProvider(device_id)
        if auto_start:
            self.start()

    # -------------------------------------------------
    # Public HCI API, simply delegates to the composite HCI provider

    def start(self):
        self.hci.open()

    def stop(self):
        self.hci.close()
    
    def on_started(self, callback):
        self.hci.on_started(callback)
    
    def invoke(self, callback):
        self.hci.invoke(callback)

    def send_cmd(self, cmd, data):
        return self.hci.send_cmd(cmd, data)
        # packet type struct : https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/lib/hci.h#n117
        # typedef struct {
        #   uint16_t	opcode;		/* OCF & OGF */
        #   uint8_t		plen;
        #   } __attribute__ ((packed))	hci_command_hdr;
        # Op-code (16 bits): identifies the command:
        #    OGF (Op-code Group Field, most significant 6 bits);
        #    OCF (Op-code Command Field, least significant 10 bits)."""

    def send_cmd_value(self, cmd, value):
        self.hci.send_cmd_value(cmd, value)

    def write(self, data):
        self.hci.write_buffer(data)

    def set_filter(self, data):
        #self.device_down()
        self.hci.set_filter(data)
        #self.device_up()
        

    def on_data(self, callback):
        self.hci.on_data(callback)

    

    # -------------------------------------------------
    # Public HCI Convenience API

    def device_up(self):
        self.send_cmd_value(HCIDEVUP, self.hci.device_id)

    def device_down(self):
        self.send_cmd_value(HCIDEVDOWN, self.hci.device_id)

    def get_device_info(self):

        # C hci_dev_info struct defined at https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/lib/hci.h#n2382
        hci_dev_info_struct = struct.Struct('=H 8s 6B L B 8B 3L 4I 10L')

        request_dta = hci_dev_info_struct.pack(
            self.hci.device_id,
            b'',
            0, 0, 0, 0, 0, 0,
            0,
            0,
            0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0,
            0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0)

        response_data = self.send_cmd(HCIGETDEVINFO, request_dta)

        hci_dev_info = hci_dev_info_struct.unpack(response_data)

        # Just extract a few parts for now
        device_id = hci_dev_info[0]
        device_name = hci_dev_info[1].split(b'\0',1)[0]
        bd_addr = "%0x:%0x:%0x:%0x:%0x:%0x" % hci_dev_info[7:1:-1]

        return dict(id=device_id,
                    name=device_name,
                    addr=bd_addr)