"""
Brother P-Touch P700 Driver
"""
import io
import random
import struct
import time
from collections import namedtuple
from enum import Enum, IntEnum
from itertools import chain, islice
from pprint import pprint
from typing import Iterable, Sequence
from math import ceil
import logging

import packbits
from PIL import Image, ImageChops

from ..label import Label
from . import BasePrinter, BaseStatus, BaseErrorStatus

logger = logging.getLogger(__name__)


def batch_iter_bytes(b, size):
    i = iter(b)

    return iter(lambda: bytes(tuple(islice(i, size))), b"")


class INFO_OFFSETS(IntEnum):
    PRINTHEAD_MARK = 0
    MODEL_CODE = 4
    ERROR_1 = 8
    ERROR_2 = 9
    MEDIA_WIDTH = 10
    MEDIA_TYPE = 11
    MODE = 15
    MEDIA_LENGTH = 17
    STATUS_TYPE = 18
    PHASE_TYPE = 19
    PHASE_NUMBER_HI = 20
    PHASE_NUMBER_LO = 21
    NOTIFY_NO = 22
    HARDWARE_SETTINGS = 26


class ERRORS(Enum):
    NO_MEDIA = 0
    CUTTER_JAM = 2
    WEAK_BATTERY = 3
    HV_ADAPTER = 6
    REPLACE_MEDIA = 8
    COVER_OPEN = 12
    OVERHEATING = 13
    UNKNOWN = -1


class MEDIA_TYPE(Enum):
    NO_MEDIA = 0
    LAMINATED_TAPE = 1
    NON_LAMINATED_TAPE = 2
    HEAT_SHRINK = 3
    INCOMPATIBLE = 4


class STATUS_TYPE(Enum):
    STATUS_REPLY = 0
    PRINTING_DONE = 1
    ERROR_OCCURRED = 2
    TURNED_OFF = 3
    NOTIFICATION = 4
    PHASE_CHANGE = 5


STATUS_TYPE_MAP = {
    0x00: STATUS_TYPE.STATUS_REPLY,
    0x01: STATUS_TYPE.PRINTING_DONE,
    0x02: STATUS_TYPE.ERROR_OCCURRED,
    0x04: STATUS_TYPE.TURNED_OFF,
    0x05: STATUS_TYPE.NOTIFICATION,
    0x06: STATUS_TYPE.PHASE_CHANGE,
}

MEDIA_TYPE_MAP = {
    0x00: MEDIA_TYPE.NO_MEDIA,
    0x01: MEDIA_TYPE.LAMINATED_TAPE,
    0x02: MEDIA_TYPE.NON_LAMINATED_TAPE,
    0x11: MEDIA_TYPE.HEAT_SHRINK,
    0xFF: MEDIA_TYPE.INCOMPATIBLE,
}

TapeInfo = namedtuple("TapeInfo", ["lmargin", "printarea", "rmargin", "width"])

MEDIA_WIDTH_INFO = {
    # media ID to tape width in dots
    0: TapeInfo(None, None, None, None),
    4: TapeInfo(52, 24, 52, 3.5),
    6: TapeInfo(48, 32, 48, 6.0),
    9: TapeInfo(39, 50, 39, 9.0),
    12: TapeInfo(29, 70, 29, 12.0),
    18: TapeInfo(8, 112, 8, 18.0),
    24: TapeInfo(0, 128, 0, 24.0),
}

ERROR_MASK = {
    0: ERRORS.NO_MEDIA,
    2: ERRORS.CUTTER_JAM,
    3: ERRORS.WEAK_BATTERY,
    6: ERRORS.HV_ADAPTER,
    8: ERRORS.REPLACE_MEDIA,
    12: ERRORS.COVER_OPEN,
    13: ERRORS.OVERHEATING,
}


def encode_line(bitmap_line: bytes, tape_info: TapeInfo) -> bytes:
    # The number of bits we need to add left or right is not always a multiple
    # of 8, so we need to convert our line into an int, shift it over by the
    # left margin and convert it to back again, padding to 16 bytes.

    line_int = int.from_bytes(bitmap_line, byteorder='big')
    line_int <<= tape_info.rmargin
    padded = line_int.to_bytes(16, byteorder='big')

    # pad to 16 bytes
    compressed = packbits.encode(padded)
    logger.debug("original bitmap: %s", bitmap_line)
    logger.debug("padded bitmap %s", padded)
    logger.debug("packbits compressed %s", compressed)
    # <h: big endian short (2 bytes)
    prefix = struct.pack("<H", len(compressed))

    return prefix + compressed


class Errors(BaseErrorStatus):
    def __init__(self, byte1: int, byte2: int) -> None:
        value = byte1 | (byte2 << 8)
        self.data = {
            err.name.lower(): bool(value & 1 << offset)

            for offset, err in ERROR_MASK.items()
        }

    def any(self):
        return any(self.data.values())

    def __getattr__(self, attr):
        return self.data[attr]

    def __repr__(self):
        return "<Errors {}>".format(self.data)


class Status(BaseStatus):
    def __init__(self, msg: Sequence) -> None:
        self.data = {i.name.lower(): msg[i.value] for i in INFO_OFFSETS}

        self.errors = Errors(self.error_1, self.error_2)
        self.tape_info = MEDIA_WIDTH_INFO[self.media_width]

    def ready(self):
        return not self.errors.any()

    def __getattr__(self, attr):
        return self.data[attr]


class P700(BasePrinter):
    """Printer Class for the Brother P-Touch P700/PT-700 Printer"""
    DPI = (180, 180)

    def connect(self) -> None:
        """Connect to Printer"""
        self.io.write(b'\x00' * 100)
        self.io.write(b'\x1b\x40')

        logger.info("connected")

    def get_status(self) -> Status:
        """get status of the printer as ``Status`` object"""
        with self.io.lock:
            self.io.write(b'\x1B\x69\x53')
            data = self.io.read(32)

            if not data:
                raise IOError("No Response from printer")

            if len(data) < 32:
                raise IOError("Invalid Response from printer")

            return Status(data)

    def _debug_status(self):
        data = self.io.read(32)

        if data:
            logger.debug(Status(data))

    def get_label_width(self):
        return self.get_status().tape_info.width

    def print_label(self, label: Label) -> Status:
        status = self.get_status()
        if not status.ready():
            raise IOError("Printer is not ready")

        img = label.render(height=status.tape_info.printarea)
        logger.debug("printarea is %s dots", status.tape_info.printarea)
        if not img.mode == "1":
            raise ValueError("render output has invalid mode '1'")
        img = img.transpose(Image.ROTATE_270).transpose(
            Image.FLIP_TOP_BOTTOM)
        img = ImageChops.invert(img)

        logger.info("label output size: %s", img.size)
        logger.info("tape info: %s", status.tape_info)

        img_bytes = img.tobytes()

        with self.io.lock:
            self._raw_print(
                status, batch_iter_bytes(img_bytes, ceil(img.size[0] / 8)))

        # wait for label to finish printing
        time.sleep(len(img_bytes)/1000 if len(img_bytes)/1000 > 5 else 5)

        return self.get_status()

    def _dummy_print(self, status: Status, document: Iterable[bytes]) -> None:
        for line in document:
            encode_line(line, status.tape_info)

    def _raw_print(self, status: Status, document: Iterable[bytes]) -> None:
        logger.info("starting print")

        self.connect()

        # raster mode
        self.io.write(b'\x1B\x69\x69\x01')

        # Various mode
        self.io.write(b'\x1B\x69\x4D\x40')

        # Advanced mode
        self.io.write(b'\x1B\x69\x4B\x08')

        # margin
        self.io.write(b'\x1B\x69\x64\x0E\x00')

        # Compression mode
        self.io.write(b'\x4D\x02')

        for line in document:
            self.io.write(b'G' + encode_line(line, status.tape_info))

        self.io.write(b'\x5A')

        # end page
        self.io.write(b'\x1A')
        logger.info("end of page")