import socket
import struct
import time
import crc16
from cobs import cobs
import logging

from pockethernet.testresult import WiremapResult, PoEResult, LinkResult, TdrResult, BerResult

COMMAND_WIREMAP = 48
COMMAND_DEVICEINFO = 1
COMMAND_TDR = 50
COMMAND_PHYCONFIG = 51
COMMAND_TDR_GRAPH = 65
COMMAND_POE = 49
COMMAND_LINK = 52
COMMAND_DHCP = 56
COMMAND_CAPTURE = 53
COMMAND_VLAN = 57
COMMAND_BER = 64
RESULT_WIREMAP = 4144
RESULT_DEVICEINFO = 4097
RESULT_TDR = 4146
RESULT_TDR_GRAPH = 4161
RESULT_POE = 4145
RESULT_LINK = 4099
RESULT_LLDP = 4352
RESULT_ACK = 47806
RESULT_BER = 4160
PACKET_DHCP = 4353
PACKET_CDPLLDP = 4352

PHY_UP = 1
PHY_ISOLATE = 2
PHY_NEGOTIATE_MDI = 4
PHY_NEGOTIATE_MDIX = 8
PHY_NEGOTIATE_ANY = 4 + 8
PHY_DTE_DETECT = 16
PHY_AN_ADV = 32
PHY_ADVERTIZE_10BASET = 64
PHY_ADVERTIZE_100BASET = 128
PHY_ADVERTIZE_1000BASET = 256
PHY_ADVERTIZE_ANY = 64 + 128 + 256
PHY_DUPLEX_HALF = 512
PHY_DUPLEX_FULL = 1024
PHY_DUPLEX_ANY = 512 + 1024
PHY_SELF = 2048
PHY_DOWNSHIFT = 4096


class Pockethernet:

    def __init__(self):
        self.sock = None
        self.lastData = {}
        self.mac = None

    def connect(self, mac, max_tries=None):
        """ Connect to a pockethernet device over bluetooth.

        The pockethernet device has to be bound already using something like the bluetooth dialog in gnome

        :param mac: Bluetooth mac address of the pockethernet
        :param max_tries: Max tries to connect, each try is 1-2 seconds
        :return: True if the connection is successful
        """
        self.mac = mac
        tries = 0
        logging.debug('Connecting to {}'.format(mac))

        # Pockethernet seems to use RFCOMM channel 5
        while True:
            try:
                self.sock = socket.socket(socket.AF_BLUETOOTH, socket.SOCK_STREAM, socket.BTPROTO_RFCOMM)
                self.sock.bind((socket.BDADDR_ANY, 0))
                self.sock.connect((mac, 5))
                logging.debug('Connection to {} successful'.format(mac))
                return True
            except OSError as e:
                logging.debug(e)
                tries += 1
                if max_tries is not None and tries > max_tries:
                    logging.debug('Max tries exceeded')
                    return False
                time.sleep(1)

    def send_command(self, command, data):
        logging.debug("Sending command {} with {} bytes data".format(command, len(data)))

        # First is command ID, the 0 is reserved space for a checksum
        header = struct.pack('<HH', command, 0)
        data = struct.pack('<{}s'.format(len(data)), data)
        packet = header + data

        # Calculate the checksum and pack the header again with it
        crc = crc16.crc16xmodem(packet, -1)
        header = struct.pack('<HH', command, crc)
        packet = header + data

        # Run COBS encoding on the data and add null bytes for framing
        encoded = cobs.encode(packet)
        raw = b'\0' + encoded + b'\0'

        # Hope that the pockethernet enjoys this packet
        self.sock.send(raw)

    def read_packet(self, until=None):
        if until is None:
            logging.debug('Reading any packet')
        else:
            logging.debug('Reading a packet (waiting for {})'.format(until))
        while True:
            raw = self.sock.recv(200)
            packets = raw.split(b'\0')
            packets = list(filter(None, packets))
            for packet in packets:
                packet = cobs.decode(packet)
                header = struct.unpack_from('<HH', packet)
                data = struct.unpack_from('<{}s'.format(len(packet) - 4), packet, 4)
                packet_type = header[0]
                self.lastData[packet_type] = data[0]
                logging.debug('Got packet with type {} and length {}'.format(packet_type, len(data[0])))
                if (isinstance(until, int) and packet_type == until) or (
                        isinstance(until, list) and packet_type in until):
                    if isinstance(until, list):
                        return packet_type, data[0]
                    else:
                        return data[0]
                else:
                    logging.warn('Got unexpected packet {} size {}'.format(packet_type, len(data[0])))

    def get_device_info(self):
        self.send_command(COMMAND_DEVICEINFO, b'')
        result = self.read_packet(RESULT_DEVICEINFO)
        result = struct.unpack_from('<BBBBBB', result)
        mac = '28:FD:80:{:02X}:{:02X}:{:02X}'.format(result[2], result[1], result[0])
        return {
            'hardware': result[4],
            'firmware': result[5],
            'mac': mac
        }

    def get_wiremap(self):
        self.send_command(COMMAND_WIREMAP, b'')
        result = self.read_packet(RESULT_WIREMAP)
        return WiremapResult(result)

    def get_poe(self):
        config = struct.pack('<HH', 3000, 100)
        self.send_command(COMMAND_POE, config)
        result = self.read_packet(RESULT_POE)
        return PoEResult(result)

    def _make_marvell_link_bitfield(self, speed=PHY_ADVERTIZE_ANY, duplexity=PHY_DUPLEX_ANY,
                                    negotiate=PHY_NEGOTIATE_ANY):
        return PHY_UP + PHY_ISOLATE + PHY_AN_ADV + speed + duplexity + negotiate

    def get_link(self):
        phy = self._make_marvell_link_bitfield()
        config = struct.pack('<IH', phy, 8000)
        self.send_command(COMMAND_LINK, config)
        result = self.read_packet(RESULT_LINK)

        return LinkResult(result)

    def link_reset(self):
        logging.info('Reset link')
        phy = self._make_marvell_link_bitfield(0, 0, 0)
        config = struct.pack('<ih', phy, 8000)
        self.send_command(COMMAND_PHYCONFIG, config)
        self.read_packet(RESULT_ACK)

    def enable_dhcp(self):
        logging.info('Enable DHCP')
        self.send_command(COMMAND_DHCP, b'')
        self.read_packet(RESULT_ACK)

    def set_capture_mode(self, dhcp=False, lldp=False, traffic=False):
        flag = 0
        if dhcp:
            flag += 1
        if lldp:
            flag += 2
        if traffic:
            flag += 4
        config = struct.pack('<ii', flag, 0)
        logging.info('Set capture flag to {}'.format(flag))
        self.send_command(COMMAND_CAPTURE, config)
        self.read_packet(RESULT_ACK)

    def set_vlan(self, vid):
        config = struct.pack('<i', vid)
        logging.info('Set vlan to {}'.format(vid))
        self.send_command(COMMAND_VLAN, config)
        self.read_packet(RESULT_ACK)

    def debug(self, send=None):
        if send:
            self.send_command(send, b'')
        self.read_packet()

    def get_tdr(self, long_distance=False):
        short_pulse = not long_distance
        magic = 0
        pulse_length = 3
        if short_pulse:
            pulse_length = 0
            magic = 23

        # More magic
        config = [1, pulse_length, 1, 0, 5, 4, magic, 3, 0, 0, 0, 0, 0, 0, 0]
        config = struct.pack('<BBBBBBBBBBBBBBB', *config)

        self.send_command(COMMAND_TDR, config)
        result = self.read_packet(until=RESULT_TDR)
        return TdrResult(result, short_pulse)

    def get_ber(self, speed=10, large_packet=False, random=True):
        if speed == 10:
            speed = 1
            timeout = 5
        elif speed == 100:
            speed = 2
            timeout = 50
        elif speed == 1000:
            speed = 3
            timeout = 200
        else:
            raise ValueError("speed should be 10, 100 or 1000")

        packet_size = 1
        if large_packet:
            packet_size = 2

        payload = 2
        if random:
            payload = 1

        config = [8000, 2000, speed, packet_size, payload, 200, timeout]
        config = struct.pack('<HHBBBBB', *config)
        self.send_command(COMMAND_BER, config)
        result = self.read_packet(until=RESULT_BER)
        return BerResult(result)

    def wait_for_capture_results(self, timeout=60):
        ptype, result = self.read_packet(until=[PACKET_DHCP, PACKET_CDPLLDP])
        #if ptype == PACKET_DHCP:
        #    packet = Ether(result)
        #    packet.pdfdump("/workspace/dhcp.pdf")
        #    print("YAY")
        #else:
        logging.error("Unexpected packet received: {}".format(ptype))
