## @file bluespy.py
"""blueSPY Python API

API for controlling blueSPY from python.

Provides functions for connecting to a moreph and loading existing captures.
Also provides a 'packets' object, which behaves like a list and can be used to access
the packets in the current file.
len(packets) shows the number of available packets, packets[0] accesses the first packet.
The queries documented in the GUI can be accessed with attribute syntax, e.g. packets[0].summary

Examples:
To connect to a moreph with serial 00010100 and capture CL and LE:

    import bluespy
    from time import sleep

    bluespy.connect(0x00010100)
    bluespy.capture("example.pcapng", CL=True, LE=True)
    sleep(20)
    bluespy.stop_capture()
    bluespy.disconnect()
    print("Captured {} packets".format(len(bluespy.packets)))
    bluespy.close_file()


To load an existing capture and print the summary strings of all packets:

    import bluespy

    bluespy.load_file("example.pcapng")
    for p in bluespy.packets:
        print(p.summary)
    bluespy.close_file()

"""

import ctypes as _ct
import atexit

import platform
import os

_path = ""

if "BLUESPY_LIBRARY_PATH" in os.environ:
    _path = os.environ["BLUESPY_LIBRARY_PATH"]

if not os.path.isfile(_path):
    if platform.system() == "Windows":
        _name = "libblueSPY.dll"
    elif platform.system() == "Linux":
        _name = "libblueSPY.so"
    elif platform.system() == "Darwin":
        _name = "../Frameworks/libblueSPY.dylib"
    _path = os.path.abspath(os.path.join(os.path.dirname(__file__), _name))
    if not os.path.isfile(_path):
        _path = _name  # Try the LoadLibrary default paths

try:
    _libbluespy = _ct.cdll.LoadLibrary(_path)
except FileNotFoundError as e:
    raise FileNotFoundError(
        "libblueSPY not found, please set BLUESPY_LIBRARY_PATH correctly in the environment"
    ) from e


class error(_ct.c_uint32):
    """! Return type showing why an operation failed.

    Evaluates to True if the operation succeeded, else False.
    str() and repr() give an error string, .value gives an error code.
    """

    def __str__(self):
        return _libbluespy.bluespy_error_string(self.value).decode("utf-8")

    def __repr__(self):
        return "{}: {}".format(self.value, self.__str__()) if self.value else ""

    def __bool__(self):
        return self.value == 0


class BluespyError(RuntimeError):
    """! Exception showing why an operation failed."""

    def get_error(self):
        """Return the underlying error object."""
        return self.args[0]


def _handle_error(err):
    if err.value:
        raise BluespyError(err)


def connect(serial=-1):
    """! Connect to Moreph hardware via USB or Ethernet.

    @param serial: A serial number as an integer, or -1 to connect to the first USB device.
    The serial number shown in the software and on the MiniMoreph is hexadecimal, so should be entered as 0xNNNNNN.
    There is a serial number on the bottom of some Moreph30s is of the form AYYYY-XXXXX, the XXXXX is the required serial number in decimal.

    @exception BluespyError: Exception in the bluespy library

    You should run disconnect() later if this is successful.
    """

    _handle_error(_libbluespy.bluespy_connect(serial))


def disconnect():
    """! Disconnect from current Moreph

    @exception BluespyError: Exception in the bluespy library"""
    atexit.unregister(disconnect)
    _handle_error(_libbluespy.bluespy_disconnect())


def capture(
    filename,
    CL=False,
    LE=False,
    QHS=False,
    _15_4=False,
    wifi=False,
    MHDT_CL=False,
    MHDT_LE=False,
    Dukosi=False,
    Varjo=False,
    CS=False,
    spectrum=0,
):
    """! Start a new capture in filename

    @param filename: Path to store capture in
    @param CL: Enable Bluetooth classic capture
    @param LE: Enable Bluetooth LE capture
    @param QHS: Enable Qualcomm High Speed capture
    @param _15_4: Enable 802.15.4 capture
    @param wifi: Enable wifi capture
    @param MHDT_CL: Enable MediaTek mHDT Classic capture
    @param MHDT_LE: Enable MediaTek mHDT LE capture
    @param Dukosi: Enable Dukosi capture
    @param Varjo: Enable Varjo capture
    @param CS: Enable Channel-Sounding capture
    @param spectrum: Spectrum capture interval in microseconds. 0 means disabled.
        Allowed values are: 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000.

    @exception BluespyError: Exception in the bluespy library"""
    opts = _libbluespy.bluespy_capture_options_alloc()
    opts.contents.enable_CL = CL
    opts.contents.enable_LE = LE
    opts.contents.enable_QHS = QHS
    opts.contents.enable_15_4 = _15_4
    opts.contents.enable_wifi = wifi
    opts.contents.enable_MHDT_CL = MHDT_CL
    opts.contents.enable_MHDT_LE = MHDT_LE
    opts.contents.enable_Dukosi = Dukosi
    opts.contents.enable_Varjo = Varjo
    opts.contents.enable_Channel_Sounding = CS
    opts.contents.spectrum_period = spectrum
    _handle_error(
        _libbluespy.bluespy_capture(
            filename.encode("utf-8"), opts,
        )
    )
    _libbluespy.bluespy_capture_options_delete(opts)
    atexit.register(close_file)
    atexit.register(stop_capture)


def stop_capture():
    """! Stop the current capture

    @exception BluespyError: Exception in the bluespy library"""
    atexit.unregister(stop_capture)
    _handle_error(_libbluespy.bluespy_stop_capture())


def load_file(filename):
    """! Load an existing capture

    @exception BluespyError: Exception in the bluespy library"""
    _handle_error(_libbluespy.bluespy_load_file(filename.encode("utf-8")))
    atexit.register(close_file)


def close_file():
    """! Load an existing capture

    @exception BluespyError: Exception in the bluespy library"""
    atexit.unregister(close_file)
    _handle_error(_libbluespy.bluespy_close_file())


class event_id(_ct.c_uint64):
    """!
    An object referencing a loaded packet.

    Do not make your own, only get these from the 'packets' object.
    After running close_file(), do not call any methods on any existing event_id objects.

    Any query (see the documentation or Help->Query List in the GUI) can be accessed as an attribute on this object
    """

    def __bool__(self):
        """! Returns true if this is a valid packet"""
        return self.value != 0xFFFFFFFFFFFFFFFF

    def parent(self):
        """! Get the a higher layer packet that contains this one.
        e.g. if this is a baseband data packet get the L2CAP packet it is part of.

        @return: event_id"""
        return _libbluespy.bluespy_get_parent(self.value)

    def children(self):
        """! Get all constituent packets of this packet.
        e.g. if this is an L2CAP packet

        @return: List of event_ids"""
        count = _ct.c_uint32()
        c = _libbluespy.bluespy_get_children(self.value, _ct.byref(count))
        return (
            list(_ct.cast(c, _ct.POINTER(event_id * count.value)).contents)
            if count.value > 0
            else []
        )

    def query(self, name):
        """!
        Get a query from this packet, and return it in its preferred form

        @param name: A query string, e.g. "summary"
        @return: A string, int or bool depending on the query"""
        s = _ct.c_char_p()
        i = _ct.c_int64()
        b = _ct.c_bool()
        r = _libbluespy.bluespy_query_auto(
            self.value, name.encode(
                "utf-8"), _ct.byref(s), _ct.byref(i), _ct.byref(b)
        )
        if r == 1:
            return s.value.decode("utf-8")
        if r == 2:
            return i.value
        if r == 3:
            return b.value
        raise AttributeError()

    def __getattr__(self, name):
        """Access queries as attributes"""
        return self.query(name)

    def query_str(self, name):
        """!
        Get a query from this packets, and return it as a string

        @param name: A query string, e.g. "summary"
        @return: string"""
        return _libbluespy.bluespy_query(self.value, name.encode("utf-8")).decode(
            "utf-8"
        )

    def query_int(self, name):
        """!
        Get a query from this packets, and return it as an integer if possible

        @param name: A query string, e.g. "summary"
        @return: int"""
        return _libbluespy.bluespy_query_int(self.value, name.encode("utf-8"))

    def query_bool(self, name):
        """!
        Get a query from this packets, and return it as a bool if possible

        @param name: A query string, e.g. "acked"
        @return: bool"""
        return _libbluespy.bluespy_query_bool(self.value, name.encode("utf-8"))


class Packets(object):
    """List-like object representing the currently loaded baseband packets."""

    def __len__(self):
        """Current number of baseband packets captured"""
        return _libbluespy.bluespy_packet_count()

    def __getitem__(self, i):
        """!
        Get an event_id for a packet

        @param i: Index of packet, 0 <= i < __len__()
        @return: An event_id
        """
        i = int(i)
        if i < 0 or i >= _libbluespy.bluespy_packet_count():
            raise IndexError()
        return _libbluespy.bluespy_get_baseband(i)


packets = Packets()


def add_link_key(key, addr0=0, addr1=0):
    """! Add a link key for decryption

    @param key: Link key as a 16-byte bytes object
    @param addr0: (Optional) MAC address of central as a 64-bit integer
    @param addr1: (Optional) MAC address of peripheral as a 64-bit integer
    """
    _libbluespy.bluespy_add_link_key(key, addr0, addr1)


def start_gui():
    """! Spawn an instance of the user interface"""
    _libbluespy.bluespy_start_gui()


class _capture_options(_ct.Structure):
    _fields_ = [
        ("enable_CL", _ct.c_bool),
        ("enable_LE", _ct.c_bool),
        ("enable_QHS", _ct.c_bool),
        ("enable_15_4", _ct.c_bool),
        ("enable_wifi", _ct.c_bool),
        ("enable_MHDT_CL", _ct.c_bool),
        ("enable_MHDT_LE", _ct.c_bool),
        ("enable_Dukosi", _ct.c_bool),
        ("enable_Varjo", _ct.c_bool),
        ("enable_Channel_Sounding", _ct.c_bool),
        # Valid spectrum periods in microseconds are:
        # 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000
        # 0 to disable spectrum
        ("spectrum_period", _ct.c_uint16),
    ]


_libbluespy.bluespy_error_string.argtypes = [_ct.c_uint32]
_libbluespy.bluespy_error_string.restype = _ct.c_char_p

_libbluespy.bluespy_connect.argtypes = [_ct.c_uint32]
_libbluespy.bluespy_connect.restype = error

_libbluespy.bluespy_disconnect.argtypes = []
_libbluespy.bluespy_disconnect.restype = error

_libbluespy.bluespy_capture_options_alloc.argtypes = []
_libbluespy.bluespy_capture_options_alloc.restype = _ct.POINTER(
    _capture_options)

_libbluespy.bluespy_capture_options_delete.argtypes = [
    _ct.POINTER(_capture_options)]
_libbluespy.bluespy_capture_options_delete.restype = None

_libbluespy.bluespy_capture.argtypes = [
    _ct.c_char_p, _ct.POINTER(_capture_options)]
_libbluespy.bluespy_capture.restype = error

_libbluespy.bluespy_stop_capture.argtypes = []
_libbluespy.bluespy_stop_capture.restype = error

_libbluespy.bluespy_load_file.argtypes = [_ct.c_char_p]
_libbluespy.bluespy_load_file.restype = error

_libbluespy.bluespy_close_file.argtypes = []
_libbluespy.bluespy_close_file.restype = error

_libbluespy.bluespy_packet_count.argtypes = []
_libbluespy.bluespy_packet_count.restype = _ct.c_uint32

_libbluespy.bluespy_get_baseband.argtypes = [_ct.c_uint32]
_libbluespy.bluespy_get_baseband.restype = event_id

_libbluespy.bluespy_get_parent.argtypes = [event_id]
_libbluespy.bluespy_get_parent.restype = event_id

_c_uint32_p = _ct.POINTER(_ct.c_uint32)
_libbluespy.bluespy_get_children.argtypes = [event_id, _c_uint32_p]
_libbluespy.bluespy_get_children.restype = _ct.POINTER(event_id)

_libbluespy.bluespy_query.argtypes = [event_id, _ct.c_char_p]
_libbluespy.bluespy_query.restype = _ct.c_char_p

_libbluespy.bluespy_query_int.argtypes = [event_id, _ct.c_char_p]
_libbluespy.bluespy_query_int.restype = _ct.c_int64

_libbluespy.bluespy_query_bool.argtypes = [event_id, _ct.c_char_p]
_libbluespy.bluespy_query_bool.restype = _ct.c_bool

_libbluespy.bluespy_query_auto.argtypes = [
    event_id,
    _ct.c_char_p,
    _ct.POINTER(_ct.c_char_p),
    _ct.POINTER(_ct.c_int64),
    _ct.POINTER(_ct.c_bool),
]
_libbluespy.bluespy_query_auto.restype = _ct.c_int

_libbluespy.bluespy_add_link_key.argtypes = [
    _ct.c_char_p, _ct.c_uint64, _ct.c_uint64]
_libbluespy.bluespy_add_link_key.restype = None

_libbluespy.bluespy_start_gui.argtypes = []
_libbluespy.bluespy_start_gui.restype = None

_libbluespy.bluespy_init.argtypes = []
_libbluespy.bluespy_init.restype = None

_libbluespy.bluespy_deinit.argtypes = []
_libbluespy.bluespy_deinit.restype = None

_libbluespy.bluespy_init()
atexit.register(_libbluespy.bluespy_deinit)
