import time
from logging import getLogger

from modelcommon.printer import SeparatorTag
from nperipherals_core.interactor.print_interactor import parse_tags
from nperipherals_core.model.custom_exceptions import FailedWritingToDevice
from nperipherals_core.model.device import WriterDevice, ReaderDevice
from typing import Union, Dict

logger = getLogger(__name__)


class PrintInteractor(object):
    def __init__(
            self,
            writer_device,  # type: Union[ReaderDevice, WriterDevice]
            tags_file       # type: Dict
    ):
        # type: (...) -> None

        self.writer_device = writer_device
        self.tags_file = tags_file

    def print_data(
            self,
            data    # type: str
    ):
        # type: (...) -> None

        device_name = self.writer_device.get_name()
        write_retries = self.writer_device.get_write_retries()
        write_retry_sleep_in_seconds = self.writer_device.get_write_retry_sleep_in_seconds()

        logger.info("Writing to device [{}]".format(device_name))

        error_messages = []
        for i in range(write_retries):
            connected = False
            try:
                self.writer_device.initialize()
                self.writer_device.connect()
                connected = True
                data = self._replace_tags_print(data)
                self.writer_device.write(data)
                return
            except Exception as ex:
                error_messages.append("Exception #[{}]: [{}]".format(i, repr(ex)))
                logger.warning("Error writing to device [{}]".format(device_name), exc_info=True)
                time.sleep(write_retry_sleep_in_seconds)
            finally:
                if connected:
                    self.writer_device.disconnect()

        raise FailedWritingToDevice("|".join(error_messages))

    def _replace_tags_print(
            self,
            data    # type: str
    ):
        # type: (...) -> str

        while True:

            if b"\x00\x02" not in data:
                break
            try:
                start_index = data.index(b"{}".format(SeparatorTag.start))
                end_index = data.index(b"{}".format(SeparatorTag.end), start_index)
                key = data[start_index + 2:end_index].strip()
                cols = self.tags_file["ColsPrinter"] if "ColsPrinter" in self.tags_file else 56
                if key in self.tags_file:
                    parsed = parse_tags(self.tags_file[key], key, cols)
                    data = self._replacer(data, parsed, start_index, end_index)
                    continue

                data = self._replacer(data, "", start_index, end_index)
            except Exception as _:
                logger.exception("Error replace tags to print. Data: {}".format(data))
                break

        return data

    @staticmethod
    def _replacer(
            data,               # type: str
            replace_string,     # type: str
            start,              # type: int
            end                 # type: int
    ):
        # type: (...) -> str

        if start not in range(len(data)):
            raise ValueError("index outside given string")

        if start < 0:
            return replace_string + data
        if start > len(data):
            return data + replace_string

        return data[:start] + replace_string + data[end+2:]
