import json
import re
import copy

from abc import ABCMeta
from abc import abstractmethod
from enum import Enum
from collections import deque
from copy import deepcopy
from extronlib.system import Wait


class ByteBuffer:
    def __init__(self, existing=None):
        self.__buffer = self.__convert_iter_to_bytes(existing) if existing else []

    @property
    def buffer(self):
        return self.__buffer

    @staticmethod
    def __convert_iter_to_bytes(bytevalues: iter) -> list:
        return [str(b).encode('utf-8') if not isinstance(b, bytes) else b for b in bytevalues]

    @staticmethod
    def __get_bytes(bytevalues: iter, count: int) -> bytes:
        copied = deepcopy(bytevalues)
        return copied[:count]

    @staticmethod
    def __is_valid(bytevalues: any) -> bool:
        return (
                isinstance(bytevalues, list) or
                isinstance(bytevalues, bytes) or
                isinstance(bytevalues, bytearray) or
                isinstance(bytevalues, str) or
                isinstance(bytevalues, int) or
                isinstance(bytevalues, ByteBuffer)
        )

    def __deepcopy__(self, item: object):
        return ByteBuffer(deepcopy(self.__buffer))

    def __getitem__(self, index: int) -> bytes:
        return self.__buffer[index]

    def __iter__(self) -> iter:
        return (n for n in self.__buffer)

    def __len__(self) -> int:
        return len(self.__buffer)

    def __str__(self):
        """ Returns the buffer as a string. """
        return "".join([b.decode('utf-8') for b in self.__buffer])

    def append(self, *args: object) -> None:
        bytevalues = args[0]
        if len(args) > 1:
            # noinspection PyTypeChecker
            count = int(args[1])
            if isinstance(bytevalues, list):
                bytevalues = self.__convert_iter_to_bytes(bytevalues)
                self.__buffer.extend(self.__get_bytes(bytevalues, count))
            else:
                raise ValueError('Positional argument 0 must be of type list when used in conjunction with count.')
        else:
            if not self.__is_valid(bytevalues):
                raise ValueError('Positional argument 0 must be of type int, str, bytes, bytearray, or ByteBuffer.')
            if isinstance(bytevalues, str) or isinstance(bytevalues, int):
                bytevalues = str(bytevalues).encode('utf-8')
            if isinstance(bytevalues, bytes):
                bytes_ = (b.encode('utf-8') for b in bytevalues.decode('utf-8'))
                self.__buffer.extend(bytes_)
            if isinstance(bytevalues, list) or isinstance(bytevalues, bytearray) or isinstance(bytevalues, ByteBuffer):
                bytevalues = self.__convert_iter_to_bytes(bytevalues)
                self.__buffer.extend(bytevalues)

    def clear(self) -> None:
        self.__buffer.clear()

    def delete(self, start: int, count: int) -> None:
        self.__buffer = self.__buffer[:start] + self.__buffer[start + count:]

    def ends_with(self, item: object) -> bool:
        if isinstance(item, str) or isinstance(item, int):
            item = str(item).encode('utf-8')
        elif isinstance(item, list) or isinstance(item, bytearray) or isinstance(item, ByteBuffer):
            item = deepcopy(item)[::-1]
            byte_buff = ByteBuffer(deepcopy(self)[::-1])
            return byte_buff.starts_with(item)
        return self.index(item) == len(self) - 1

    def index(self, item: any) -> int:
        if isinstance(item, str) or isinstance(item, int):
            item = str(item).encode('utf-8')
        elif isinstance(item, bytearray) or isinstance(item, list) or isinstance(item, ByteBuffer):
            return self.__index_of_iter(item)
        return self.__buffer.index(item)

    def __index_of_iter(self, items: iter) -> int:
        for index, _ in enumerate(self.__buffer):
            if self.__is_match(self.__buffer, index, items):
                return index
        return -1

    def starts_with(self, item):
        if isinstance(item, str) or isinstance(item, int):
            item = str(item).encode('utf-8')
        return self.index(item) == 0

    @staticmethod
    def __is_match(arr: list, pos: int, candidate: iter) -> bool:
        # noinspection PyTypeChecker
        if len(candidate) > (len(arr) - pos):
            return False
        else:
            for index, _ in enumerate(candidate):
                if arr[pos + index] != candidate[index]:
                    return False
        return True


class ConnectionStates(Enum):
    CONNECTED = 0
    DISCONNECTED = 1
    ERROR = 2
    INITIALIZING = 3


class ClientSocketError(Exception):
    def __init__(self, message):
        super().__init__(message)


class QueuePriorities(Enum):
    CMD = 0
    QUERY = 1


class DebugTypes(Enum):
    ERROR = 0
    INFO = 1


class Environ(Enum):
    EXTRON = 0
    LINUX = 1
    WINDOWS = 2


class LoggerInterface(metaclass=ABCMeta):
    @classmethod
    def __subclasshook__(cls, subclass):
        return (
                hasattr(subclass, 'log') and
                callable(subclass.log)
        )

    @abstractmethod
    def log(self, msg: str, severity: DebugTypes = None, log: bool = True) -> None:
        pass


class PrinterUtil:
    def __init__(self, logger=None):
        if logger is not None and hasattr(logger, "log") and callable(logger.log):
            logger.log("PrinterUtil Initialized", severity=DebugTypes.INFO, log=False)
            self.__logger = logger
            self.__debug = None
        else:
            raise ValueError("`logger` must be an object with method `log`")

    @property
    def debug(self):
        return self.__debug

    @debug.setter
    def debug(self, value):
        if value != "ON" and value != "OFF":
            raise ValueError("Invalid value for debug: {0}".format(value))
        self.__debug = True if value == "ON" else False

    def Print(self, msg: str or list, severity: DebugTypes = None, log: bool = True) -> None:
        if severity is None:
            severity = DebugTypes.INFO
        if log is True:
            msg += "\n"
        if self.__debug or severity is DebugTypes.ERROR:
            if isinstance(msg, list):
                self.__logger.log(*msg, severity=severity, log=log)
            else:
                self.__logger.log(msg, severity=severity, log=log)


class LoggerInstance(LoggerInterface):
    def __init__(self, program_log: callable, stdout: callable) -> None:
        self.__program_log = program_log
        self.__stdout = stdout

    def log(self, msg: str, severity: DebugTypes = None, log=True) -> None:
        logger = self.__program_log
        if log is False:
            logger = self.__stdout
        if severity is None:
            severity = DebugTypes.INFO
        if severity is DebugTypes.INFO:
            logger('Info: {0}'.format(msg))
        else:
            logger('Error: {0}'.format(msg))


class Range:
    def __init__(self, minimum, maximum, step=1):
        self.minimum = minimum
        self.maximum = maximum
        self.step = step


class IntegerRange(Range):
    def __init__(self, minimum, maximum, step=1):
        minimum = int(minimum)
        maximum = int(maximum)
        super().__init__(minimum, maximum, step=step)


class FloatRange(Range):
    def __init__(self, minimum, maximum, step=1):
        minimum = float(minimum)
        maximum = float(maximum)
        super().__init__(minimum, maximum, step=step)


class IComponentInterface(metaclass=ABCMeta):
    @classmethod
    def __subclasscheck__(cls, subclass):
        return (
                hasattr(subclass, 'Emulate') and callable(subclass.Emulate) and
                hasattr(subclass, 'Set') and callable(subclass.Set) and
                hasattr(subclass, 'ReadStatus') and callable(subclass.ReadStatus) and
                hasattr(subclass, 'Update') and callable(subclass.Update) and
                hasattr(subclass, 'SubscribeStatus') and callable(subclass.SubscribeStatus) and
                hasattr(subclass, 'PatternMatch') and callable(subclass.PatternMatch)
        )

    @abstractmethod
    def Emulate(self, val: any, params: dict = None) -> None:
        raise NotImplementedError

    @abstractmethod
    def Set(self, val: any, params: dict = None):
        raise NotImplementedError

    @abstractmethod
    def ReadStatus(self, params=None):
        raise NotImplementedError

    @abstractmethod
    def Query(self, params=None):
        raise NotImplementedError

    @abstractmethod
    def SubscribeStatus(self, callback, params=None):
        raise NotImplementedError

    @abstractmethod
    def HandleIncomingResponse(self, response):
        raise NotImplementedError


class Message:
    def __init__(self, message, sender=None):
        self.message = message
        self.sender = sender

    def encode(self, encoding):
        return self.message.encode(encoding)

    def __contains__(self, item):
        return item in self.message

    def __str__(self):
        return str(self.message)

    def __len__(self):
        return len(self.message)


class Response(Message):
    def __init__(self, message, sender=None):
        super().__init__(message, sender)


class Request(Message):
    def __init__(self, message, sender=None):
        super().__init__(message, sender=sender)


class IProcessor(metaclass=ABCMeta):
    @classmethod
    def __subclasshook__(cls, subclass):
        return (
            hasattr(cls, 'enqueue') and callable(cls.enqueue),
            hasattr(cls, 'id'),
            hasattr(cls, 'debug_message') and callable(cls.debug_message)
        )

    @property
    @abstractmethod
    def id(self):
        pass

    @abstractmethod
    def debug_message(self, d_type: DebugTypes, msg: str, params: dict = None) -> None:
        pass

    @abstractmethod
    def enqueue(self, sender: object, req: Request) -> None:
        pass


ENVIRON = Environ.EXTRON

if ENVIRON is not Environ.EXTRON:
    import threading
else:
    from extronlib.system import Timer

if ENVIRON is Environ.WINDOWS:
    import socket


    class TCPClientInterface:
        def __init__(self, ip: str, port: int, buffer_size: int = 1024, debugger: PrinterUtil = None) -> None:
            self.ip = ip
            self.port = port
            self.__connected = False
            self.__debugger = debugger
            self.__buffer_size = buffer_size
            self.__sock = None
            self.__rx_data = None
            self.__socket_status_changed = None

        @property
        def connected(self):
            return self.__connected

        @property
        def rx_data(self) -> callable:
            return self.__rx_data

        @rx_data.setter
        def rx_data(self, handler: callable) -> None:
            self.__rx_data = handler

        @property
        def socket_status_changed(self) -> callable:
            return self.__socket_status_changed

        @socket_status_changed.setter
        def socket_status_changed(self, handler: callable) -> None:
            self.__socket_status_changed = handler

        def connect(self) -> None or Exception:
            self.socket_status_changed(self, ConnectionStates.INITIALIZING)
            try:
                self.__sock = socket.create_connection((self.ip, self.port))
                self.__connected = True
                self.socket_status_changed(self, ConnectionStates.CONNECTED)
            except (ConnectionError, BlockingIOError, Exception) as err:
                if self.__debugger:
                    self.__debugger.Print(
                        "TCPClientInterface - {0}".format(str(err)), severity=DebugTypes.ERROR, log=True
                    )
                self.socket_status_changed(self, ConnectionStates.ERROR)
                return err

        def disconnect(self):
            try:
                self.__sock.close()
                self.__connected = False
                if self.socket_status_changed:
                    self.socket_status_changed(self, ConnectionStates.DISCONNECTED)
            except (AttributeError, Exception):
                pass

        def send_data(self, data: str) -> None or Exception:
            try:
                self.__sock.sendall(data.encode('utf-8'))
            except (ConnectionError, BlockingIOError, Exception) as err:
                self.__connected = False
                if self.__debugger:
                    self.__debugger.Print(
                        "TCPClientInterface - {0}".format(str(err)), severity=DebugTypes.ERROR, log=True
                    )
                self.socket_status_changed(self, ConnectionStates.ERROR)
                return err

        def listen(self) -> None or Exception:
            while True:
                try:
                    data = self.__sock.recv(1024)
                    if self.rx_data is not None:
                        self.rx_data(self.__sock, data)
                except (ConnectionError, BlockingIOError, Exception) as err:
                    self.__connected = False
                    if self.__debugger:
                        self.__debugger.Print(
                            "TCPClientInterface - {0}".format(str(err)), severity=DebugTypes.ERROR, log=True
                        )
                    self.socket_status_changed(self, ConnectionStates.ERROR)
                    return err
else:
    from extronlib.interface import EthernetClientInterface
    from extronlib import event


    class TCPClientInterface:
        def __init__(self, ip: str, port: int, buffer_size: int = 1024, debugger: PrinterUtil = None,
                     mode='TCP') -> None:
            self.ip = ip
            self.port = port
            self.__sock = EthernetClientInterface(ip, port, Protocol=mode)
            self.__connected = False
            self.__debugger = debugger
            self.__buffer_size = buffer_size
            self.__rx_data = None
            self.__socket_status_changed = None
            self.__connection_handler()
            self.listen()

        @property
        def connected(self):
            return self.__connected

        @property
        def rx_data(self) -> callable:
            return self.__rx_data

        @rx_data.setter
        def rx_data(self, handler: callable) -> None:
            self.__rx_data = handler

        @property
        def socket_status_changed(self) -> callable:
            return self.__socket_status_changed

        @socket_status_changed.setter
        def socket_status_changed(self, handler: callable) -> None:
            self.__socket_status_changed = handler

        def __connection_handler(self):
            @event(self.__sock, ['Connected', 'Disconnected'])
            def __status_changed(interface, state):
                if state == 'Connected':
                    self.__connected = True
                    self.__socket_status_changed(self, ConnectionStates.CONNECTED)
                elif state == 'Disconnected':
                    self.__connected = False
                    self.__socket_status_changed(self, ConnectionStates.ERROR)

        def connect(self) -> None or Exception:
            self.socket_status_changed(self, ConnectionStates.INITIALIZING)
            if 'Connected' not in self.__sock.Connect(2):
                self.socket_status_changed(self, ConnectionStates.ERROR)

        def disconnect(self):
            if self.__connected:
                self.__sock.Disconnect()

        def send_data(self, data: str) -> None or Exception:
            try:
                if self.__connected:
                    self.__sock.Send(data.encode('utf-8'))
            except (TypeError, IOError) as err:
                if self.__debugger:
                    self.__debugger.Print(
                        "TCPClientInterface - {0}".format(str(err)), severity=DebugTypes.INFO, log=True
                    )
                self.socket_status_changed(self, ConnectionStates.ERROR)

        def listen(self) -> None or Exception:
            @event(self.__sock, 'ReceiveData')
            def __incoming_data(interface, data):
                self.rx_data(interface, data)


    class UDPClientInterface:
        def __init__(self, ip: str, port: int, buffer_size: int = 1024, debugger: PrinterUtil = None,
                     mode='UDP') -> None:
            self.ip = ip
            self.port = port
            self.__sock = EthernetClientInterface(ip, port, Protocol=mode, ServicePort=port)
            self.__connected = False
            self.__debugger = debugger
            self.__buffer_size = buffer_size
            self.__rx_data = None
            self.__debouncing = False
            self.listen()

        def disconnect(self):
            self.__sock.Disconnect()

        @property
        def connected(self):
            return self.__connected

        @property
        def rx_data(self) -> callable:
            return self.__rx_data

        @rx_data.setter
        def rx_data(self, handler: callable) -> None:
            self.__rx_data = handler

        def listen(self) -> None or Exception:
            @event(self.__sock, 'ReceiveData')
            def __incoming_data(interface, data):
                if self.rx_data is not None:
                    if not self.__debouncing:
                        self.rx_data(interface, data)
                        self.__debouncing = True

                        @Wait(0.1)
                        def __reset_debounce():
                            self.__debouncing = False


class ThreadLock:
    def __init__(self, env=Environ.EXTRON):
        self.__env = env
        self.__lock = None

    def __enter__(self, *args, **kwargs):
        if self.__env is not Environ.EXTRON:
            return threading.Lock()
        return None

    def __exit__(self, *args, **kwargs):
        pass


if ENVIRON is Environ.EXTRON:
    class CCITimer:
        def __init__(self, fn, interval):
            self.fn = fn
            self.timer = Timer(interval, lambda *args: self.fn())

        def start(self):
            self.timer.Restart()

        def reset(self, time):
            if self.timer.State != 'Stopped':
                self.timer.Stop()
            self.timer.Restart()

        def stop(self):
            if self.timer.State != 'Stopped':
                self.timer.Stop()
else:
    class CCITimer:
        def __init__(self, fn, interval):
            if ENVIRON is Environ.EXTRON:
                self.timer = Timer(interval, fn)
            else:
                self.timer = threading.Timer(interval, fn)
            self.fn = fn
            self.timer.start()

        def start(self):
            self.timer.start()

        def reset(self, time):
            self.timer.cancel()
            return CCITimer(self.fn, time)

        def stop(self):
            self.timer.cancel()


class ICommManager(metaclass=ABCMeta):
    @classmethod
    def __subclasshook__(cls, subclass):
        methods = [
            'clear',
            'dequeue',
            'enqueue',
            'good_response',
            'received_heartbeat',
            'start',
            'start_heartbeat_timer',
            'start_reconnection_timer',
            'stop',
            'stop_heartbeat_timer',
            'stop_reconnection_timer',
            'received_error_send_again'
        ]
        booleans = [
            'is_locked',
            'is_ready',
            'auto_initialize',
        ]
        contains = [hasattr(subclass, m) and callable(getattr(subclass, m)) for m in methods]
        bools = [hasattr(subclass, b) for b in booleans]
        return False not in contains or False not in bools

    @property
    @abstractmethod
    def is_locked(self):
        pass

    @is_locked.setter
    @abstractmethod
    def is_locked(self, val):
        pass

    @property
    @abstractmethod
    def is_ready(self):
        pass

    @is_ready.setter
    @abstractmethod
    def is_ready(self, val):
        pass

    @property
    @abstractmethod
    def auto_initialize(self):
        pass

    @auto_initialize.setter
    @abstractmethod
    def auto_initialize(self, val):
        pass

    @abstractmethod
    def received_error_send_again(self):
        pass

    @abstractmethod
    def clear(self, priority: int = 0) -> None:
        pass

    @abstractmethod
    def dequeue(self):
        pass

    @abstractmethod
    def enqueue(self, msg: str, priority: int = 0) -> None:
        pass

    @abstractmethod
    def good_response(self):
        pass

    @abstractmethod
    def received_heartbeat(self):
        pass

    @abstractmethod
    def start(self):
        pass

    @abstractmethod
    def start_heartbeat_timer(self):
        pass

    @abstractmethod
    def start_reconnection_timer(self):
        pass

    @abstractmethod
    def stop(self):
        pass

    @abstractmethod
    def stop_heartbeat_timer(self):
        pass

    @abstractmethod
    def stop_reconnection_timer(self):
        pass


class ICommUtil(metaclass=ABCMeta):
    @classmethod
    def __subclasshook__(cls, subclass):
        methods = [
            'send_heartbeat',
            'get_initialized',
            'strikeout',
            'reconnect',
            'send_trace',
            'failed_response'
        ]
        booleans = [
            'is_initialized',
        ]
        integers = [
            'get_response_time',
            'get_heartbeat_time'
        ]
        contains = [hasattr(subclass, m) and callable(getattr(subclass, m)) for m in methods]
        bools = [hasattr(subclass, b) for b in booleans]
        ints = [hasattr(subclass, i) for i in integers]
        return False not in contains or False not in bools or False not in ints

    @property
    @abstractmethod
    def is_initialized(self):
        pass

    @property
    @abstractmethod
    def get_response_time(self):
        pass

    @property
    @abstractmethod
    def get_heartbeat_time(self):
        pass

    @abstractmethod
    def send_heartbeat(self):
        pass

    @abstractmethod
    def get_initialized(self):
        pass

    @abstractmethod
    def strikeout(self):
        pass

    @abstractmethod
    def reconnect(self):
        pass

    @abstractmethod
    def send_trace(self, msg: str) -> None:
        pass

    @abstractmethod
    def failed_response(self):
        pass

    @abstractmethod
    def send_message(self, message: Request) -> None:
        pass


class PriorityQueue:
    """
    PriorityQueue manages a list of deque objects that each have a specific priority. The default `priority_count` is
    1, so the default number of queues is also 1. PriorityQueue uses deque under the hood, and each element is appended
    to the queue in the order they came in. There is an exception for items that need to be enqueued first; these items
    are appended to the left side of the deque. Each item is popped from the left side of the deque, though this
    detail is not entirely important.
    """

    def __init__(self, count=1):
        self.__queues = [deque() for n in range(0, count)]

    def enqueue(self, message: str, priority: int = 0) -> None:
        try:
            self.__queues[priority].append(message)
        except (IndexError, Exception):
            pass

    def enqueue_first(self, message: str, priority: int = 0) -> None:
        try:
            self.__queues[priority].appendleft(message)
        except (IndexError, Exception):
            pass

    def dequeue(self, priority: int = 0) -> any:
        try:
            return self.__queues[priority].popleft()
        except (IndexError, Exception):
            pass

    def clear(self, priority: int = 0) -> None:
        try:
            if priority is None:
                for queue in self.__queues:
                    queue.clear()
            else:
                self.__queues[priority].clear()
        except (IndexError, Exception):
            pass

    def count(self, priority: int = 0) -> int:
        if priority is None:
            return sum(len(q) for q in self.__queues)
        else:
            return len(self.__queues[priority])


class PriorityCommUtil(ICommManager):
    def __init__(self, listener: ICommUtil, priority_count=1) -> None:
        if not isinstance(listener, ICommUtil):
            raise TypeError('`listener` must implement the ICommUtil interface.')
        self.listener = listener
        self.__locked = None
        self.__queues = PriorityQueue(count=priority_count)
        self.__response_timer = None
        self.__heartbeat_timer = None
        self.__reconnection_timer = None
        self.__dequeue_pacer_time = .0001 / 1000000
        self.__dequeue_pacer_timer = None
        self.__thread_lock = ThreadLock
        self.__strike_count = 0
        self.__retry_count = 0
        self.__RECONNECT_TIME = 15000
        self.__is_locked = False
        self.__auto_initialize = False
        self.__last_message = None
        self.__retry_fail_count = 3
        self.__received_error_send_again = False
        self.__pacer_time = 0.001

    @property
    def auto_initialize(self) -> bool:
        return self.__auto_initialize

    @property
    def dequeue_pacer_time(self):
        return self.__dequeue_pacer_time

    @dequeue_pacer_time.setter
    def dequeue_pacer_time(self, val: int) -> None:
        if val >= 50:
            self.__dequeue_pacer_time = val

    @property
    def is_ready(self) -> bool:
        return not self.is_locked and self.__queues.count() == 0

    @property
    def is_locked(self) -> bool:
        return self.__is_locked

    @is_locked.setter
    def is_locked(self, val: bool):
        if isinstance(val, bool):
            self.__is_locked = val
        else:
            raise TypeError('val must be of type `bool`')

    @property
    def retry_fail_count(self) -> int:
        return self.__retry_fail_count

    @retry_fail_count.setter
    def retry_fail_count(self, val):
        if val > 0:
            self.__retry_fail_count = val

    def clear(self, priority=None) -> None:
        with self.__thread_lock():
            self.__queues.clear(priority=priority)

    def dequeue(self) -> None:
        with self.__thread_lock():
            if self.__locked and self.__queues.count() == 0:
                return
            elif self.listener:
                self.__last_message = self.__queues.dequeue()
                if self.__last_message is not None:
                    self.__locked = True
                    self.listener.send_message(Request(self.__last_message))
                    self.start_response_timer()

    def enqueue(self, msg: str, priority: int = 0) -> None:
        with self.__thread_lock():
            self.__queues.enqueue(msg, priority=priority)
            if not self.__locked and self.__queues.count(priority=priority) == 1:
                self.reset_dequeue_pacer_timer()

    def failed_response(self):
        self.__strike_count += 1

        if self.listener:
            self.listener.failed_response()

        if self.__strike_count < 3:
            self.stop_response_timer()
            self.__locked = False
            self.reset_dequeue_pacer_timer()
        else:
            self.stop()
            if self.listener:
                self.listener.strikeout()

    def good_response(self) -> None:
        self.__strike_count = 0
        self.__retry_count = 0
        self.__locked = False
        self.stop_response_timer()
        self.reset_dequeue_pacer_timer()

    def on_heartbeat_event(self, *_: list) -> None:
        if self.__heartbeat_timer:
            self.__heartbeat_timer.reset(self.listener.get_heartbeat_time)
            if self.listener:
                self.listener.send_heartbeat()

    def on_reconnection_timeout(self, *_: list) -> None:
        if self.__reconnection_timer:
            self.__reconnection_timer.reset(self.__RECONNECT_TIME)
            if self.listener:
                self.listener.reconnect()

    def queue_size(self, priority: int = None) -> int:
        return self.__queues.count(priority=priority)

    def received_error_send_again(self) -> bool:
        response = False
        self.__strike_count = 0
        self.__retry_count += 1
        self.stop_response_timer()

        if self.__retry_count < self.__retry_fail_count:
            response = True
            self.__queues.enqueue_first(self.__last_message)

        if not response:
            self.__retry_count = 0

        self.__locked = False
        self.reset_dequeue_pacer_timer()
        return response

    def received_heartbeat(self):
        if self.listener:
            if not self.listener.is_initialized() and self.auto_initialize:
                self.listener.get_initialized()

    def reset_dequeue_pacer_timer(self) -> None:
        self.start_dequeue_pacer_timer()

    def start(self):
        self.stop()
        self.stop_reconnection_timer()
        self.start_heartbeat_timer()

    def start_dequeue_pacer_timer(self) -> None:
        if self.__dequeue_pacer_timer is not None:
            self.__dequeue_pacer_timer.reset(.01)
        else:
            self.__dequeue_pacer_timer = CCITimer(
                self.dequeue,
                .01
            )

    def start_heartbeat_timer(self):
        if self.__heartbeat_timer is None:
            if self.listener is not None:
                self.__heartbeat_timer = CCITimer(
                    self.on_heartbeat_event,
                    self.listener.get_heartbeat_time
                )
        else:
            self.__heartbeat_timer.reset(self.listener.get_heartbeat_time)
        self.listener.send_heartbeat()

    def start_reconnection_timer(self):
        if self.__reconnection_timer is not None:
            self.__reconnection_timer.reset(self.__RECONNECT_TIME)
        else:
            self.__reconnection_timer = CCITimer(
                self.on_reconnection_timeout,
                self.__RECONNECT_TIME
            )

    def start_response_timer(self) -> None:
        if self.__response_timer is not None:
            self.__response_timer.reset(self.listener.get_response_time)
        else:
            self.__response_timer = CCITimer(
                self.failed_response,
                self.listener.get_response_time
            )

    def stop(self):
        self.clear()
        self.__strike_count = 0
        self.__retry_count = 0
        self.stop_response_timer()
        self.stop_heartbeat_timer()
        self.stop_reconnection_timer()
        self.stop_dequeue_pacer_timer()
        self.__locked = False

    def stop_dequeue_pacer_timer(self):
        if self.__dequeue_pacer_timer:
            self.__dequeue_pacer_timer.stop()
            self.__dequeue_pacer_timer = None

    def stop_heartbeat_timer(self):
        if self.__heartbeat_timer:
            self.__heartbeat_timer.stop()

    def stop_reconnection_timer(self):
        if self.__reconnection_timer:
            self.__reconnection_timer.stop()

    def stop_response_timer(self) -> None:
        if self.__response_timer:
            self.__response_timer.stop()


class AtlasIEDCommand:
    def __init__(self, method: str = "set", params: dict = None, val: object = None, fmt: object = None,
                 terminator: str = '\n', id_tag: object = None) -> None:
        if method not in ['get', 'set', 'unsub', 'sub', 'bmp']:
            raise ValueError('AtlasIEDCommand - Invalid method {0}'.format(method))
        self.method = method
        self.params = params
        self.val = val
        self.fmt = fmt
        self.terminator = terminator
        self.id_tag = id_tag

    def __raise_error(self, key: str) -> None:
        raise ValueError("{0} cannot be None".format(key))

    def __subscription_packet(self) -> str:
        return json.dumps({
            "jsonrpc": "2.0",
            "method": self.method,
            "params": self.params
        }) + self.terminator

    def __set_packet(self) -> str:
        outgoing = {
            "jsonrpc": "2.0",
        }
        if self.id_tag is not None:
            outgoing["id"] = self.id_tag
        outgoing.update({
            "method": self.method,
            "params": self.params
        })
        return json.dumps(outgoing) + self.terminator

    def __str__(self) -> str:
        if self.method == 'sub' or self.method == 'unsub' or self.method == "get":
            if self.fmt is None:
                self.__raise_error("fmt")
            self.params.update({
                'fmt': self.fmt
            })
            return self.__subscription_packet()
        else:
            if self.val is None:
                self.__raise_error("val")
            self.params.update({
                'val': self.val
            })
            return self.__set_packet()


class ComponentState:
    def __init__(self):
        self.CurrentState = None

    def Update(self, value: str) -> bool:
        if self.CurrentState != value:
            self.CurrentState = value
            return True
        return False

    @staticmethod
    def ProcessResponse(component: object, commandname: str, value: str, params: dict, encoded: str) -> None:
        listeners = component.Listeners
        if 'All' in listeners:
            callback = listeners['All']['Callback']
            callback(commandname, value, params)
        listener = listeners.get(encoded, None)
        if listener is not None:
            callback = listener['Callback']
            callback(commandname, value, params)


class BaseComponent(IComponentInterface):
    def __init__(self, listener: object, commandname: str, pattern: object = None) -> None:
        self.Pattern = pattern
        self.commandname = commandname
        self.listener = listener
        self.Listeners = {}
        self.Subscriptions = {}
        self.ValueConversion = {}
        self.ComponentStates = {}
        self.Parameters = []

    def DebugMessage(self, command: str, err: Exception, severity: DebugTypes = DebugTypes.INFO) -> None:
        self.listener.debug_message(
            severity,
            "{0} {1} {2} - {3}".format(self.commandname, command, type(err).__name__, str(err))
        )

    def Emulate(self, *args, **kwargs):
        pass

    @staticmethod
    def Encode(params: dict = None) -> str:
        if params is not None:
            return "".join(['{0}{1}'.format(k, v) for k, v in params.items()])

    def CreateMessages(
            self, val: object, params: dict, key: str = None, commandname: str = None, method: str = 'set') -> None:
        if key in params:
            command_params = {'param': '{0}_{1}'.format(commandname, params[key])}
            if params[key] not in self.Subscriptions[key]:
                self.Subscriptions[key][params[key]] = True
                command = AtlasIEDCommand(method='sub', params=copy.deepcopy(command_params), fmt='str')
                self.listener.enqueue(Request(str(command), sender=self))
            command = AtlasIEDCommand(method=method, params=command_params, val=val)
            self.listener.enqueue(Request(str(command), sender=self))

    @staticmethod
    def ConvertMatch(*args, **kwargs):
        return args

    def __GetMatches(self, matches: re.match) -> tuple:
        param = matches.group(1).decode('utf-8')
        num = matches.group(2).decode('utf-8')
        encoded = "{0}{1}".format(param, num)
        value = matches.group(3).decode('utf-8')
        value, num, param = self.ConvertMatch(value, num, param)
        return value, num, param, encoded

    @staticmethod
    def __GuaranteeOrder(para: dict) -> str:
        # Python 3.5 doesn't guarantee the order of the dictionary.
        p = str(para['param'])
        if 'val' in para:
            v = str(para['val'])
            k = "{{'param': '{0}', 'val': '{1}'}}".format(p, v)
        else:
            v = str(para['str'])
            k = "{{'param': '{0}', 'str': '{1}'}}".format(p, v)
        return k

    @staticmethod
    def __HandleSourceValue(value):
        value = int(value)
        if value == 0:
            value = None
        else:
            value = value - 1
        if value is not None:
            value = str(value)
        return value

    def __RegularMatch(self, para: dict, commandname: str = None) -> None:
        k = self.__GuaranteeOrder(para)
        matches = re.search(self.Pattern, str(k).encode('utf-8'))
        if matches is not None:
            value, num, param, encoded = self.__GetMatches(matches)
            if commandname is None:
                commandname = self.commandname
            try:
                state = self.ComponentStates.setdefault(encoded, ComponentState())
                update = state.Update(value)
                if update:
                    if commandname == 'Source':
                        value = self.__HandleSourceValue(value)
                    if commandname == 'LoudNoise':
                        state.ProcessResponse(self, commandname, value, {}, encoded)
                    else:
                        state.ProcessResponse(self, commandname, value, {param: num}, encoded)
            except (KeyError, Exception) as err:
                self.DebugMessage('RegularMatch', err)

    def __ScheduleMatch(self, para: dict, commandname: str = None) -> None:
        k = self.__GuaranteeOrder(para)
        matches = re.search(self.Pattern, str(k).encode('utf-8'))
        if matches is not None:
            try:
                value = matches.group(2).decode('utf-8')
                state = self.ComponentStates.setdefault('TodaysBellSchedule', ComponentState())
                update = state.Update(value)
                if update:
                    state.ProcessResponse(self, commandname, value, {}, 'TodaysBellSchedule')
            except (KeyError, Exception) as err:
                self.DebugMessage('RegularMatch', err)

    def HandleIncomingResponse(self, response: Response, commandname: str = None) -> None:
        params = response.message.get('params', None)
        if params is not None:
            method = response.message['method']
            if method != 'update' and method != 'getResp':
                return
            for para in params:
                if 'result' not in para:
                    if para['param'] != 'TodaysBellSchedule':
                        self.__RegularMatch(para, commandname=commandname)
                    else:
                        self.__ScheduleMatch(para, commandname=commandname)

    def Query(self, params: dict = None) -> None:
        if params is None:
            raise KeyError('Must provide parameters {0}'.format(", ".join(self.Parameters)))
        self.ValidateParameters(params=params)

    def Reinitialize(self):
        for k, v in self.Listeners.items():
            try:
                params = v['Update']
                if params is not None:
                    self.Query(params=v['Update'])
            except KeyError as err:
                pass

    def ReadStatus(self, params: dict = None) -> object:
        try:
            encoded = self.Encode(params=params)
            state = self.ComponentStates[encoded]
            if state is not None:
                return state.CurrentState
        except (KeyError, Exception) as err:
            self.DebugMessage('ReadStatus', err)

    def Set(self, val: str, params: dict = None) -> None:
        if params is None:
            raise KeyError('Must provide one of {0} for parameters'.format(", ".join(self.Parameters)))
        self.ValidateValues(val)
        self.ValidateParameters(params=params)
        if hasattr(self, 'SetHooks'):
            self.SetHooks(val, params=params)

    def SubscribeStatus(self, callback: callable, params: dict = None) -> None:
        if params is None:
            encoded = 'All'
        else:
            encoded = self.Encode(params=params)
        listener = self.Listeners.setdefault(encoded, {
            'Update': params,
            'Callback': callback
        })
        if listener['Callback'] is not callback:
            listener['Callback'] = callback
        self.ComponentStates.setdefault(encoded, ComponentState())

    def ValidateParameters(self, params=None):
        for k, v in params.items():
            if k not in self.Parameters:
                raise KeyError('Invalid Key For {0}'.format(self.commandname))

    def ValidateValues(self, val):
        pass


class SourceComponent(BaseComponent):
    def __init__(self, listener: object, pattern: object = None) -> None:
        super().__init__(listener, 'Source', pattern=pattern)
        self.Subscriptions = {
            'Zone': {},
            'Group': {},
        }
        self.Parameters = [k for k in self.Subscriptions]

    def Set(self, val: str, params: dict = None) -> None:
        try:
            super().Set(val, params=params)
            command = self.commandname
            for key in params:
                self.CreateMessages(int(val), params=params, key=key, commandname="{0}{1}".format(key, command))
        except (KeyError, ValueError, Exception) as err:
            self.DebugMessage('Set', err)

    def Query(self, params: dict = None) -> None:
        try:
            super().Query(params=params)
            update = AtlasIEDCommand(method="sub", params=None, fmt="str")
            command = self.commandname
            for key in params:
                update.params = {"param": "{0}{1}_{2}".format(key, command, params[key])}
                self.listener.enqueue(Request(str(update)))
        except (KeyError, Exception) as err:
            self.DebugMessage('Query', err)

    def SubscribeStatus(self, callback: callable, params: dict = None):
        try:
            if params is not None:
                super().ValidateParameters(params=params)
                if isinstance(params, dict) and len(params.items()) > 1:
                    raise KeyError(
                        'Got more than a single parameter for subscribe: {0}'.format(", ".join(self.Parameters))
                    )
            super().SubscribeStatus(callback, params=params)
        except (KeyError, Exception) as err:
            self.DebugMessage('SubscribeStatus', err)


class ControlNameComponent(SourceComponent):
    def __init__(self, listener, pattern=None):
        super().__init__(listener, pattern=pattern)
        self.Subscriptions = {
            'Mix': {},
            'Zone': {},
            'Group': {},
            'Source': {}
        }
        self.Parameters = [k for k in self.Subscriptions]
        self.commandname = 'Name'

    def Set(self, val: str, params: dict = None) -> None:
        raise NotImplementedError

    def HandleIncomingResponse(self, response: Response, commandname: str = None) -> None:
        super().HandleIncomingResponse(response, commandname='ControlName')


class LevelComponent(SourceComponent):
    def __init__(self, listener: object, pattern: object = None) -> None:
        super().__init__(listener, pattern=pattern)
        self.commandname = 'Gain'
        self.VolumeRange = IntegerRange(-80, 0, step=1)
        self.Subscriptions = {
            'Zone': {},
            'Group': {},
            'Mix': {},
            'Source': {}
        }
        self.Parameters = ('Group', 'Zone', 'Mix', 'Source')

    def Query(self, params: dict = None) -> None:
        super().Query(params=params)

    def Set(self, val: int, params: dict = None) -> None:
        super().Set(str(val), params=params)

    def SetHooks(self, val: str, params: dict = None):
        val = int(val)
        if not (self.VolumeRange.minimum < val < self.VolumeRange.maximum):
            raise ValueError('{0} not within range {1} - {2}'.format(
                val, self.VolumeRange.minimum, self.VolumeRange.maximum
            ))

    def SubscribeStatus(self, callback: callable, params: dict = None):
        super().SubscribeStatus(callback, params=params)

    def HandleIncomingResponse(self, response: Response, commandname: str = None) -> None:
        super().HandleIncomingResponse(response, commandname='Level')


class LevelRampComponent(LevelComponent):
    def __init__(self, listener, pattern=None):
        super().__init__(listener, pattern=pattern)

    def Set(self, val: int, params: dict = None) -> None:
        try:
            self.ValidateParameters(params=params)
            command = self.commandname
            for key in params:
                self.CreateMessages(
                    int(val), params=params, key=key, commandname="{0}{1}".format(key, command), method='bmp'
                )
        except (KeyError, ValueError, Exception) as err:
            self.DebugMessage('Set', err)

    def Query(self, params: dict = None) -> None:
        raise NotImplementedError

    def ReadStatus(self, params: dict = None) -> str or int:
        raise NotImplementedError

    def SubscribeStatus(self, callback: callable, params: dict = None):
        raise NotImplementedError

    def HandleIncomingResponse(self, response: Response, commandname: str = None) -> None:
        pass


class MeterComponent(SourceComponent):
    def __init__(self, listener, pattern=None):
        super().__init__(listener, pattern=pattern)
        self.commandname = 'Meter'
        self.Subscriptions = {
            'Source': {},
            'Zone': {},
            'Group': {},
            'Mix': {}
        }
        self.Enabled = {
            'Source': {},
            'Zone': {},
            'Group': {},
            'Mix': {}
        }
        self.Parameters = [k for k in self.Subscriptions]

    def Set(self, val: str, params: dict = None) -> None:
        try:
            if val != 'On' and val != 'Off':
                raise ValueError('Value for SetMeter must be `On` or `Off`, got {0}'.format(val))
            val = True if val == 'On' else False
            self.ValidateParameters(params=params)
            for key, value in params.items():
                enabled = self.Enabled.setdefault(key, {})
                enabled[value] = val
                self.Query({key: value})
        except (KeyError, ValueError, Exception) as err:
            self.DebugMessage('Set', err)

    def Query(self, params: dict = None) -> None:
        try:
            self.ValidateParameters(params=params)
            update = AtlasIEDCommand(method="sub", params=None, fmt="str")
            command = self.commandname
            for key, value in params.items():
                enabled = self.Enabled.setdefault(key, {})
                val = enabled.setdefault(value, False)
                if val is True:
                    update.method = "sub"
                else:
                    update.method = "unsub"
                update.params = {"param": "{0}{1}_{2}".format(key, command, params[key])}
                self.listener.enqueue(Request(str(update)))
        except (KeyError, Exception) as err:
            self.DebugMessage('Query', err)


class MuteComponent(SourceComponent):
    def __init__(self, listener: object, pattern: object = None):
        super().__init__(listener, pattern=pattern)
        self.commandname = 'Mute'
        self.ValueStates = {
            "On": 1,
            "Off": 0
        }
        self.Subscriptions = {
            'Zone': {},
            'Group': {},
            'Mix': {},
            'Source': {}
        }
        self.ValueConversion = {
            "Yes": "On",
            "No": "Off"
        }
        self.Parameters = [k for k in self.Subscriptions]

    def Query(self, params: dict = None) -> None:
        super().Query(params=params)

    def ConvertMatch(self, *args, **kwargs) -> tuple:
        if args[0] == 'On' or args[0] == 'Off':
            args = (args[0], args[1], args[2])
        else:
            val = self.ValueConversion[args[0]]
            args = (val, args[1], args[2])
        return args

    def SetHooks(self, val: str, params: dict = None):
        if val not in self.ValueStates:
            raise ValueError('Value must be one of {0}'.format(", ".join(self.ValueStates)))

    def Set(self, val: str, params: dict = None):
        try:
            self.ValidateParameters(params=params)
            self.SetHooks(val, params=params)
            command = self.commandname
            for key in params:
                self.CreateMessages(self.ValueStates[val], params=params, key=key,
                                    commandname="{0}{1}".format(key, command))
        except (KeyError, Exception) as err:
            self.DebugMessage("Set", err)

    def SubscribeStatus(self, callback: callable, params: dict = None):
        super().SubscribeStatus(callback, params=params)


class GroupActiveComponent(MuteComponent):
    def __init__(self, listener: object, pattern: object = None) -> None:
        super().__init__(listener, pattern=pattern)
        self.commandname = 'Active'
        self.Pattern = re.compile(
            rb"'(Group)Active_(.*)', 'str': '(.*)'"
        )
        self.Subscriptions = {
            'Group': {},
        }
        self.Parameters = [k for k in self.Subscriptions]

    def Set(self, val: str, params: dict = None) -> None:
        super().Set(val, params=params)

    def Query(self, params: dict = None) -> None:
        super().Query(params=params)

    def SubscribeStatus(self, callback: callable, params: dict = None):
        super().SubscribeStatus(callback, params=params)

    def HandleIncomingResponse(self, response: Response, commandname: str = None) -> None:
        super().HandleIncomingResponse(response, commandname='GroupActive')


class BellScheduleComponent(SourceComponent):
    def __init__(self, listener: object, pattern: object = None) -> None:
        super().__init__(listener, pattern=pattern)
        self.commandname = "Schedule"
        self.Subscribed = False

    def Set(self, val: str, params: dict = None):
        try:
            if params is not None:
                raise KeyError('`BellSchedule` does not support parameters')
            command_params = {'param': 'TodaysBellSchedule'}
            if not self.Subscribed:
                self.Subscribed = True
                command = AtlasIEDCommand(method='sub', params=copy.deepcopy(command_params), fmt='str')
                self.listener.enqueue(Request(str(command), sender=self))
            command = AtlasIEDCommand(method='set', params=command_params, val=int(val))
            self.listener.enqueue(Request(str(command), sender=self))
        except (KeyError, Exception) as err:
            self.DebugMessage("Set", err)

    def Reinitialize(self):
        self.Query()

    def Query(self, params=None):
        try:
            if params is not None:
                raise KeyError('`BellSchedule` does not support parameters')
            update = AtlasIEDCommand(method="sub", params=None, fmt="str")
            update.params = {"param": "TodaysBellSchedule"}
            self.listener.enqueue(Request(str(update)))
        except (KeyError, ValueError) as err:
            self.DebugMessage('Update', err)

    def HandleIncomingResponse(self, response: Response, commandname: str = None) -> None:
        super().HandleIncomingResponse(response, commandname='BellSchedule')

    def SubscribeStatus(self, callback: callable, params: dict = None):
        try:
            if params is not None:
                raise ValueError('BellSchedule does not support parameter assignment')
            super().SubscribeStatus(callback, params=params)
        except (KeyError, Exception) as err:
            self.DebugMessage('SubscribeStatus', err)


class ActionComponent(SourceComponent):
    def __init__(self, listener: object, pattern: object = None) -> None:
        super().__init__(listener, pattern=pattern)
        self.Subscriptions = {
            'PlayMessage': {},
            'RecallScene': {},
            'RecallRoutine': {},
            'RecallGpoPreset': {}
        }
        self.Parameters = [k for k in self.Subscriptions]

    def HandleIncomingResponse(self, response: Response, commandname: str = None) -> None:
        pass

    def Set(self, val: str, params: dict = None) -> None:
        try:
            val = 0
            self.ValidateParameters(params=params)
            for key in params:
                command_params = {'param': '{0}_{1}'.format(key, params[key])}
                command = AtlasIEDCommand(method='set', params=command_params, val=int(val), id_tag=1)
                self.listener.enqueue(Request(str(command), sender=self))
        except (KeyError, Exception) as err:
            self.DebugMessage("Set", err)

    def Query(self, params: dict = None) -> None:
        raise NotImplementedError

    def SubscribeStatus(self, callback: callable, params: dict = None):
        raise NotImplementedError


class ZoneGroupedComponent(MuteComponent):
    def __init__(self, listener: object, pattern: object = None) -> None:
        super().__init__(listener, pattern=pattern)
        self.commandname = 'Grouped'
        self.Subscriptions = {
            'Zone': {}
        }
        self.Parameters = [k for k in self.Subscriptions]

    def HandleIncomingResponse(self, response: Response, commandname: str = None) -> None:
        super().HandleIncomingResponse(response, commandname='ZoneGrouped')

    def Query(self, params=None):
        super().Query(params=params)

    def Set(self, val: str, params: dict = None) -> None:
        raise NotImplementedError

    def SubscribeStatus(self, callback: callable, params: dict = None):
        super().SubscribeStatus(callback, params=params)


class GPOStateComponent(MuteComponent):
    def __init__(self, listener: object, pattern: object = None) -> None:
        super().__init__(listener, pattern=pattern)
        self.commandname = 'State'
        self.Subscriptions = {
            'Gpo': {}
        }
        self.Parameters = [k for k in self.Subscriptions]

    def HandleIncomingResponse(self, response: Response, commandname: str = None) -> None:
        super().HandleIncomingResponse(response, commandname='GPOState')

    def Set(self, val: str, params: dict = None) -> None:
        raise NotImplementedError

    def Query(self, params: dict = None) -> None:
        super().Query(params=params)

    def SubscribeStatus(self, callback: callable, params: dict = None):
        super().SubscribeStatus(callback, params=params)


class LoudNoiseComponent(MuteComponent):
    def __init__(self, listener: object, pattern: object = None) -> None:
        super().__init__(listener, pattern=pattern)
        self.commandname = 'LoudNoise'
        self.Subscribed = False
        self.ValueConversion = {
            "1.0": "On",
            "0.0": "Off"
        }
        self.Parameters = [k for k in self.Subscriptions]

    def HandleIncomingResponse(self, response: Response, commandname: str = None) -> None:
        super().HandleIncomingResponse(response, commandname='LoudNoise')

    def Reinitialize(self):
        self.Query()

    def Set(self, val: str, params: dict = None) -> None:
        raise NotImplementedError

    def Query(self, params=None):
        try:
            if params is not None:
                raise KeyError('`LoudNoise` does not support parameters')
            update = AtlasIEDCommand(method="sub", params=None, fmt="val")
            update.params = {"param": "LoudNoise_0"}
            self.listener.enqueue(Request(str(update)))
        except (KeyError, ValueError) as err:
            self.DebugMessage('Update', err)

    def SubscribeStatus(self, callback: callable, params: dict = None):
        try:
            if params is not None:
                raise ValueError('`LoudNoise` does not support parameter assignment')
            super().SubscribeStatus(callback, params=params)
        except (KeyError, Exception) as err:
            self.DebugMessage('SubscribeStatus', err)


class ExtronAPI:
    def Set(self, component, value, params=None):
        component = self.components.get(component)
        if component:
            try:
                component.Set(value, params=params)
            except NotImplementedError as err:
                self.debug_message(DebugTypes.INFO, "Set - {0}".format(str(err)))

    def ReadStatus(self, component, params=None):
        component = self.components.get(component)
        if component:
            try:
                return component.ReadStatus(params=params)
            except NotImplementedError as err:
                self.debug_message(DebugTypes.INFO, "ReadStatus - {0}".format(str(err)))

    def Update(self, component, params=None):
        component = self.components.get(component)
        if component:
            try:
                component.Query(params=params)
            except NotImplementedError as err:
                self.debug_message(DebugTypes.INFO, "Update - {0}".format(str(err)))

    def SubscribeStatus(self, component, callback, params=None):
        component = self.components.get(component)
        if component:
            try:
                component.SubscribeStatus(callback, params=params)
            except NotImplementedError as err:
                self.debug_message(DebugTypes.INFO, "SubscribeStatus - {0}".format(str(err)))


class ITCPTransportListener(metaclass=ABCMeta):
    @classmethod
    def __subclasshook__(cls, subclass):
        return (
                hasattr(subclass, 'connect_status_change') and
                callable(subclass.connect_status_change) and
                hasattr(subclass, 'tcp_data_received') and
                callable(subclass.tcp_data_received)
        )

    @abstractmethod
    def connect_status_change(self, ifc: object, state: ConnectionStates) -> None:
        raise NotImplementedError

    @abstractmethod
    def tcp_data_received(self, message: Response) -> None:
        raise NotImplementedError


if ENVIRON is Environ.WINDOWS:
    import sys

    program_log = sys.stderr.write
    debug = print
else:
    from extronlib.system import ProgramLog

    program_log = ProgramLog
    debug = print


class ITransportListener(metaclass=ABCMeta):
    @classmethod
    def __subclasshook__(cls, subclass):
        """ Can't implement this interface in Extron as the compiler will obfuscate the __mro__ property. """
        return True


class ITCPTransportListener(metaclass=ABCMeta):
    @classmethod
    def __subclasshook__(cls, subclass):
        return (
                hasattr(subclass, 'connect_status_change') and
                callable(subclass.connect_status_change) and
                hasattr(subclass, 'tcp_data_received') and
                callable(subclass.tcp_data_received)
        )

    @abstractmethod
    def connect_status_change(self, ifc: object, state: ConnectionStates) -> None:
        raise NotImplementedError

    @abstractmethod
    def tcp_data_received(self, message: Response) -> None:
        raise NotImplementedError


class ITransport(ITransportListener):
    __metaclass__ = ABCMeta

    @classmethod
    def __subclasshook__(cls, subclass):
        return hasattr(subclass, 'send_message') and callable(subclass.send_message)

    @abstractmethod
    def send_message(self, msg: Request) -> None:
        raise NotImplementedError


class IUDPTransportListener(metaclass=ABCMeta):
    @abstractmethod
    def udp_data_received(self, message: Response) -> None:
        raise NotImplementedError


class ITCPTransport(ITransport):
    __metaclass__ = ABCMeta

    @classmethod
    def __subclasshook__(cls, subclass):
        return (
                hasattr(subclass, 'configure') and
                callable(subclass.configure) and
                hasattr(subclass, 'connect') and
                callable(subclass.connect) and
                hasattr(subclass, 'disconnect') and
                callable(subclass.disconnect) and
                hasattr(subclass, 'is_connected') and
                callable(subclass.is_connected) and
                hasattr(subclass, 'send_message') and
                callable(subclass.send_message)
        )

    @abstractmethod
    def configure(self, transport_listener: ITCPTransportListener, ip_address: str) -> None:
        raise NotImplementedError

    @abstractmethod
    def connect(self) -> None:
        raise NotImplementedError

    @abstractmethod
    def disconnect(self) -> None:
        raise NotImplementedError

    @abstractmethod
    def is_connected(self) -> bool:
        raise NotImplementedError


class IUDPTransport(ITransport):
    __metaclass__ = ABCMeta

    @classmethod
    def __subclasshook__(cls, subclass):
        return (
                hasattr(subclass, 'configure') and
                callable(subclass.configure) and
                hasattr(subclass, 'start') and
                callable(subclass.start) and
                hasattr(subclass, 'stop') and
                callable(subclass.stop) and
                hasattr(subclass, 'restart') and
                callable(subclass.restart) and
                hasattr(subclass, 'send_message') and
                callable(subclass.send_message)
        )

    @abstractmethod
    def configure(self, transport_listener: IUDPTransportListener, ip_address: str) -> None:
        raise NotImplementedError

    def start(self) -> None:
        raise NotImplementedError

    def stop(self) -> None:
        raise NotImplementedError

    def restart(self) -> None:
        raise NotImplementedError


class UDPTransportComm(IUDPTransport):
    def __init__(
            self,
            port: int = 3131,
            buffersize: int = 2048,
            delimiter: str = '\n',
            debugger: PrinterUtil = None
    ) -> None:
        self.__client = None
        self.__listener = None
        self.buffer = ByteBuffer()
        self.buffersize = buffersize
        if debugger is None:
            self.debugger = PrinterUtil(LoggerInstance(program_log, debug))
        else:
            self.debugger = debugger
        self.delimiter = delimiter
        self.ip_address = None
        self.port = port

    @property
    def client(self) -> UDPClientInterface:
        return self.__client

    @client.setter
    def client(self, val: UDPClientInterface) -> None:
        if not isinstance(val, UDPClientInterface):
            raise TypeError('Client must be an instance of UDPClientInterface.')
        if self.__client is not None:
            self.__client.rx_data = None
            self.__client.disconnect()
            self.__client = None
        self.__client = val
        self.__client.rx_data = self.__on_client_rx_callback
        self.__client.listen()

    @property
    def listener(self) -> IUDPTransportListener:
        return self.__listener

    @listener.setter
    def listener(self, val: IUDPTransportListener) -> None:
        if not isinstance(val, IUDPTransportListener):
            raise TypeError('`listener` must implement IUDPTransportListener')
        self.__listener = val

    @staticmethod
    def transform(msg):
        return json.loads(msg.decode('utf-8'))

    def start(self) -> None:
        if self.client is not None:
            return
        else:
            if not self.ip_address:
                raise ValueError("IP address must be set to connect.")
            self.client = UDPClientInterface(
                self.ip_address, self.port, buffer_size=self.buffersize, debugger=self.debugger
            )

    def configure(self, transport_listener: IUDPTransportListener, ip_address: str) -> None:
        self.listener, self.ip_address = transport_listener, ip_address

    def debug(self, msg: str, severity: DebugTypes = None, log: bool = False) -> None:
        self.debugger.Print("UDPTransportComm - {0}".format(msg), severity=severity, log=log)

    def stop(self) -> None:
        pass

    def restart(self):
        self.stop()
        self.start()

    def is_connected(self) -> bool:
        return self.client.connected

    def __on_client_rx_callback(self, client: UDPClientInterface, data: bytes) -> None:
        if len(data) == 0:
            return
        else:
            self.buffer.append(data)
            while len(self.buffer) > 0:
                try:
                    start = self.buffer.index("{")
                    end = self.buffer.index("]")
                    if 0 <= start < end:
                        msg = b''.join(self.buffer[start:end + 2])
                        self.buffer.delete(start, end + 2)
                        payload = self.transform(msg)
                        self.listener.udp_data_received(Response(payload))
                except (ValueError, Exception):
                    self.buffer.clear()
                    break

    def send_message(self, message: Request) -> None:
        if self.client is not None:
            self.client.send_data('{0}{1}'.format(str(message), self.delimiter))


class TCPTransportComm(ITCPTransport):
    def __init__(
            self,
            port: int = 5321,
            buffersize: int = 2048,
            delimiter: str = '\n',
            debugger: PrinterUtil = None
    ) -> None:
        self.__client = None
        self.__listener = None
        self.buffer = ByteBuffer()
        self.buffersize = buffersize
        if debugger is None:
            self.debugger = PrinterUtil(LoggerInstance(program_log, debug))
        else:
            self.debugger = debugger
        self.delimiter = delimiter
        self.ip_address = None
        self.port = port

    @property
    def client(self) -> TCPClientInterface:
        return self.__client

    @client.setter
    def client(self, val: TCPClientInterface) -> None:
        if not isinstance(val, TCPClientInterface):
            raise TypeError('Client must be an instance of TCPClientInterface.')
        if self.__client is not None:
            self.__client.rx_data = None
            self.__client.socket_status_changed = None
            self.__client.disconnect()
            self.__client = None
        self.__client = val
        self.__client.socket_status_changed = self.__socket_status_changed
        self.__client.rx_data = self.__on_client_rx_callback
        self.__client.connect()
        self.__client.listen()

    @property
    def listener(self) -> ITCPTransportListener:
        return self.__listener

    @listener.setter
    def listener(self, val: ITCPTransportListener) -> None:
        if not isinstance(val, ITCPTransportListener):
            raise TypeError('`listener` must implement ITCPTransportListener')
        self.__listener = val

    @staticmethod
    def transform(msg):
        return json.loads(msg.decode('utf-8'))

    def connect(self) -> None:
        if self.client is not None:
            self.client.connect()
        else:
            if not self.ip_address:
                raise ValueError("IP address must be set to connect.")
            self.client = TCPClientInterface(
                self.ip_address, self.port, buffer_size=self.buffersize, debugger=self.debugger
            )

    def configure(self, transport_listener: ITCPTransportListener, ip_address: str) -> None:
        self.listener, self.ip_address = transport_listener, ip_address

    def debug(self, msg: str, severity: DebugTypes = None, log: bool = False) -> None:
        self.debugger.Print("TCPTransportComm - {0}".format(msg), severity=severity, log=log)

    def disconnect(self) -> None:
        if self.client is not None:
            self.client.disconnect()

    def is_connected(self) -> bool:
        return self.client.connected

    def __socket_status_changed(self, ifc: object, status: ConnectionStates) -> None:
        if self.listener is not None:
            self.listener.connect_status_change(ifc, status)

    def __on_client_rx_callback(self, client: TCPClientInterface, data: bytes) -> None:
        if len(data) == 0:
            return
        else:
            self.buffer.append(data)
            while len(self.buffer) > 0:
                try:
                    index = self.buffer.index("\n")
                    msg = b''.join(self.buffer[:index + 1])
                    self.buffer.delete(0, index + 1)
                    payload = self.transform(msg)
                    self.listener.tcp_data_received(Response(payload))
                except (ValueError, Exception) as err:
                    break

    def send_message(self, message: Request) -> None:
        if self.client is not None:
            self.client.send_data('{0}{1}'.format(str(message), self.delimiter))


class CommandProcessor(IProcessor, ICommUtil, ITCPTransportListener, IUDPTransportListener, ExtronAPI):
    def __init__(self, heartbeat_time=30, response_time=5, logger=None):
        self.ip_address = None
        self.__connection = None
        self.__components = {
            'Source': SourceComponent(
                self,
                pattern=re.compile(
                    rb"'(Zone|Group)Source_(.*)', 'str': '(.*)'"
                )
            ),
            'Level': LevelComponent(
                self,
                pattern=re.compile(
                    rb"'(Zone|Group|Mix|Source)Gain_(.*)', 'str': '(.*)'"
                )
            ),
            'Mute': MuteComponent(
                self,
                pattern=re.compile(
                    rb"'(Zone|Group|Mix|Source)Mute_(.*)', 'str': '(.*)'"
                )
            ),
            'GroupActive': GroupActiveComponent(
                self,
                pattern=re.compile(
                    rb"'(Group)Active_(.*)', 'str': '(.*)'"
                )
            ),
            'ZoneGrouped': ZoneGroupedComponent(
                self,
                pattern=re.compile(
                    rb"'(Zone)Grouped_(.*)', 'str': '(.*)'"
                )
            ),
            'BellSchedule': BellScheduleComponent(
                self,
                pattern=re.compile(
                    rb"'(TodaysBellSchedule)', 'str': '(.*)'"
                )
            ),
            'Action': ActionComponent(
                self,
                pattern=None
            ),
            'LevelRamp': LevelRampComponent(
                self,
                pattern=None
            ),
            'GPOState': GPOStateComponent(
                self,
                pattern=re.compile(
                    rb"'(Gpo)State_(.*)', 'str': '(.*)'"
                )
            ),
            'ControlName': ControlNameComponent(
                self,
                pattern=re.compile(
                    rb"'(Zone|Group|Mix|Source)Name_(.*)', 'str': '(.*)'"
                )
            ),
            'Meter': MeterComponent(
                self,
                pattern=re.compile(
                    rb"'(Zone|Group|Mix|Source)Meter_(.*)', 'str': '(.*)'"
                )
            ),
            'LoudNoise': LoudNoiseComponent(
                self,
                pattern=re.compile(
                    rb"'(Loud)Noise_(.*)', 'val': '(.*)'"
                )
            )
        }
        self.__debug = False
        self.__debug_enabled = None
        if logger is None:
            self.__debugger = PrinterUtil(LoggerInstance(program_log, debug))
        else:
            self.__debugger = logger
        self.__heartbeat_time = heartbeat_time
        self.__initializing = None
        self.__last_out = None
        self.__message_package = None
        self.__metering = None
        self.__passback = False
        self.__priority_comm_util = PriorityCommUtil(self)
        self.__reinit_timer = None
        self.__response_time = response_time
        self.__transport = None
        self.__initialized = False
        self.__initiating = True
        self.__Initialized = None
        self.__Communicating = None

    def __initialize_components(self):
        """ Initialize the components. """
        for _, component in self.__components.items():
            component.Reinitialize()

    @staticmethod
    def __is_response_type(response: Response) -> bool:
        if isinstance(response, Response):
            return True
        else:
            raise TypeError("TCPDataReceived got invalid object type")

    def __needs_initializing(self, response: Response) -> bool:
        if isinstance(self.__last_out, Request) and self.__last_out.sender is self:
            try:
                params = response.message['params'][0]
                return params['param'] == 'FirmwareVersion'
            except (IndexError, Exception) as err:
                return False
        return False

    def __update_passback(self, response: Response) -> None:
        try:
            if self.Passback:
                self.__components['Passback'](response.message, params={
                    'CommandProcessor': self
                })
        except (KeyError, TypeError):
            self.debug_message(DebugTypes.INFO, "TCPDataReceived - No Component `Passback`")

    @property
    def communicating(self) -> bool:
        return self.__connection is ConnectionStates.CONNECTED

    @property
    def components(self):
        return self.__components

    @property
    def get_response_time(self) -> int:
        return self.__response_time

    @property
    def get_heartbeat_time(self) -> int:
        return self.__heartbeat_time

    @property
    def id(self):
        return hex(id(self))

    @property
    def is_initializing(self) -> bool:
        return self.__connection is ConnectionStates.INITIALIZING

    @property
    def is_initialized(self):
        return self.__initialized

    @property
    def Initialized(self):
        return self.__Initialized

    @Initialized.setter
    def Initialized(self, handler):
        if callable(handler):
            self.__Initialized = handler
        else:
            raise TypeError('"handler" is not callable.')

    @property
    def Communicating(self):
        return self.__Communicating

    @Communicating.setter
    def Communicating(self, handler):
        if callable(handler):
            self.__Communicating = handler
        else:
            raise TypeError('"handler" is not callable.')

    @property
    def Connect(self):
        return self.connect

    @property
    def Debug(self) -> bool:
        return self.__debug

    @Debug.setter
    def Debug(self, val: bool) -> None:
        if val is True or val is False:
            self.__debug = val
            self.__debugger.debug = "ON" if True else "OFF"
            if self.__debug_enabled is not None:
                self.__debug_enabled(self, val)
            self.debug_message(DebugTypes.INFO, "EnableDebug - {0}".format(val))
        else:
            raise TypeError("Value for `debug` must be either True or False")

    @property
    def DebugEnabled(self):
        return self.__debug_enabled

    @DebugEnabled.setter
    def DebugEnabled(self, handler):
        if callable(handler):
            self.__debug_enabled = handler
        else:
            raise TypeError('"handler" is not callable')

    @property
    def IPAddress(self):
        return self.ip_address

    @IPAddress.setter
    def IPAddress(self, val):
        self.ip_address = val

    @property
    def metering(self) -> object:
        return self.__metering

    @metering.setter
    def metering(self, val: object):
        if self.__metering is not None:
            self.__metering.disconnect()
            self.__metering = None
        self.__metering = val

    @property
    def Passback(self) -> bool:
        return self.__passback

    @Passback.setter
    def Passback(self, val: bool) -> None:
        self.__passback = val

    @property
    def Reinitialize(self):
        return self.reinitialize

    @property
    def transport(self) -> TCPTransportComm:
        return self.__transport

    @transport.setter
    def transport(self, val: TCPTransportComm) -> None:
        if not isinstance(val, TCPTransportComm):
            raise TypeError('`transport` must be an instance of TCPTransportComm')
        if self.__transport is not None:
            self.__transport.disconnect()
            self.__transport = None
        self.__transport = val

    def connect(self) -> None:
        if self.ip_address is not None:
            self.debug_message(DebugTypes.INFO, "Connect")
            if self.__transport is None:
                self.__transport = TCPTransportComm(debugger=self.__debugger)
            if self.__metering is None:
                self.__metering = UDPTransportComm(debugger=self.__debugger)
                self.__metering.configure(self, self.ip_address)
                self.__metering.start()
            if self.__priority_comm_util is None:
                self.__priority_comm_util = PriorityCommUtil(self)
            self.__transport.configure(self, self.ip_address)
            self.__transport.connect()
            self.__priority_comm_util.start()

    def connect_status_change(self, ifc: object, state: ConnectionStates) -> None:
        self.__connection = state
        if state is ConnectionStates.ERROR:
            self.debug_message(DebugTypes.ERROR, "ConnectStatusChange - Connection Error")
        elif state is ConnectionStates.CONNECTED:
            self.debug_message(DebugTypes.INFO, "ConnectStatusChange - Connected To Device")
        elif state is ConnectionStates.DISCONNECTED:
            self.debug_message(DebugTypes.INFO, "ConnectStatusChange - Disconnected From Device")
        if self.Communicating is not None:
            self.Communicating(self, self.__connection is ConnectionStates.CONNECTED)
        if self.__connection is not ConnectionStates.CONNECTED:
            self.__initialized = False
            if self.Initialized is not None:
                self.Initialized(self, self.__initialized)

    def debug_message(self, severity: DebugTypes, msg: str, params: dict = None, stderr=False) -> None:
        if self.Debug:
            msg = "AtlasIED Atmosphere ({0}) {1}".format(self.ip_address, msg)
            self.__debugger.Print(msg, severity=severity, log=stderr)

    def disconnect(self):
        self.__transport.disconnect()
        self.__priority_comm_util.stop()
        self.__initialized = False
        if self.Initialized is not None:
            self.Initialized(self, self.__initialized)

    def dispatch(self, response: Response) -> None:
        for _, v in self.__components.items():
            v.HandleIncomingResponse(response)

    def enqueue(self, request: Request, priority: int = 0) -> None:
        if self.communicating:
            self.__priority_comm_util.enqueue(request.message, priority=priority)

    def failed_response(self) -> None:
        self.debug_message(DebugTypes.INFO, "FailedResponse")
        self.__last_out = None

    def get_initialized(self):
        self.__initialize_components()
        self.__initialized = True
        if self.Initialized is not None:
            self.Initialized(self, self.__initialized)

    def process_response(self, response: Response) -> None:
        try:
            self.dispatch(response)
        except (ValueError, Exception) as err:
            self.debug_message(
                DebugTypes.INFO,
                "ProcessResponse {0} - {1}".format(type(err).__name__, str(err))
            )

    def reinitialize(self):
        self.reconnect()

    def reconnect(self):
        self.disconnect()
        self.connect()

    def reinit_timer_event(self):
        if self.__reinit_timer is not None:
            self.reinitialize()
            self.__reinit_timer.reset(20)
        else:
            self.__reinit_timer = CCITimer(
                self.reinit_timer_event,
                20
            )

    def send_trace(self, msg: str) -> None:
        self.debug_message(DebugTypes.INFO, "SendTrace - {0}".format(msg))

    def send_message(self, request: Request) -> None:
        if not isinstance(request, Request):
            raise TypeError('AtlasIED {0} - SendMessage expected `Request` type, instead got {1}'.format(
                self.ip_address,
                type(request)
            ))
        self.__last_out = request
        self.__last_out.sender = self
        self.debug_message(DebugTypes.INFO, "SendMessage - {0}".format(request.encode("unicode_escape")))
        if self.__transport is not None:
            self.__transport.send_message(request)

    def send_heartbeat(self) -> None:
        if self.__priority_comm_util is not None and self.__priority_comm_util.is_ready:
            self.debug_message(DebugTypes.INFO, "SendHeartbeat")
            heartbeat = '{"jsonrpc":"2.0","method":"get","params":{"param":"FirmwareVersion","fmt":"str"}}\n'
            request = Request(heartbeat)
            self.__priority_comm_util.enqueue(request.message)

    def strikeout(self) -> None:
        self.debug_message(DebugTypes.INFO, "Strikeout")
        if self.__reinit_timer is not None:
            self.__reinit_timer.stop()
            self.__reinit_timer.reset(10)
        else:
            self.__reinit_timer = CCITimer(
                self.reinit_timer_event,
                10
            )

    def tcp_data_received(self, response: Response) -> None:
        if self.__reinit_timer is not None:
            self.__reinit_timer.stop()

        try:
            self.debug_message(DebugTypes.INFO, "TCPDataReceived - {0}".format(response.message))
            self.__is_response_type(response)
        except TypeError as err:
            self.debug_message(DebugTypes.INFO, str(err))
            return

        self.__update_passback(response)

        if isinstance(self.__last_out, Request):
            self.__priority_comm_util.good_response()
            if self.__needs_initializing(response) and not self.__initialized:
                self.get_initialized()
                try:
                    self.__components['FirmwareVersion'](response.message, params=None)
                except KeyError:
                    self.debug_message(
                        DebugTypes.INFO,
                        "TCPDataReceived - No Component `FirmwareVersion`"
                    )
            else:
                self.process_response(response)
            if not self.__initialized and self.is_initialized:
                self.__initializing = False
                self.__initialized = True
                if self.Initialized is not None:
                    self.Initialized(self, self.__initialized)
        else:
            self.process_response(response)

    def udp_data_received(self, message: Response) -> None:
        @Wait(0.1)
        def __match_level():
            self.__components['Meter'].HandleIncomingResponse(message)
