import socket
from logging import getLogger

from externals.device import SerialDevice
from nperipherals_core.interactor.print_interactor import parse_commands
from nperipherals_core.model.custom_exceptions import DeviceNotConnected, PrinterDoorOpened, PrinterNotConnected
from nperipherals_core.model.device import WriterDevice, CashDrawer
from nperipherals_core.model.device_status import CashDrawerStatus, DeviceStatus
from typing import Dict

logger = getLogger(__name__)


class SerialPosPrinterDevice(WriterDevice, SerialDevice, CashDrawer):
    REQUEST_STATUS_CODE = b'\x10\x04\x01'
    SOCKET_BUF_SIZE = 1

    def __init__(
            self,
            device_configurations,  # type: Dict[unicode, unicode]
            tags_file               # type: Dict
    ):
        # type: (...) -> None

        super(SerialPosPrinterDevice, self).__init__(logger, device_configurations)

        self.tags_file = tags_file
        self.pre_commands = parse_commands(self.tags_file, device_configurations.get("PrePrintCmds", []))
        self.post_commands = parse_commands(self.tags_file, device_configurations.get("PostPrintCmds", []))
        self.write_retries = int(device_configurations.get("Retries", 1))
        self.write_retry_sleep_in_seconds = float(device_configurations.get("RetriesSleepSeconds", 1.0))

    def _get_bin_status(
            self
    ):
        # type: (...) -> str

        self.serial.write(self.REQUEST_STATUS_CODE)
        status = self.serial.read(self.SOCKET_BUF_SIZE)

        return format(ord(status), '08b')

    def get_status(
            self
    ):
        # type: (...) -> DeviceStatus

        bin_status = self._get_bin_status()

        if "1" == bin_status[-6]:
            raise PrinterDoorOpened()

        if "1" == bin_status[-4]:
            raise PrinterNotConnected()

        return DeviceStatus.Ok

    def get_drawer_status(
            self
    ):
        # type: (...) -> CashDrawerStatus

        try:
            bin_status = self._get_bin_status()

            if "0" == bin_status[-3]:
                return CashDrawerStatus.Opened

            return CashDrawerStatus.Closed

        except (socket.error, AttributeError):
            raise DeviceNotConnected()

    def write(
            self,
            data    # type: unicode
    ):
        # type: (...) -> None

        try:
            if self.pre_commands:
                self.serial.write(self.pre_commands)

            self.serial.write(data)

            if self.post_commands:
                self.serial.write(self.post_commands)

        except (socket.error, AttributeError):
            raise DeviceNotConnected()

    def open_drawer(
            self
    ):
        # type: (...) -> None

        try:
            self._send_open_drawer_command()
        except (socket.error, AttributeError) as _:
            raise DeviceNotConnected()

    def _send_open_drawer_command(
            self
    ):
        # type: (...) -> None

        cash_drawer_pin_2_pulse = b'\x1bp\x0022'
        cash_drawer_pin_5_pulse = b'\x1bp\x0122'

        self.serial.write(cash_drawer_pin_2_pulse)
        self.serial.write(cash_drawer_pin_5_pulse)

    def get_write_retries(
            self
    ):
        # type: (...) -> int

        return self.write_retries

    def get_write_retry_sleep_in_seconds(
            self
    ):
        # type: (...) -> float

        return self.write_retry_sleep_in_seconds
