import asyncio
import functools
import logging
import time

from pycec.commands import CecCommand, KeyPressCommand, KeyReleaseCommand, \
    PollCommand
from pycec.const import CMD_STANDBY, KEY_POWER
from pycec.network import AbstractCecAdapter, HDMINetwork

DEFAULT_PORT = 9526
MAX_CONNECTION_ATTEMPTS = 5
CONNECTION_ATTEMPT_DELAY = 3
_LOGGER = logging.getLogger(__name__)


# pragma: no cover
class TcpAdapter(AbstractCecAdapter):
    def __init__(self, host, port=DEFAULT_PORT, name=None,
                 activate_source=None):
        super().__init__()
        self._polling = dict()
        self._command_callback = None
        self._tcp_loop = asyncio.new_event_loop()
        self._host = host
        self._port = port
        self._transport = None
        self._osd_name = name
        self._activate_source = activate_source

    def _after_init(self, callback, f):
        _LOGGER.debug("New client: %s", self._transport)
        self._initialized = True
        if callback:
            callback()
        self._tcp_loop.run_in_executor(None, self._tcp_loop.run_forever)

    def _init(self):
        for i in range(0, MAX_CONNECTION_ATTEMPTS):
            try:
                self._transport, protocol = self._tcp_loop.run_until_complete(
                    self._tcp_loop.create_connection(lambda: TcpProtocol(self),
                                                     host=self._host,
                                                     port=self._port))
                _LOGGER.debug("Connection started.")
                break
            except ConnectionRefusedError as e:
                _LOGGER.warning(
                    "Unable to connect due to %s. Trying again in %d seconds, "
                    "%d attempts remaining.",
                    e, CONNECTION_ATTEMPT_DELAY, MAX_CONNECTION_ATTEMPTS - i)
                time.sleep(CONNECTION_ATTEMPT_DELAY)
        else:
            _LOGGER.error("Unable to connect! Giving up.")
            self.shutdown()

    def init(self, callback: callable = None):
        _LOGGER.debug("Starting connection...")
        task = self._loop.run_in_executor(None, self._init)
        task.add_done_callback(functools.partial(self._after_init, callback))
        return task

    def shutdown(self):
        self._initialized = False
        if self._transport:
            self._transport.close()

    def _poll_device(self, device):
        req = self._loop.time()
        poll_bucket = self._polling.get(device, set())
        poll_bucket.add(req)
        self._polling.update({device: poll_bucket})
        self.transmit(PollCommand(device))
        while True:
            if req not in self._polling.get(device, set()):
                _LOGGER.debug("Found device %d.", device)
                return True
            if self._loop.time() > (req + 5):
                return False
            asyncio.sleep(.1, loop=self._loop)

    def poll_device(self, device):
        return self._loop.run_in_executor(None, self._poll_device, device)

    def get_logical_address(self):
        return 0xf

    def standby_devices(self):
        self.transmit(CecCommand(CMD_STANDBY))

    def transmit(self, command: CecCommand):
        self._transport.write(("%s\n" % command.raw).encode())

    def set_command_callback(self, callback):
        self._command_callback = callback

    def power_on_devices(self):
        self.transmit(KeyPressCommand(KEY_POWER))
        self.transmit(KeyReleaseCommand())

    def set_transport(self, transport):
        self._transport = transport


class TcpProtocol(asyncio.Protocol):
    buffer = ''

    def __init__(self, adapter: TcpAdapter):
        self._adapter = adapter
        self.transport = None

    def connection_made(self, transport):
        self.transport = transport
        self._adapter.set_transport = transport

    def data_received(self, data: bytes):
        self.buffer += bytes.decode(data)
        for line in self.buffer.splitlines(keepends=True):
            if line.count('\n'):
                line = line.rstrip()
                _LOGGER.debug("Received %s from %s", line,
                              self.transport.get_extra_info('peername'))
                if len(line) == 2:
                    cmd = CecCommand(line)
                    if cmd.src in self._adapter._polling:
                        del self._adapter._polling[cmd.src]
                else:
                    self._adapter._command_callback("<< " + line)
                self.buffer = ''
            else:
                self.buffer = line

    def eof_received(self):
        self._adapter.shutdown()

    def connection_lost(self, exc):
        self._adapter.shutdown()
        _LOGGER.warning("Connection lost. Trying to reconnect...")
        self._adapter._tcp_loop.stop()
        self._adapter.init()


def main():
    """For testing purpose"""
    tcp_adapter = TcpAdapter("192.168.1.3", name="HASS", activate_source=False)
    hdmi_network = HDMINetwork(tcp_adapter)
    hdmi_network.start()
    while True:
        for d in hdmi_network.devices:
            _LOGGER.info("Device: %s", d)

        time.sleep(7)


if __name__ == '__main__':
    # Configure logging
    _LOGGER.setLevel(logging.DEBUG)
    ch = logging.StreamHandler()
    ch.setLevel(logging.DEBUG)
    try:
        from colorlog import ColoredFormatter

        formatter = ColoredFormatter(
            "%(log_color)s%(levelname)-8s %(message)s",
            datefmt=None,
            reset=True,
            log_colors={
                'DEBUG': 'cyan',
                'INFO': 'green',
                'WARNING': 'yellow',
                'ERROR': 'red',
                'CRITICAL': 'red',
            }
        )
    except ImportError:
        formatter = logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - %(message)s')

    ch.setFormatter(formatter)
    _LOGGER.addHandler(ch)

    main()
