#!/usr/bin/python
# -*-coding:utf-8-*-
__author__ = "ihciah"

import struct, zlib, logging
from bluetooth import BluetoothSocket, find_service, RFCOMM, discover_devices
from const import BtCommandByte


class BtManager:
    standardKey = 0x35769521
    padding_line = 300
    max_send_msg_length = 2016
    max_recv_msg_length = 1024
    uuid = "00001101-0000-1000-8000-00805F9B34FB"

    def __init__(self, address=None):
        self.address = address
        self.crckeyset = False
        self.connected = True if self.connect() else False

    def connect(self):
        if self.address is None and not self.scandevices():
            return False
        if not self.scanservices():
            return False
        logging.info("Service found. Connecting to \"%s\" on %s..." % (self.service["name"], self.service["host"]))
        self.sock = BluetoothSocket(RFCOMM)
        self.sock.connect((self.service["host"], self.service["port"]))
        self.sock.settimeout(60)
        logging.info("Connected.")
        self.registerCrcKeyToBt()
        return True

    def disconnect(self):
        try:
            self.sock.close()
        except:
            pass
        logging.info("Disconnected.")

    def scandevices(self):
        logging.warning("Searching for devices...\n"
                        "It may take time, you'd better specify mac address to avoid a scan.")
        valid_names = ['MiaoMiaoJi', 'Paperang']
        nearby_devices = discover_devices(lookup_names=True)
        valid_devices = filter(lambda d: len(d) == 2 and d[1] in valid_names, nearby_devices)
        if len(valid_devices) == 0:
            logging.error("Cannot find device with name %s." % " or ".join(valid_names))
            return False
        elif len(valid_devices) > 1:
            logging.warning("Found multiple valid machines, the first one will be used.\n")
            logging.warning("\n".join(valid_devices))
        else:
            logging.warning(
                "Found a valid machine with MAC %s and name %s" % (valid_devices[0][0], valid_devices[0][1])
            )
        self.address = valid_devices[0][0]
        return True

    def scanservices(self):
        logging.info("Searching for services...")
        service_matches = find_service(uuid=self.uuid, address=self.address)
        valid_service = filter(
            lambda s: 'protocol' in s and 'name' in s and s['protocol'] == 'RFCOMM' and s['name'] == 'SerialPort',
            service_matches
        )
        if len(valid_service) == 0:
            logging.error("Cannot find valid services on device with MAC %s." % self.address)
            return False
        logging.info("Found a valid service on target device.")
        self.service = valid_service[0]
        return True

    def sendMsgAllPackage(self, msg):
        # Write data directly to device
        sent_len = self.sock.send(msg)
        logging.info("Sending msg with length = %d..." % sent_len)

    def crc32(self, content):
        return zlib.crc32(content, self.crcKey if self.crckeyset else self.standardKey)

    def packPerBytes(self, bytes, control_command, i):
        result = struct.pack('<BBB', 2, control_command, i)
        result += struct.pack('<H', len(bytes))
        result += bytes
        result += struct.pack('<i', self.crc32(bytes))
        result += struct.pack('<B', 3)
        return result


    def addBytesToList(self, bytes):
        length = self.max_send_msg_length
        result = [bytes[i:i+length] for i in range(0, len(bytes), length)]
        return result

    def sendToBt(self, allbytes, control_command, need_reply=True):
        bytes_list = self.addBytesToList(allbytes)
        for i, bytes in enumerate(bytes_list):
            tmp = self.packPerBytes(bytes, control_command, i)
            self.sendMsgAllPackage(tmp)
        if need_reply:
            return self.recv()

    def recv(self):
        # Here we assume that there is only one received packet.
        raw_msg = self.sock.recv(self.max_recv_msg_length)
        parsed = self.resultParser(raw_msg)
        logging.info("Recv: " + raw_msg.encode('hex'))
        logging.info("Received %d packets: " % len(parsed) + "".join([str(p) for p in parsed]))
        return raw_msg, parsed

    def resultParser(self, data):
        base = 0
        res = []
        while base < len(data) and data[base] == '\x02':
            class Info(object):
                def __str__(self):
                    return "\nControl command: %s(%s)\nPayload length: %d\nPayload(hex): %s" % (
                        self.command, BtCommandByte.findCommand(self.command)
                        , self.payload_length, self.payload.encode('hex')
                    )
            info = Info()
            _, info.command, _, info.payload_length = struct.unpack('<BBBH', data[base:base+5])
            info.payload = data[base + 5: base + 5 + info.payload_length]
            info.crc32 = data[base + 5 + info.payload_length: base + 9 + info.payload_length]
            base += 10 + info.payload_length
            res.append(info)
        return res

    def registerCrcKeyToBt(self, key=0x6968634 ^ 0x2e696d):
        logging.info("Setting CRC32 key...")
        msg = struct.pack('<I', int(key ^ self.standardKey))
        self.sendToBt(msg, BtCommandByte.PRT_SET_CRC_KEY)
        self.crcKey = key
        self.crckeyset = True
        logging.info("CRC32 key set.")

    def sendPaperTypeToBt(self, paperType=0):
        # My guess:
        # paperType=0: normal paper
        # paperType=1: official paper
        msg = struct.pack('<B', paperType)
        self.sendToBt(msg, BtCommandByte.PRT_SET_PAPER_TYPE)

    def sendPowerOffTimeToBt(self, poweroff_time=0):
        msg = struct.pack('<H', poweroff_time)
        self.sendToBt(msg, BtCommandByte.PRT_SET_POWER_DOWN_TIME)

    def sendImageToBt(self, binary_img):
        self.sendPaperTypeToBt()
        msg = struct.pack("<%dc" % len(binary_img), *binary_img)
        self.sendToBt(msg, BtCommandByte.PRT_PRINT_DATA, need_reply=False)
        self.sendFeedLineToBt(self.padding_line)

    def sendSelfTestToBt(self):
        msg = struct.pack('<B', 0)
        self.sendToBt(msg, BtCommandByte.PRT_PRINT_TEST_PAGE)

    def sendDensityToBt(self, density):
        msg = struct.pack('<B', density)
        self.sendToBt(msg, BtCommandByte.PRT_SET_HEAT_DENSITY)

    def sendFeedLineToBt(self, length):
        msg = struct.pack('<H', length)
        self.sendToBt(msg, BtCommandByte.PRT_FEED_LINE)

    def queryBatteryStatus(self):
        msg = struct.pack('<B', 1)
        self.sendToBt(msg, BtCommandByte.PRT_GET_BAT_STATUS)

    def queryDensity(self):
        msg = struct.pack('<B', 1)
        self.sendToBt(msg, BtCommandByte.PRT_GET_HEAT_DENSITY)

    def sendFeedToHeadLineToBt(self, length):
        msg = struct.pack('<H', length)
        self.sendToBt(msg, BtCommandByte.PRT_FEED_TO_HEAD_LINE)

    def queryPowerOffTime(self):
        msg = struct.pack('<B', 1)
        self.sendToBt(msg, BtCommandByte.PRT_GET_POWER_DOWN_TIME)

    def querySNFromBt(self):
        msg = struct.pack('<B', 1)
        self.sendToBt(msg, BtCommandByte.PRT_GET_SN)

    def queryHardwareInfo(self):
        msg = struct.pack('<B', 1)
        self.sendToBt(msg, BtCommandByte.PRT_GET_HW_INFO)

if __name__ == "__main__":
    logging.getLogger().setLevel(logging.INFO)

    # If you know the MAC address of your device, use this parameter to avoid a scan, which is time-consuming
    # mmj = BtManager("69:68:63:69:61:68")

    # Start a scan to find valid devices
    mmj = BtManager()

    if mmj.connected:
        mmj.sendDensityToBt(95)

        # If you want it never powered-off
        # mmj.sendPowerOffTimeToBt(0)
        # mmj.queryPowerOffTime()

        # Print an existing image(need opencv):
        # from image_process import ImageConverter
        # img = ImageConverter.image2bmp(r"C:\Users\Lemon\Desktop\0.jpg")
        # mmj.sendImageToBt(img)

        # Print a pure black image with 300 lines
        # img = "\xff" * 48 * 300
        # mmj.sendImageToBt(img)

        # Print 2 line of text(need opencv)
        from image_process import TextConverter
        img = TextConverter.text2bmp("Coded By") + TextConverter.text2bmp(__author__)
        mmj.sendImageToBt(img)
        mmj.disconnect()
    else:
        logging.error("Oops! Cannot establish connection with Paperang devices.")