import time
import struct
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, List

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):
            try:
                self.writer_device.initialize()
                self.writer_device.connect()
                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 [{}] - Retry: {}".format(device_name, str(i)), exc_info=True)
                time.sleep(write_retry_sleep_in_seconds)
            finally:
                self.writer_device.disconnect()

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

    @staticmethod
    def _to_int(byte):

        # type: (...) -> str

        return struct.unpack('<B', byte)[0]

    def split_image_from_data(
            self,
            data,       # type: str
            new_data    # type: List
    ):
        # type: (...) -> List

        if b'\x1D' + b'v0' in data:
            index = data.find(b'\x1D' + b'v0')
            struct.unpack('<B', data[index + 5])
            width = self._to_int(data[index + 4]) + self._to_int(data[index + 5]) * 256
            height = self._to_int(data[index + 6]) + self._to_int(data[index + 7]) * 256
            length = width * height
            new_data.append((1, data[0:index]))
            new_data.append((0, data[index:index + length + 8]))
            return self.split_image_from_data(data[index + length + 8:], new_data)
        else:
            return new_data.append((1, data)) if len(data) > 0 else new_data

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

        index = 0
        data_split = []
        self.split_image_from_data(data, data_split)

        while index < len(data_split):
            if data_split[index][0] == 0 or b"\x00\x02" not in data_split[index][1]:
                index += 1
                continue
            else:
                segment = data_split[index][1]

            try:
                start_index = segment.index(b"{}".format(SeparatorTag.start))
                end_index = segment.index(b"{}".format(SeparatorTag.end), start_index)
                key = segment[start_index + 2:end_index].strip()
                cols = self.tags_file["ColsPrinter"] if "ColsPrinter" in self.tags_file else 56
                parsed = ""
                if key in self.tags_file:
                    parsed = parse_tags(self.tags_file[key], key, cols)

                data_split[index] = (1, self._replacer(segment, parsed, start_index, end_index))
            except Exception as _: # noqa
                logger.exception("Error replace tags to print. Data: {}".format(segment))
                break

        return ''.join([x[1] for x in data_split])

    @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:]
