from datetime import datetime
import protocol
from threading import Event
from collections import namedtuple
from lifx.color import modify_color
import color
import time

DEFAULT_DURATION = 200
DEFAULT_TIMEOUT = 2.0
DEFAULT_RETRANSMITS = 10

StatsTuple = namedtuple('StatsTuple', ['dropped_packets', 'sent_packets'])

class DeviceTimeoutError(Exception):
    '''Raise when we time out waiting for a response'''
    def __init__(self, device, timeout, retransmits):
        message = "Device with id:'%s' timed out after %s seconds and %s retransmissions." % (protocol.mac_string(device.id), timeout, retransmits)

        super(DeviceTimeoutError, self).__init__(message)
        self.device = device
        self.timeout = timeout
        self.retransmits = retransmits

class Device(object):
    def __init__(self, device_id, host, client):
        # Our Device
        self._device_id = device_id
        self._host = host

        # Services
        self._services = {}

        # Last seen time
        self._lastseen = datetime.now()

        # For sending packets
        self._client = client

        # Tools for tracking responses
        self._tracked = {}
        self._responses = {}

        # Stats tracking
        self._dropped_packets = 0
        self._sent_packets = 0

    @property
    def _seq(self):
        return self._client._seq

    def _packethandler(self, host, port, packet):
        self._seen()

        # If it was a service packet
        if packet.protocol_header.pkt_type == protocol.TYPE_STATESERVICE:
            self._services[packet.payload.service] = packet.payload.port

        # Store packet and fire events
        self._responses[packet.frame_address.sequence] = packet
        event = self._tracked.get(packet.frame_address.sequence, None)
        if event is not None:
            event.set()

    def _send_packet(self, *args, **kwargs):
        """
        At this point we have most of the required arguments for the packet. The
        only arguments left that we need are:

        * ack_required
        * res_required
        * pkt_type
        * Arguments for the payload
        """

        kwargs['address'] = self.host
        kwargs['port'] = self.get_port()
        kwargs['target'] = self._device_id

        self._sent_packets += 1

        return self._client.send_packet(
                *args,
                **kwargs
        )

    def _block_for_response(self, *args, **kwargs):
        return self._block_for(False, True, *args, **kwargs)

    def _block_for_ack(self, *args, **kwargs):
        return self._block_for(True, False, *args, **kwargs)

    def _block_for(self, need_ack, need_res, *args, **kwargs):
        """
        Send a packet and block waiting for the replies.

        Only needs the type and an optional payload.
        """
        if need_ack and need_res:
            raise NotImplemented('Waiting for both acknowledgement and response not yet supported.')

        sequence = self._seq
        timeout = kwargs.get('timeout', DEFAULT_TIMEOUT)
        sub_timeout = timeout / DEFAULT_RETRANSMITS

        for i in range(1, DEFAULT_RETRANSMITS):
            if i != 1:
                self._dropped_packets += 1

            e = Event()
            self._tracked[sequence] = e

            self._send_packet(
                    ack_required=need_ack,
                    res_required=need_res,
                    sequence=sequence,
                    *args,
                    **kwargs
            )

            # If we don't care about a response, don't block at all
            if not (need_ack or need_res):
                return None

            if e.wait(sub_timeout):
                response = self._responses[sequence]

                # TODO: Check if the response was actually what we expected

                if need_res:
                    return response.payload
                else:
                    return True

        # We did get a response
        raise DeviceTimeoutError(self, timeout, DEFAULT_RETRANSMITS)


    def _get_group_data(self):
        """
        Called by the group object so it can see the updated_at from the group
        """
        return self._block_for_response(pkt_type=protocol.TYPE_GETGROUP)

    def _get_location_data(self):
        """
        Called by the group object so it can see the updated_at from the location
        """
        return self._block_for_response(pkt_type=protocol.TYPE_GETLOCATION)

    def send_poll_packet(self):
        """
        Send a poll packet to the device, without waiting for a response. The
        response will be received later and will update the time we last saw
        the bulb.
        """
        return self._send_packet(
                ack_required=False,
                res_required=True,
                pkt_type=protocol.TYPE_GETSERVICE,
        )

    def _seen(self):
        self._lastseen = datetime.now()

    def __repr__(self):
        return u'<Device MAC:%s, Label:%s>' % (protocol.mac_string(self._device_id), repr(self.label))

    def get_port(self, service_id=protocol.SERVICE_UDP):
        """
        Get the port for a service, by default the UDP service.

        :param service_id: The service whose port we are fetching.
        """
        return self._services[service_id]

    @property
    def stats(self):
        return StatsTuple(
                dropped_packets=self._dropped_packets,
                sent_packets=self._sent_packets,
        )

    @property
    def group_id(self):
        """
        The id of the group that the Device is in. Read Only.
        """
        response = self._get_group_data()
        return response.group

    @property
    def location_id(self):
        """
        The id of the group that the Device is in. Read Only.
        """
        response = self._get_location_data()
        return response.location

    @property
    def udp_port(self):
        """
        The port of the UDP service. Read Only.
        """
        return self.get_port(protocol.SERVICE_UDP)

    @property
    def seen_ago(self):
        """
        The time in seconds since we last saw a packet from the device. Read Only.
        """
        return datetime.now() - self._lastseen

    @property
    def host(self):
        """
        The ip address of the device. Read Only.
        """
        return self._host

    @property
    def host_firmware(self):
        """
        The version string representing the firmware version.
        """
        response = self._block_for_response(pkt_type=protocol.TYPE_GETHOSTFIRMWARE)
        return protocol.version_string(response.version)

    @property
    def wifi_firmware(self):
        """
        The version string representing the firmware version.
        """
        response = self._block_for_response(pkt_type=protocol.TYPE_GETWIFIFIRMWARE)
        return protocol.version_string(response.version)

    @property
    def id(self):
        """
        The device id. Read Only.
        """
        return self._device_id

    @property
    def latency(self):
        """
        The latency to the device. Read Only.
        """
        ping_payload = bytearray('\x00' * 64)
        start = time.time()
        response = self._block_for_response(ping_payload, pkt_type=protocol.TYPE_ECHOREQUEST)
        end = time.time()
        return end - start

    @property
    def label(self):
        """
        The label for the device, setting this will change the label on the device.
        """
        response = self._block_for_response(pkt_type=protocol.TYPE_GETLABEL)
        return protocol.bytes_to_label(response.label)

    @label.setter
    def label(self, label):
        newlabel = bytearray(label.encode('utf-8')[0:protocol.LABEL_MAXLEN])

        return self._block_for_ack(newlabel, pkt_type=protocol.TYPE_SETLABEL)

    def fade_power(self, power, duration=DEFAULT_DURATION):
        """
        Transition to another power state slowly.

        :param power: The new power state
        :param duration: The number of milliseconds to perform the transition over.
        """
        if power:
            msgpower = protocol.UINT16_MAX
        else:
            msgpower = 0

        return self._block_for_ack(msgpower, duration, pkt_type=protocol.TYPE_LIGHT_SETPOWER)

    def power_toggle(self, duration=DEFAULT_DURATION):
        """
        Transition to the opposite power state slowly.

        :param duration: The number of milliseconds to perform the transition over.
        """
        self.fade_power(not self.power, duration)

    @property
    def power(self):
        """
        The power state of the device. Set to False to turn of and True to turn on.
        """
        response = self._block_for_response(pkt_type=protocol.TYPE_GETPOWER)
        if response.level > 0:
            return True
        else:
            return False

    @power.setter
    def power(self, power):
        self.fade_power(power)

    def fade_color(self, newcolor, duration=DEFAULT_DURATION):
        """
        Transition the light to a new color.

        :param newcolor: The HSBK tuple of the new color to transition to
        :param duration: The number of milliseconds to perform the transition over.
        """
        colormsg = color.message_from_color(newcolor)
        return self._block_for_ack(
                0,
                colormsg.hue,
                colormsg.saturation,
                colormsg.brightness,
                colormsg.kelvin,
                duration,
                pkt_type=protocol.TYPE_LIGHT_SETCOLOR
        )

    @property
    def color(self):
        """
        The color the device is currently set to. Set this value to change the
        color of the bulb all at once.
        """
        response = self._block_for_response(pkt_type=protocol.TYPE_LIGHT_GET)
        return color.color_from_message(response)

    @color.setter
    def color(self, newcolor):
        self.fade_color(newcolor)

    # Helpers to change the color on the bulb
    @property
    def hue(self):
        """
        The hue value of the color the bulb is set to. Set this to alter only
        the Hue.
        """
        return self.color.hue

    @hue.setter
    def hue(self, hue):
        self.color = modify_color(self.color, hue=hue)

    @property
    def saturation(self):
        """
        The saturation value the bulb is currently set to. Set this to alter
        only the saturation.
        """
        return self.color.saturation

    @saturation.setter
    def saturation(self, saturation):
        self.color = modify_color(self.color, saturation=saturation)

    @property
    def brightness(self):
        """
        The brightness value the bulb is currently set to. Set this to alter
        only the current brightness of the bulb.
        """
        return self.color.brightness

    @brightness.setter
    def brightness(self, brightness):
        self.color = modify_color(self.color, brightness=brightness)

    @property
    def kelvin(self):
        """
        The kelvin value the bulb is currently set to. Set this to alter
        only the current kelvin of the bulb.
        """
        return self.color.kelvin

    @kelvin.setter
    def kelvin(self, kelvin):
        self.color = modify_color(self.color, kelvin=kelvin)