import time
import numpy as np
import lib.config as config

# _GAMMA_TABLE = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1,
#                 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5,
#                 5, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 11,
#                 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17,
#                 18, 18, 19, 19, 20, 20, 21, 21, 22, 23, 23, 24, 24, 25,
#                 26, 26, 27, 28, 28, 29, 30, 30, 31, 32, 32, 33, 34, 35,
#                 35, 36, 37, 38, 38, 39, 40, 41, 42, 42, 43, 44, 45, 46,
#                 47, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 56, 57, 58,
#                 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 73,
#                 74, 75, 76, 77, 78, 79, 80, 81, 82, 84, 85, 86, 87, 88,
#                 89, 91, 92, 93, 94, 95, 97, 98, 99, 100, 102, 103, 104,
#                 105, 107, 108, 109, 111, 112, 113, 115, 116, 117, 119,
#                 120, 121, 123, 124, 126, 127, 128, 130, 131, 133, 134,
#                 136, 137, 139, 140, 142, 143, 145, 146, 148, 149, 151,
#                 152, 154, 155, 157, 158, 160, 162, 163, 165, 166, 168,
#                 170, 171, 173, 175, 176, 178, 180, 181, 183, 185, 186,
#                 188, 190, 192, 193, 195, 197, 199, 200, 202, 204, 206,
#                 207, 209, 211, 213, 215, 217, 218, 220, 222, 224, 226,
#                 228, 230, 232, 233, 235, 237, 239, 241, 243, 245, 247,
#                 249, 251, 253, 255]

_GAMMA_TABLE = [0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
                0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,
                1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,
                2,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4,  4,  5,  5,  5,
                5,  6,  6,  6,  6,  7,  7,  7,  7,  8,  8,  8,  9,  9,  9, 10,
               10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16,
               17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25,
               25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36,
               37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50,
               51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68,
               69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89,
               90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114,
              115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142,
              144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175,
              177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213,
              215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255]
              
_GAMMA_TABLE = np.array(_GAMMA_TABLE)

class LEDController:
    """Base class for interfacing with hardware LED strip controllers
    To add support for another hardware device, simply inherit this class
    and implement the show() method.
    Example usage:
        import numpy as np
        N_pixels = 60
        pixels = np.random.random(size=(3, N_pixels))
        device = LEDController()
        device.show(pixels)
    """

    def __init__(self):
        pass

    def show(self, pixels):
        """Set LED pixels to the values given in the array
        This function accepts an array of RGB pixel values (pixels)
        and displays them on the LEDs. To add support for another
        hardware device, you should create a class that inherits from
        this class, and then implement this method.
        Parameters
        ----------
        pixels: numpy.ndarray
            2D array containing RGB pixel values for each of the LEDs.
            The shape of the array is (3, n_pixels), where n_pixels is the
            number of LEDs that the device has.
            The array is formatted as shown below. There are three rows
            (axis 0) which represent the red, green, and blue color channels.
            Each column (axis 1) contains the red, green, and blue color values
            for a single pixel:
                np.array([ [r0, ..., rN], [g0, ..., gN], [b0, ..., bN]])
            Each value brightness value is an integer between 0 and 255.
        Returns
        -------
        None
        """
        raise NotImplementedError('Show() was not implemented')

    def test(self, n_pixels):
        pixels = np.zeros((3, n_pixels))
        pixels[0][0] = 255
        pixels[1][1] = 255
        pixels[2][2] = 255
        print('Starting LED strip test.')
        print('Press CTRL+C to stop the test at any time.')
        print('You should see a scrolling red, green, and blue pixel.')
        while True:
            self.show(pixels)
            pixels = np.roll(pixels, 1, axis=1)
            time.sleep(0.2)


class ESP8266(LEDController):
    def __init__(self, auto_detect=False,
                 mac_addr="aa-bb-cc-dd-ee-ff",
                 ip='192.168.0.150',
                 port=7778):
        """Initialize object for communicating with as ESP8266
        Parameters
        ----------
        auto_detect: bool, optional
            Automatically search for and find devices on windows hotspot
            with given mac addresses. Windows hotspot resets the IP
            addresses of any devices on reset, meaning the IP of the 
            ESP8266 changes every time you turn on the hotspot. This
            will find the IP address of the devices for you.
        mac_addr: str, optional
            The MAC address of the ESP8266 on the network. Only used if
            auto-detect is used
        ip: str, optional
            The IP address of the ESP8266 on the network. This must exactly
            match the IP address of your ESP8266 device, unless using
            the auto-detect feature.
        port: int, optional
            The port number to use when sending data to the ESP8266. This
            must exactly match the port number in the ESP8266's firmware.
        """
        import socket
        self._mac_addr = mac_addr
        self._ip = ip
        self._port = port
        self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        if auto_detect:
            self.detect()

    def detect(self):
        from subprocess import check_output
        from time import sleep
        """ Uses "arp -a" to find esp8266 on windows hotspot"""
        # Find the audio strip automagically
        ip_addr = False
        while not ip_addr:
            arp_out = check_output(['arp', '-a']).splitlines()
            for i in arp_out:
                if self._mac_addr in str(i):
                    ip_addr = i.split()[0].decode("utf-8")
                    break
            else:
                print("Device not found at physical address {}, retrying in 1s".format(self._mac_addr))
                sleep(1)
        print("Found device {}, with IP address {}".format(self._mac_addr, ip_addr))
        self._ip = ip_addr

    def show(self, pixels):
        """Sends UDP packets to ESP8266 to update LED strip values
        The ESP8266 will receive and decode the packets to determine what values
        to display on the LED strip. The communication protocol supports LED strips
        with a maximum of 256 LEDs.
        The packet encoding scheme is:
            |i|r|g|b|
        where
            i (0 to 255): Index of LED to change (zero-based)
            r (0 to 255): Red value of LED
            g (0 to 255): Green value of LED
            b (0 to 255): Blue value of LED
        """
        message = pixels.T.clip(0, config.settings["configuration"]["MAX_BRIGHTNESS"]).astype(np.uint32).ravel()
        message = _GAMMA_TABLE[message].astype(np.uint8).tostring()
        self._sock.sendto(message, (self._ip, self._port))

class PxMatrix(LEDController):
    def __init__(self, auto_detect=False,
                 mac_addr="aa-bb-cc-dd-ee-ff",
                 ip='192.168.0.150',
                 port=7778):
        """Initialize object for communicating with as ESP8266 based PxMatrix Controller
        Parameters
        ----------
        auto_detect: bool, optional
            Automatically search for and find devices on windows hotspot
            with given mac addresses. Windows hotspot resets the IP
            addresses of any devices on reset, meaning the IP of the 
            ESP8266 changes every time you turn on the hotspot. This
            will find the IP address of the devices for you.
        mac_addr: str, optional
            The MAC address of the ESP8266 on the network. Only used if
            auto-detect is used
        ip: str, optional
            The IP address of the ESP8266 on the network. This must exactly
            match the IP address of your ESP8266 device, unless using
            the auto-detect feature.
        port: int, optional
            The port number to use when sending data to the ESP8266. This
            must exactly match the port number in the ESP8266's firmware.
        """
        import socket
        self._mac_addr = mac_addr
        self._ip = ip
        self._port = port
        self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        if auto_detect:
            self.detect()

    def detect(self):
        from subprocess import check_output
        from time import sleep
        """ Uses "arp -a" to find esp8266 on windows hotspot"""
        # Find the audio strip automagically
        ip_addr = False
        while not ip_addr:
            arp_out = check_output(['arp', '-a']).splitlines()
            for i in arp_out:
                if self._mac_addr in str(i):
                    ip_addr = i.split()[0].decode("utf-8")
                    break
            else:
                print("Device not found at physical address {}, retrying in 1s".format(self._mac_addr))
                sleep(1)
        print("Found device {}, with IP address {}".format(self._mac_addr, ip_addr))
        self._ip = ip_addr

    def flatten_and_clip(self, pixels):
        print(pixels)
        pixels = np.reshape(pixels, len(pixels)*4, order='F')
        pixels = pixels[len(pixels)//4:]
        pixels = numpy.reshape(pixels, (3, len(pixels)//3)).T
        return pixels

    def RGB565_array(self, array):
        output = np.empty(len(array), dtype=np.uint16)
        for i in range(len(array)):
            output[i] = (((array[i][0] & 0b11111000) << 8) | ((array[i][1] & 0b11111100) << 3) | (array[i][2] >> 3))
        return output

    def show(self, pixels):
        """Sends UDP packets to ESP8266 to update PxMatrix
        The ESP8266 will receive an image to display on the matrix.
        """
        #message = self.flatten_and_clip(pixels)
        message = self.RGB565_array(pixels.T.astype(np.uint16))
        # print(len(message))
        # print(message[0])
        message = message[:700].ravel().tostring()
        self._sock.sendto(message, (self._ip, self._port))


class FadeCandy(LEDController):
    def __init__(self, server='localhost:7890'):
        """Initializes object for communicating with a FadeCandy device
        Parameters
        ----------
        server: str, optional
            FadeCandy server used to communicate with the FadeCandy device.
        """
        try:
            import audioled.opc
        except ImportError as e:
            print('Unable to import audioled.opc library')
            print('You can install this library with `pip install opc`')
            raise e
        self.client = audioled.opc.Client(server)
        if self.client.can_connect():
            print('Successfully connected to FadeCandy server.')
        else:
            print('Could not connect to FadeCandy server.')
            print('Ensure that fcserver is running and try again.')

    def show(self, pixels):
        self.client.put_pixels(pixels.T.clip(0, 255).astype(int).tolist())


class BlinkStick(LEDController):
    def __init__(self):
        """Initializes a BlinkStick controller"""
        try:
            from blinkstick import blinkstick
        except ImportError as e:
            print('Unable to import the blinkstick library')
            print('You can install this library with `pip install blinkstick`')
            raise e
        self.stick = blinkstick.find_first()

    def show(self, pixels):
        """Writes new LED values to the Blinkstick.
        This function updates the LED strip with new values.
        """
        # Truncate values and cast to integer
        n_pixels = pixels.shape[1]
        pixels = pixels.clip(0, 255).astype(int)
        pixels = _GAMMA_TABLE[pixels]
        # Read the rgb values
        r = pixels[0][:].astype(int)
        g = pixels[1][:].astype(int)
        b = pixels[2][:].astype(int)

        # Create array in which we will store the led states
        newstrip = [None] * (n_pixels * 3)

        for i in range(n_pixels):
            # Blinkstick uses GRB format
            newstrip[i * 3] = g[i]
            newstrip[i * 3 + 1] = r[i]
            newstrip[i * 3 + 2] = b[i]
        # Send the data to the blinkstick
        self.stick.set_led_data(0, newstrip)


class RaspberryPi(LEDController):
    def __init__(self, n_pixels, pin=18, invert_logic=False,
                 freq=800000, dma=5):
        """Creates a Raspberry Pi output device
        Parameters
        ----------
        n_pixels: int
            Number of LED strip pixels
        pin: int, optional
            GPIO pin used to drive the LED strip (must be a PWM pin).
            Pin 18 can be used on the Raspberry Pi 2.
        invert_logic: bool, optional
            Whether or not to invert the driving logic.
            Set this to True if you are using an inverting logic level
            converter, otherwise set to False.
        freq: int, optional
            LED strip protocol frequency (Hz). For ws2812 this is 800000.
        dma: int, optional
            DMA (direct memory access) channel used to drive PWM signals.
            If you aren't sure, try 5.
        """
        try:
            import neopixel
        except ImportError as e:
            url = 'learn.adafruit.com/neopixels-on-raspberry-pi/software'
            print('Could not import the neopixel library')
            print('For installation instructions, see {}'.format(url))
            raise e
        self.strip = neopixel.Adafruit_NeoPixel(n_pixels, pin, freq, dma,
                                                invert_logic, 255)
        self.strip.begin()

    def show(self, pixels):
        """Writes new LED values to the Raspberry Pi's LED strip
        Raspberry Pi uses the rpi_ws281x to control the LED strip directly.
        This function updates the LED strip with new values.
        """
        # Truncate values and cast to integer
        n_pixels = pixels.shape[1]
        pixels = pixels.clip(0, 255).astype(int)
        # Optional gamma correction
        pixels = _GAMMA_TABLE[pixels]
        # Encode 24-bit LED values in 32 bit integers
        r = np.left_shift(pixels[0][:].astype(int), 8)
        g = np.left_shift(pixels[1][:].astype(int), 16)
        b = pixels[2][:].astype(int)
        rgb = np.bitwise_or(np.bitwise_or(r, g), b)
        # Update the pixels
        for i in range(n_pixels):
            self.strip.setPixelColor(i, neopixel.Color(rgb[i]))
        self.strip.show()


class DotStar(LEDController):
    def __init__(self, pixels, brightness=31):
        """Creates an APA102-based output device
        Parameters
        ----------
        pixels: int
            Number of LED strip pixels
        brightness: int, optional
            Global brightness
        """
        try:
            import apa102
        except ImportError as e:
            url = 'https://github.com/tinue/APA102_Pi'
            print('Could not import the apa102 library')
            print('For installation instructions, see {}'.format(url))
            raise e
        self.strip = apa102.APA102(numLEDs=pixels, globalBrightness=brightness) # Initialize the strip
        led_data = np.array(self.strip.leds, dtype=np.uint8)
        # memoryview preserving the first 8 bits of LED frames (w/ global brightness)
        self.strip.leds = led_data.data
        # 2D view of led_data
        self.led_data = led_data.reshape((pixels, 4)) # or (-1, 4)

    def show(self, pixels):
        bgr = [2,1,0]
        self.led_data[0:,1:4] = pixels[bgr].T.clip(0,255)
        self.strip.show()


class sACNClient(LEDController):
    def __init__(self,
                 start_universe=1,
                 start_channel=1,
                 channel_count=3,
                 universe_size=512,
                 fps=30,
                 ip='192.168.1.180'):
        """Initialize object for communicating with as E1.31 capable device.
        Parameters
        ----------
        start_universe: int, optional
            The start universe for the strip
        start_channel: int, optional
            The state channel offset within the universe
        channel_count: int, optional
            The total number of channels for the device. Typically this will
            be 3 * N_PIXELS as you need a sperate channel for the red, green,
            and blue components
        universe_size: int, optional
            The number of channels in each universe. This should pretty much
            never be changed unless using a controller that only support 510
            channels.
        ip: str, optional
            The IP address of a E1.31 capable device on the network.
        fps: int, optional
            The frequency in which to flush the DMX buffer out. This does
            not need to align with the actually processing rate. Running
            the processing at 60fps and flusing at 30fps provides a pretty
            good balance.
        """
        import sacn
        self._ip = ip
        self._start_universe = start_universe
        self._start_channel = start_channel
        self._channel_count = channel_count
        self._universe_size = universe_size
        self._sender = sacn.sACNsender()
        self._sender._fps = fps
        self._sender.start()

        self._stop_universe = start_universe + round(0.5 + ((start_channel + channel_count) / universe_size))
        for universe in range(self._start_universe, self._stop_universe):
            self._sender.activate_output(universe)
            self._sender[universe].destination = ip
            self._sender[universe].multicast = False

    def __del__(self):
        self._sender.stop()

    def show(self, pixels):
        """
        Calculate the mapping for pixels to universe and channel and populate
        the dmx buffer for each universe. The dmx buffer will be flushed by a
        background thread created by the sACNSender class.
        """
        message = pixels.T.clip(0, config.settings["configuration"]["MAX_BRIGHTNESS"]).astype(np.uint8).ravel().tostring()
        for universe in range(self._start_universe, self._stop_universe):
            pixel_start = (universe - self._start_universe) * self._universe_size
            pixel_end = min(pixel_start + self._universe_size, len(message))
            self._sender[universe].dmx_data = message[pixel_start:pixel_end]

class Stripless(LEDController):
    def __init__(self):
        pass
    def show(self, pixels):
        pass