import struct
import socket
import json

from io import BytesIO

from pymoku.network import log, NetworkError
from pymoku.network.zmq import ZMQReq


class Control(object):
    def __init__(self, skt):
        self.skt = skt

    def _transfer(self, data, timeout=3000):
        rep = self.skt.transfer(data, timeout=timeout)

        ptype, status = struct.unpack("<BB", rep[:2])

        # Response should begin with the same packet type, and either 0 or
        # <5 status code
        if data[0] != rep[0] or (status <= 5 and status > 0):
            raise NetworkError("Status: 0x{:02X}, {:s}".format(
                status, rep[2:].decode('utf-8')))
        return rep

    def json_packet(self, pid, data={}):
        pkt = struct.pack("<B", pid)
        pkt += json.dumps(data).encode('utf8')
        reply = self._transfer(pkt)
        ptype, status = struct.unpack("<BB", reply[:2])
        return json.loads(reply[2:].decode('utf8'))

    def register_access(self, xfers):
        # xfers [(name, r/w, addr, length/data), ...]
        pkt = struct.pack("<BH", 0x55, len(xfers))

        for name, write, addr, data in xfers:
            pkt += struct.pack("<B", len(name)) + bytes(name, 'utf-8')
            pkt += struct.pack("<BI", int(write), addr)
            if write:
                pkt += struct.pack("<I", len(data)) + data
            else:
                pkt += struct.pack("<I", data)

        rep = BytesIO(self._transfer(pkt))

        ptype, status, nr = struct.unpack("<BBH", rep.read(4))
        assert ptype == 0x55 and status == 0x00 and nr == len(xfers)

        results = []
        for _ in range(nr):
            nlen = struct.unpack("<B", rep.read(1))[0]
            name = rep.read(nlen).decode('ascii')
            addr, status, dlen = struct.unpack("<IBI", rep.read(9))
            if status:
                log.error("Failed Transaction {:s}:{:08X}".format(name, addr))
            results.append((status, addr, dlen,
                            rep.read(dlen) if dlen else None))

        return results

    def readregs(self, region, commands):
        # commands [uint8, ...]
        xfers = []
        for addr in commands:
            xfers.append((region, False, addr * 4, 4))
        return [(addr // 4, struct.unpack("<I", val)[0] if val else None)
                for _, addr, _, val in self.register_access(xfers)]

    def readreg(self, region, reg):
        return self.readregs(region, [reg])[0][1]

    def writeregs(self, region, commands):
        # commands [(uint8, int32), ...]
        xfers = []
        for addr, value in commands:
            xfers.append((region, True, addr * 4, struct.pack('<I', value)))
        self.register_access(xfers)

    def writereg(self, region, reg, val):
        self.writeregs(region, [(reg, val)])

    def get_properties(self, properties):
        if len(properties) > 255:
            raise NetworkError(f"Properties request too long ({len(properties):d})")

        data = [{'read': x} for x in properties]
        return self.json_packet(0x56, data)

    def get_property(self, prop):
        r = self.get_properties([prop])
        return r[0][prop]

    def set_properties(self, properties):
        if len(properties) > 255:
            raise NetworkError(f"Properties request too long ({len(properties):d})")
        data = [{'write': x[0], 'data': x[1]} for x in properties]
        return self.json_packet(0x56, data)

    def set_property(self, prop, val):
        r = self.set_properties([(prop, val)])
        return r[0][prop]

    def ownership(self, claim=0.0, release=None, force=False):
        d = dict()
        if claim:
            d['name'] = socket.gethostname()[:255]
            d['duration'] = float(claim)
            d['force'] = bool(force)
        elif release:
            d['name'] = ''
        return self.json_packet(0x40, d)

    def write_calibration(self, cal_data=None, write_eeprom=False):
        d = dict()
        if cal_data is not None:
            d.update(data=cal_data)
            d.update(write_eeprom=write_eeprom)
        return self.json_packet(0x58, d)

    def get_calibration(self):
        return self.write_calibration()

    def reload_bitstreams(self):
        return self.json_packet(0x59, dict(reload=True))

    def get_statistics(self):
        return self.json_packet(0x61)

    def close(self):
        self.skt.close()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        self.close()


class Control529(Control):
    def get_properties(self, properties):
        ret = []

        if len(properties) > 255:
            raise NetworkError("Properties request too long ({:d})"
                               .format(len(properties)))
        pkt = struct.pack('<BB', 0x56, len(properties))

        for p in properties:
            pkt += struct.pack('<BB', 1, len(p))  # Read action
            pkt += p.encode('ascii')
            pkt += b'\x00'  # No data for reads

        with BytesIO(self._transfer(pkt)) as reply:
            ptype, status, nr = struct.unpack("<BBB", reply.read(3))

            p, d = '', ''
            for n in range(nr):
                plen = ord(reply.read(1))
                p = reply.read(plen).decode('ascii')
                dlen = ord(reply.read(1))
                d = reply.read(dlen).decode('ascii')

                if status == 0:
                    ret.append((p, d))
                else:
                    break

        if status != 0x00:
            # An error will have exactly one property reply, the property that
            # caused the error with empty data
            raise NetworkError("Property Read Error, status {:d} "
                               "on property {:s}".format(status, p))

        return ret

    def get_property(self, prop):
        r = self.get_properties([prop])
        return r[0][1]

    def set_properties(self, properties):
        raise NetworkError("Unsupported firmware version")

    def deploy(self, instr_id, slot):
        raise NetworkError("Unsupported firmware version")

    def write_calibration(self, **kwargs):
        raise NetworkError("Unsupported firmware version")


class Control511(Control529):
    def _transfer(self, data, timeout=3000):
        # skip error checks
        return ZMQReq.transfer(self.skt, data, timeout=timeout)

    def get_property(self, prop):
        pkt = struct.pack('<BQB', 0x56, 0x00, 0x01)
        pkt += struct.pack('<BB', 1, len(prop))  # Read action
        pkt += prop.encode('ascii')
        pkt += b'\x00'  # No data for reads

        with BytesIO(self._transfer(pkt)) as reply:
            ptype, _, _, status, nr = struct.unpack("<BQQBB", reply.read(19))

            if status != 0x00 or nr != 1:
                raise NetworkError(
                    "Property Read Error, status {:d}".format(status))

            plen = ord(reply.read(1))
            reply.read(plen).decode('ascii')
            dlen = ord(reply.read(1))
            d = reply.read(dlen).decode('ascii')

        import ast  # noqa
        try:
            return ast.literal_eval(d)
        except ValueError:
            return d

    def register_access(self, xfers):
        raise NetworkError("Unsupported firmware version")

    def get_properties(self, properties):
        res = []
        for prop in properties:
            res.append({prop: self.get_property(prop)})
        return res

    def ownership(self, *args, **kwargs):
        raise NetworkError("Unsupported firmware version")


class Control433(Control511):
    def connect(self, ip_addr):
        return ZMQReq.connect(self.skt, ip_addr)

    def _transfer(self, data, timeout=3000):
        # skip error checks
        return ZMQReq.transfer(self.skt, data, timeout=timeout)

    def get_property(self, prop):
        pkt = struct.pack('<BBB', 0x46, 0x00, 0x01)  # ptype, seq, num
        pkt += struct.pack('<BB', 1, len(prop))  # Read action
        pkt += prop.encode('ascii')
        pkt += b'\x00'  # No data for reads

        with BytesIO(self._transfer(pkt)) as reply:
            ptype, seq, status, nr = struct.unpack("<BBBB", reply.read(4))

            if status != 0x00 or nr != 1:
                raise NetworkError(
                    "Property Read Error, status {:d}".format(status))

            plen = ord(reply.read(1))
            reply.read(plen).decode('ascii')
            dlen = ord(reply.read(1))
            d = reply.read(dlen).decode('ascii')

        return d

    def get_firmware_version(self):
        return int(self.get_property('system.micro'))
