# Bluetool code is placed under the GPL license.
# Written by Aleksandr Aleksandrov (aleksandr.aleksandrov@emlid.com)
# Copyright (c) 2016-2017, Emlid Limited
# All rights reserved.

# If you are interested in using Bluetool code as a part of a
# closed source project, please contact Emlid Limited (info@emlid.com).

# This file is part of Bluetool.

# Bluetool 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.

# Bluetool 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 Bluetool.  If not, see <http://www.gnu.org/licenses/>.

import time
import logging
import threading

import dbus
import dbus.mainloop.glib
import bluezutils


logger = logging.getLogger(__name__)


class Bluetooth(object):

    def __init__(self):
        dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
        self._bus = dbus.SystemBus()
        self._scan_thread = None

    def start_scanning(self, timeout=10):
        if self._scan_thread is None:
            self._scan_thread = threading.Thread(
                target=self.scan, args=(timeout,))
            self._scan_thread.daemon = True
            self._scan_thread.start()

    def scan(self, timeout=10):
        try:
            adapter = bluezutils.find_adapter()
        except (bluezutils.BluezUtilError,
                dbus.exceptions.DBusException) as error:
            logger.error(str(error) + "\n")
        else:
            try:
                adapter.StartDiscovery()
                time.sleep(timeout)
                adapter.StopDiscovery()
            except dbus.exceptions.DBusException as error:
                logger.error(str(error) + "\n")

        self._scan_thread = None

    def get_devices_to_pair(self):
        devices = self.get_available_devices()

        for key in self.get_paired_devices():
            devices.remove(key)

        return devices

    def get_available_devices(self):
        available_devices = self._get_devices("Available")
        logger.debug("Available devices: {}".format(available_devices))
        return available_devices

    def get_paired_devices(self):
        paired_devices = self._get_devices("Paired")
        logger.debug("Paired devices: {}".format(paired_devices))
        return paired_devices

    def get_connected_devices(self):
        connected_devices = self._get_devices("Connected")
        logger.debug("Connected devices: {}".format(connected_devices))
        return connected_devices

    def _get_devices(self, condition):
        devices = []
        conditions = ("Available", "Paired", "Connected")

        if condition not in conditions:
            logger.error("_get_devices: unknown condition - {}\n".format(
                condition))
            return devices

        try:
            man = dbus.Interface(
                self._bus.get_object("org.bluez", "/"),
                "org.freedesktop.DBus.ObjectManager")
            objects = man.GetManagedObjects()

            for path, interfaces in objects.items():
                if "org.bluez.Device1" in interfaces:
                    dev = interfaces["org.bluez.Device1"]

                    if condition == "Available":
                        if "Address" not in dev:
                            continue

                        if "Name" not in dev:
                            dev["Name"] = "<unknown>"

                        device = {
                            "mac_address": dev["Address"].encode("utf-8"),
                            "name": dev["Name"].encode("utf-8")
                        }

                        devices.append(device)
                    else:
                        props = dbus.Interface(
                            self._bus.get_object("org.bluez", path),
                            "org.freedesktop.DBus.Properties")

                        if props.Get("org.bluez.Device1", condition):
                            if "Address" not in dev:
                                continue

                            if "Name" not in dev:
                                dev["Name"] = "<unknown>"

                            device = {
                                "mac_address": dev["Address"].encode("utf-8"),
                                "name": dev["Name"].encode("utf-8")
                            }

                            devices.append(device)
        except dbus.exceptions.DBusException as error:
            logger.error(str(error) + "\n")

        return devices

    def make_discoverable(self, value=True, timeout=180):
        try:
            adapter = bluezutils.find_adapter()
        except (bluezutils.BluezUtilError,
                dbus.exceptions.DBusException) as error:
            logger.error(str(error) + "\n")
            return False

        try:
            props = dbus.Interface(
                self._bus.get_object("org.bluez", adapter.object_path),
                "org.freedesktop.DBus.Properties")

            timeout = int(timeout)
            value = int(value)

            if int(props.Get(
                    "org.bluez.Adapter1", "DiscoverableTimeout")) != timeout:
                props.Set(
                    "org.bluez.Adapter1", "DiscoverableTimeout",
                    dbus.UInt32(timeout))

            if int(props.Get("org.bluez.Adapter1", "Discoverable")) != value:
                props.Set(
                    "org.bluez.Adapter1", "Discoverable", dbus.Boolean(value))
        except dbus.exceptions.DBusException as error:
            logger.error(str(error) + "\n")
            return False

        logger.info("Discoverable: {}".format(value))
        return True

    def start_pairing(self, address, callback=None, args=()):
        pair_thread = threading.Thread(
            target=self._pair_trust_and_notify,
            args=(address, callback, args))
        pair_thread.daemon = True
        pair_thread.start()

    def _pair_trust_and_notify(self, address, callback=None, args=()):
        result = self.pair(address)

        if callback is not None:
            if result:
                result = self.trust(address)
            callback(result, *args)

    def pair(self, address):
        try:
            device = bluezutils.find_device(address)
        except (bluezutils.BluezUtilError,
                dbus.exceptions.DBusException) as error:
            logger.error(str(error) + "\n")
            return False

        try:
            props = dbus.Interface(
                self._bus.get_object("org.bluez", device.object_path),
                "org.freedesktop.DBus.Properties")

            if not props.Get("org.bluez.Device1", "Paired"):
                device.Pair()
        except dbus.exceptions.DBusException as error:
            logger.error(str(error) + "\n")
            return False

        logger.info("Successfully paired with {}".format(address))
        return True

    def connect(self, address):
        try:
            device = bluezutils.find_device(address)
        except (bluezutils.BluezUtilError,
                dbus.exceptions.DBusException) as error:
            logger.error(str(error) + "\n")
            return False

        try:
            props = dbus.Interface(
                self._bus.get_object("org.bluez", device.object_path),
                "org.freedesktop.DBus.Properties")

            if not props.Get("org.bluez.Device1", "Connected"):
                device.Connect()
        except dbus.exceptions.DBusException as error:
            logger.error(str(error) + "\n")
            return False

        logger.info("Successfully connected to {}".format(address))
        return True

    def disconnect(self, address):
        try:
            device = bluezutils.find_device(address)
        except (bluezutils.BluezUtilError,
                dbus.exceptions.DBusException) as error:
            logger.error(str(error) + "\n")
            return False

        try:
            props = dbus.Interface(
                self._bus.get_object("org.bluez", device.object_path),
                "org.freedesktop.DBus.Properties")

            if props.Get("org.bluez.Device1", "Connected"):
                device.Disconnect()
        except dbus.exceptions.DBusException as error:
            logger.error(str(error) + "\n")
            return False

        return True

    def trust(self, address):
        try:
            device = bluezutils.find_device(address)
        except (bluezutils.BluezUtilError,
                dbus.exceptions.DBusException) as error:
            logger.error(str(error) + "\n")
            return False

        try:
            props = dbus.Interface(
                self._bus.get_object("org.bluez", device.object_path),
                "org.freedesktop.DBus.Properties")

            if not props.Get("org.bluez.Device1", "Trusted"):
                props.Set("org.bluez.Device1", "Trusted", dbus.Boolean(1))
        except dbus.exceptions.DBusException as error:
            logger.error(str(error) + "\n")
            return False

        return True

    def remove(self, address):
        try:
            adapter = bluezutils.find_adapter()
            dev = bluezutils.find_device(address)
        except (bluezutils.BluezUtilError,
                dbus.exceptions.DBusException) as error:
            logger.error(str(error) + "\n")
            return False

        try:
            adapter.RemoveDevice(dev.object_path)
        except dbus.exceptions.DBusException as error:
            logger.error(str(error) + "\n")
            return False

        logger.info("Successfully removed: {}".format(address))
        return True

    def set_adapter_property(self, prop, value):
        try:
            adapter = bluezutils.find_adapter()
        except (bluezutils.BluezUtilError,
                dbus.exceptions.DBusException) as error:
            logger.error(str(error) + "\n")
            return False

        try:
            props = dbus.Interface(
                self._bus.get_object("org.bluez", adapter.object_path),
                "org.freedesktop.DBus.Properties")

            if props.Get("org.bluez.Adapter1", prop) != value:
                props.Set("org.bluez.Adapter1", prop, value)
        except dbus.exceptions.DBusException as error:
            logger.error(str(error) + "\n")
            return False

        return True

    def get_adapter_property(self, prop):
        try:
            adapter = bluezutils.find_adapter()
        except (bluezutils.BluezUtilError,
                dbus.exceptions.DBusException) as error:
            logger.error(str(error) + "\n")
            return None

        try:
            props = dbus.Interface(
                self._bus.get_object("org.bluez", adapter.object_path),
                "org.freedesktop.DBus.Properties")

            return props.Get("org.bluez.Adapter1", prop)
        except dbus.exceptions.DBusException as error:
            logger.error(str(error) + "\n")
            return None

    def set_device_property(self, address, prop, value):
        try:
            device = bluezutils.find_device(address)
        except (bluezutils.BluezUtilError,
                dbus.exceptions.DBusException) as error:
            logger.error(str(error) + "\n")
            return False

        try:
            props = dbus.Interface(
                self._bus.get_object("org.bluez", device.object_path),
                "org.freedesktop.DBus.Properties")

            if props.Get("org.bluez.Device1", prop) != value:
                props.Set("org.bluez.Device1", prop, value)
        except dbus.exceptions.DBusException as error:
            logger.error(str(error) + "\n")
            return False

        return True

    def get_device_property(self, address, prop):
        try:
            device = bluezutils.find_device(address)
        except (bluezutils.BluezUtilError,
                dbus.exceptions.DBusException) as error:
            logger.error(str(error) + "\n")
            return None

        try:
            props = dbus.Interface(
                self._bus.get_object("org.bluez", device.object_path),
                "org.freedesktop.DBus.Properties")

            return props.Get("org.bluez.Device1", prop)
        except dbus.exceptions.DBusException as error:
            logger.error(str(error) + "\n")
            return None