import base64
import logging
from time import sleep
from typing import List

from .connection import Connection
from .deadline import Deadline, Timeout
from .message import Message

logger = logging.getLogger(__name__)

QUIT_MSG = Message("q")
L_MSG = Message("l")
L_REPLY_CMD = L_MSG.reply_cmd()

UPDATE_TIMEOUT = Timeout("update", 3.0)
CONNECT_TIMEOUT = Timeout("connect", 3.0)
FLUSH_INPUT_TIMEOUT = Timeout("flush-input", 0)
SEND_RADIO_MSG_TIMEOUT = Timeout("send-radio-msg", 30.0)
CMD_REPLY_TIMEOUT = Timeout("cmd-reply", 2.0)


class Commander(object):
    def __init__(self, host: str, port: int):
        self.__host: str = host
        self.__port: int = port
        self.use_persistent_connection = True
        self.__connection: Connection = None
        self.__unsolicited_messages: List[Message] = []

    def disconnect(self):
        if self.__connection:
            try:
                self.__connection.send(QUIT_MSG)
            except Exception:
                logger.debug(
                    "Unable to properly shutdown MAX Cube connection. Resetting it..."
                )
            finally:
                self.__close()

    def get_unsolicited_messages(self) -> List[Message]:
        result = self.__unsolicited_messages
        self.__unsolicited_messages = []
        return result

    def update(self) -> List[Message]:
        deadline = UPDATE_TIMEOUT.deadline()
        if self.__is_connected():
            try:
                response = self.__call(L_MSG, deadline)
                if response:
                    self.__unsolicited_messages.append(response)
            except Exception:
                self.__connect(deadline)
        else:
            self.__connect(deadline)
        if not self.use_persistent_connection:
            self.disconnect()
        return self.get_unsolicited_messages()

    def send_radio_msg(self, hex_radio_msg: str) -> bool:
        deadline = SEND_RADIO_MSG_TIMEOUT.deadline()
        request = Message(
            "s", base64.b64encode(bytearray.fromhex(hex_radio_msg)).decode("utf-8")
        )
        while not deadline.is_expired():
            if self.__cmd_send_radio_msg(request, deadline):
                return True
        return False

    def __cmd_send_radio_msg(self, request: Message, deadline: Deadline) -> bool:
        try:
            response = self.__call(request, deadline)
            duty_cycle, status_code, free_slots = response.arg.split(",", 3)
            if status_code == "0":
                return True
            logger.debug(
                "Radio message %s was not send [DutyCycle:%s, StatusCode:%s, FreeSlots:%s]"
                % (request, duty_cycle, status_code, free_slots)
            )
            if int(duty_cycle, 16) == 100 and int(free_slots, 16) == 0:
                sleep(deadline.remaining(upper_bound=10.0))
        except Exception as ex:
            logger.error("Error sending radio message to Max! Cube: " + str(ex))
        return False

    def __call(self, msg: Message, deadline: Deadline) -> Message:
        already_connected = self.__is_connected()
        if not already_connected:
            self.__connect(deadline.subtimeout(CONNECT_TIMEOUT))
        else:
            # Protection in case some late answer arrives for a previous command
            self.__wait_for_reply(None, deadline.subtimeout(FLUSH_INPUT_TIMEOUT))

        try:
            self.__connection.send(msg)
            subdeadline = deadline.subtimeout(CMD_REPLY_TIMEOUT)
            result = self.__wait_for_reply(msg.reply_cmd(), subdeadline)
            if result is None:
                raise TimeoutError(str(subdeadline))
            return result

        except Exception:
            self.__close()
            if already_connected:
                return self.__call(msg, deadline)
            else:
                raise

        finally:
            if not self.use_persistent_connection:
                self.disconnect()

    def __is_connected(self) -> bool:
        return self.__connection is not None

    def __connect(self, deadline: Deadline):
        self.__unsolicited_messages = []
        self.__connection = Connection(self.__host, self.__port)
        reply = self.__wait_for_reply(
            L_REPLY_CMD, deadline.subtimeout(CMD_REPLY_TIMEOUT)
        )
        if reply:
            self.__unsolicited_messages.append(reply)

    def __wait_for_reply(self, reply_cmd: str, deadline: Deadline) -> Message:
        while True:
            msg = self.__connection.recv(deadline)
            if msg is None:
                return None
            elif reply_cmd and msg.cmd == reply_cmd:
                return msg
            else:
                self.__unsolicited_messages.append(msg)

    def __close(self):
        self.__connection.close()
        self.__connection = None
