import asyncio
import subprocess
import logging
from bleak import BleakClient

from .daly_bms import DalyBMS


class DalyBMSBluetooth(DalyBMS):
    def __init__(self, request_retries=3, logger=None):
        """

        :param request_retries: How often read requests should get repeated in case that they fail (Default: 3).
        :param logger: Python Logger object for output (Default: None)
        """
        if logger:
            self.logger = logger
        else:
            self.logger = logging.getLogger(__name__)
        DalyBMS.__init__(self, request_retries=request_retries, address=8, logger=logger)
        self.client = None
        self.response_cache = {}

    async def connect(self, mac_address):
        """
        Open the connection to the Bluetooth device.

        :param mac_address: MAC address of the Bluetooth device
        """
        try:
            """
            When an earlier execution of the script crashed, the connection to the devices stays open and future 
            connection attempts would fail with this error:
            bleak.exc.BleakError: Device with address AA:BB:CC:DD:EE:FF was not found.
            see https://github.com/hbldh/bleak/issues/367
            """
            open_blue = subprocess.Popen(["bluetoothctl"], shell=True, stdout=subprocess.PIPE,
                                         stderr=subprocess.STDOUT, stdin=subprocess.PIPE)
            open_blue.communicate(b"disconnect %s\n" % mac_address.encode('utf-8'))
            open_blue.kill()
        except:
            pass
        self.client = BleakClient(mac_address)
        await self.client.connect()
        await self.client.start_notify(17, self._notification_callback)
        await self.client.write_gatt_char(48, bytearray(b""))

    async def disconnect(self):
        """
        Disconnect from the Bluetooth device
        """
        self.logger.info("Bluetooth Disconnecting")
        await self.client.disconnect()
        self.logger.info("Bluetooth Disconnected")

    async def _read_request(self, command, max_responses=1):
        response_data = None
        x = None
        for x in range(0, self.request_retries):
            response_data = await self._read(
                command=command,
                max_responses=max_responses)
            if not response_data:
                self.logger.debug("%x. try failed, retrying..." % (x + 1))
                await asyncio.sleep(0.2)
            else:
                break
        if not response_data:
            self.logger.error('%s failed after %s tries' % (command, x + 1))
            return False
        return response_data

    async def _read(self, command, max_responses=1):
        self.logger.debug("-- %s ------------------------" % command)
        self.response_cache[command] = {"queue": [],
                                        "future": asyncio.Future(),
                                        "max_responses": max_responses,
                                        "done": False}

        message_bytes = self._format_message(command)
        result = await self._async_char_write(command, message_bytes)
        self.logger.debug("got %s" % result)
        if not result:
            return False
        if max_responses == 1:
            return result[0]
        else:
            return result

    def _notification_callback(self, handle, data):
        self.logger.debug("%s %s %s" % (handle, repr(data), len(data)))
        responses = []
        if len(data) == 13:
            responses.append(data)
        elif len(data) == 26:
            responses.append(data[0:13])
            responses.append(data[13:])
        else:
            self.logger.error(len(data), "bytes received, not 13 or 26, not implemented")

        for response_bytes in responses:
            command = response_bytes[2:3].hex()
            if self.response_cache[command]["done"] is True:
                self.logger.debug("skipping response for %s, done" % command)
                return
            self.response_cache[command]["queue"].append(response_bytes[4:-1])
            if len(self.response_cache[command]["queue"]) == self.response_cache[command]["max_responses"]:
                self.response_cache[command]["done"] = True
                self.response_cache[command]["future"].set_result(self.response_cache[command]["queue"])

    async def _async_char_write(self, command, value):
        if not self.client.is_connected:
            self.logger.info("Connecting...")
            await self.client.connect()

        await self.client.write_gatt_char(15, value)
        self.logger.debug("Waiting...")
        try:
            result = await asyncio.wait_for(self.response_cache[command]["future"], 5)
        except asyncio.TimeoutError:
            self.logger.warning("Timeout while waiting for %s response" % command)
            return False
        self.logger.debug("got %s" % result)
        return result

    # wrap all sync functions so that they can be awaited
    async def get_soc(self, response_data=None):
        response_data = await self._read_request("90")
        return super().get_soc(response_data=response_data)

    async def get_cell_voltage_range(self, response_data=None):
        response_data = await self._read_request("91")
        return super().get_cell_voltage_range(response_data=response_data)

    async def get_max_min_temperature(self, response_data=None):
        response_data = await self._read_request("92")
        return super().get_max_min_temperature(response_data=response_data)

    async def get_mosfet_status(self, response_data=None):
        response_data = await self._read_request("93")
        return super().get_mosfet_status(response_data=response_data)

    async def get_status(self, response_data=None):
        response_data = await self._read_request("94")
        return super().get_status(response_data=response_data)

    async def get_cell_voltages(self, response_data=None):
        if not self.status:
            await self.get_status()
        max_responses = self._calc_cell_voltage_responses()
        if not max_responses:
            return
        response_data = await self._read_request("95", max_responses=max_responses)

        return super().get_cell_voltages(response_data=response_data)

    async def get_temperatures(self, response_data=None):
        response_data = await self._read_request("95")
        return super().get_temperatures(response_data=response_data)

    async def get_balancing_status(self, response_data=None):
        response_data = await self._read_request("96")
        return super().get_balancing_status(response_data=response_data)

    async def get_errors(self, response_data=None):
        response_data = await self._read_request("97")
        return super().get_errors(response_data=response_data)

    async def get_all(self):
        return {
            "soc": await self.get_soc(),
            "cell_voltage_range": await self.get_cell_voltage_range(),
            "temperature_range": await self.get_temperature_range(),
            "mosfet_status": await self.get_mosfet_status(),
            "status": await self.get_status(),
            "cell_voltages": await self.get_cell_voltages(),
            "temperatures": await self.get_temperatures(),
            "balancing_status": await self.get_balancing_status(),
            "errors": await self.get_errors()
        }
