import time
from neopixel import Neopixel
import random

red = (255, 0, 0)
orange = (255, 50, 0)
yellow = (255, 100, 0)
green = (0, 255, 0)
blue = (0, 0, 255)
indigo = (100, 0, 90)
violet = (200, 0, 100)

class PIXEL_EFFECTS:
    def __init__(self, num_pixels, ring_pixel_sizes, pin=27, brightness=20):
     
        print('start pixel effect init')
 
        # Anzahl der LEDs im Strip
        self.ring_pixel_sizes = ring_pixel_sizes
        self.num_pixels = num_pixels  # Summe aller Pixelgrößen im Array
        self.ring_counts = len(self.ring_pixel_sizes)

        # Streifen mit 30 Leds an Pin 28
        self.pixels = Neopixel(num_leds=self.num_pixels, state_machine=0, pin=pin, mode="GRB")

        # Initialisierung
        self.pixels.brightness(brightness)
        self.pixels.clear()
        self.pixels.show()


    def in_and_out(self, direction, speed, color, fill):
        self.pixels.clear()
        
        if direction == "in":
            start_pixel = 0
        else:  # direction == "out"
            stop_pixel = self.num_pixels - self.ring_pixel_sizes[-1]
        
        for ring in range(self.ring_counts):
            if direction == "in":
                ring_size = self.ring_pixel_sizes[ring]
                stop_pixel = start_pixel + ring_size - 1
            else:  # direction == "out"
                ring_size = self.ring_pixel_sizes[self.ring_counts - ring - 1]
                start_pixel = stop_pixel - ring_size + 1
            
            for pixel in range(start_pixel, stop_pixel + 1):
                self.pixels.set_pixel(pixel, color)
            
            self.pixels.show()
            time.sleep(speed / 1000.0)  # Konvertiere Geschwindigkeit von ms in Sekunden
            
            if not fill:
                self.pixels.clear()
            
            if direction == "in":
                start_pixel += ring_size
            else:
                stop_pixel -= ring_size


    def all_on(self, color):
        # Setzt alle LEDs auf die angegebene Farbe
        for pixel in range(self.num_pixels):
            self.pixels.set_pixel(pixel, color)
            
        self.pixels.show()


    def wheel(self, pos):
        """Gibt eine Farbe basierend auf einer Position zurück, die im Farbrad bestimmt ist."""
        if pos < 0 or pos > 255:
            return (0, 0, 0)
        elif pos < 85:
            return (int(pos * 3), int(255 - pos * 3), 0)
        elif pos < 170:
            pos -= 85
            return (int(255 - pos * 3), 0, int(pos * 3))
        else:
            pos -= 170
            return (0, int(pos * 3), int(255 - pos * 3))

    def rainbow_inside_out(self, duration):
        """Zeigt einen Rainbow-Effekt von innen nach außen, wobei die Farbe in jedem Ring identisch ist."""
        offsets = [sum(self.ring_pixel_sizes[:i]) for i in range(len(self.ring_pixel_sizes))]
        start_time = time.time()

        while time.time() - start_time < duration:
            for j in range(255):
                for ring in range(len(self.ring_pixel_sizes)):
                    color = self.wheel((j + (ring * 35)) & 255)  # Offset für jeden Ring für eine sanfte Verschiebung
                    start = offsets[ring]
                    end = start + self.ring_pixel_sizes[ring]
                    for i in range(start, end):
                        self.pixels[i] = color
                self.pixels.show()
        
        
    def clock_hand_effect(self, color, duration, speed, inverted=False):
        """Simuliert einen Uhrzeiger, der in allen Ringen den gleichen Winkel anzeigt."""
        offsets = [sum(self.ring_pixel_sizes[:i]) for i in range(len(self.ring_pixel_sizes))]
        start_time = time.time()
        step = 0

        while time.time() - start_time < duration:
            for ring in range(len(self.ring_pixel_sizes)):
                self.num_pixels_in_ring = self.ring_pixel_sizes[ring]
                angle_fraction = step / max(self.ring_pixel_sizes)  # Normalisierte Position des Zeigers
                pos = int(angle_fraction * self.num_pixels_in_ring) % self.num_pixels_in_ring

                if inverted:
                    # Setze alle Pixel im Ring auf die gewünschte Farbe
                    for i in range(self.num_pixels_in_ring):
                        if i != pos:
                            self.pixels[offsets[ring] + i] = color
                        else:
                            self.pixels[offsets[ring] + i] = (0, 0, 0)
                else:
                    # Nur den Zeiger setzen
                    self.pixels[offsets[ring] + pos] = color

            self.pixels.show()
            time.sleep(speed)

            # Lösche die vorherige Position des Zeigers oder fülle den Ring, je nach Inversion
            for ring in range(len(self.ring_pixel_sizes)):
                self.num_pixels_in_ring = self.ring_pixel_sizes[ring]
                angle_fraction = step / max(self.ring_pixel_sizes)
                pos = int(angle_fraction * self.num_pixels_in_ring) % self.num_pixels_in_ring

                if inverted:
                    # Fülle das Pixel, das gerade der Zeiger war
                    self.pixels[offsets[ring] + pos] = color
                else:
                    # Lösche nur das Pixel, das der Zeiger war
                    self.pixels[offsets[ring] + pos] = (0, 0, 0)

            step += 1
        

    def random_lights(self, color, duration, speed):
        start_time = time.time()  # Speichere den Startzeitpunkt
        frame_delay = speed / 1000.0  # Zeit zwischen den Frames in Sekunden
        
        while (time.time() - start_time) < duration:
            # Bereinige alle LEDs
            pixel_buffer = [(0, 0, 0)] * self.num_pixels
            
            for i in range(self.num_pixels):
                if random.random() < 0.5:  # 50% Chance, dass eine LED ein- oder ausgeschaltet wird
                    # Generiere eine zufällige Helligkeit
                    brightness = random.randint(0, 255)
                    # Setze die LED auf eine zufällige Farbe mit der berechneten Helligkeit
                    pixel_buffer[i] = (int(color[0] * (brightness / 255)),
                                       int(color[1] * (brightness / 255)),
                                       int(color[2] * (brightness / 255)))
            
            # Wende die Änderungen an
            for i, color_rgb in enumerate(pixel_buffer):
                self.pixels.set_pixel(i, color_rgb)
            self.pixels.show()

            # Verzögere die nächste Position basierend auf der Geschwindigkeit
            time.sleep(frame_delay)


    def twinkle(self, duration, color):
        """Simuliert ein zufälliges Aufleuchten von LEDs wie Sterne."""
        start_time = time.time()
        
        while time.time() - start_time < duration:
            pixel = random.randint(0, self.num_pixels - 1)  # Zufällige LED auswählen
            brightness = random.randint(50, 255)  # Zufällige Helligkeit
            
            # Farbe mit zufälliger Helligkeit setzen
            adjusted_color = tuple(int(c * (brightness / 255.0)) for c in color)
            self.pixels.set_pixel(pixel, adjusted_color)
            self.pixels.show()
            
            time.sleep(random.uniform(0.05, 0.2))  # Zufällige Pause zwischen Aufleuchten
            self.pixels.set_pixel(pixel, (0, 0, 0))  # LED wieder ausschalten

        self.pixels.clear()
        self.pixels.show()


    def heartbeat_effect(self, color, duration, pulse_speed=0.05, pause_time=0.3):
        """Simuliert einen Herzschlag-Effekt, bei dem die LEDs zwischen 50 % und 100 % Helligkeit pulsieren."""
        start_time = time.time()

        while time.time() - start_time < duration:
            # Schnelles Aufleuchten (25% -> 100%)
            for brightness in range(75, 256, 15):  # Von 50% zu 100% Helligkeit
                scaled_color = (brightness * color[0] // 255, brightness * color[1] // 255, brightness * color[2] // 255)
                self.pixels.fill(scaled_color)
                self.pixels.show()
                time.sleep(pulse_speed)

            # Schnelles Abdunkeln (100% -> 25%)
            for brightness in range(255, 75, -15):  # Zurück zu 50% Helligkeit
                scaled_color = (brightness * color[0] // 255, brightness * color[1] // 255, brightness * color[2] // 255)
                self.pixels.fill(scaled_color)
                self.pixels.show()
                time.sleep(pulse_speed)
            
            # Kurze Pause bei 25% Helligkeit
            self.pixels.fill((75 * color[0] // 255, 75 * color[1] // 255, 75 * color[2] // 255))
            self.pixels.show()
            time.sleep(pause_time)

            # Zweiter kleiner Herzschlag (optional, auch 25% -> 100%)
            for brightness in range(75, 200, 15):  # Weniger intensiver Schlag (50% -> ~80%)
                scaled_color = (brightness * color[0] // 255, brightness * color[1] // 255, brightness * color[2] // 255)
                self.pixels.fill(scaled_color)
                self.pixels.show()
                time.sleep(pulse_speed)
                
            for brightness in range(200, 75, -15):  # Zurück zu 50% Helligkeit
                scaled_color = (brightness * color[0] // 255, brightness * color[1] // 255, brightness * color[2] // 255)
                self.pixels.fill(scaled_color)
                self.pixels.show()
                time.sleep(pulse_speed)
            
            # Längere Pause zwischen den Herzschlägen bei 25% Helligkeit
            self.pixels.fill((75 * color[0] // 255, 75 * color[1] // 255, 75 * color[2] // 255))
            self.pixels.show()
            time.sleep(pause_time * 1.5)




    def color_wipe(self, color1, color2, wipe_speed=0.05):
        """Wischt eine Farbe über die LEDs und ersetzt sie durch eine andere."""
        for i in range(self.num_pixels):
            self.pixels.set_pixel(i, color1)
            self.pixels.show()
            time.sleep(wipe_speed)
        
        for i in range(self.num_pixels):
            self.pixels.set_pixel(i, color2)
            self.pixels.show()
            time.sleep(wipe_speed)


    def theater_chase(self, color, duration, chase_speed=0.1):
        """Simuliert einen Theater-Chase-Effekt, bei dem eine Reihe von LEDs sich anschaltet und 'läuft'."""
        start_time = time.time()
        
        while time.time() - start_time < duration:
            for q in range(3):  # Wir machen den Chase-Effekt in 3er-Schritten
                for i in range(q, self.num_pixels, 3):  # Startet bei q, dann jeder 3. Pixel
                    self.pixels.set_pixel(i, color)
                self.pixels.show()
                time.sleep(chase_speed)
                for i in range(q, self.num_pixels, 3):  # Setzt die LEDs wieder auf Schwarz zurück
                    self.pixels.set_pixel(i, (0, 0, 0))
                    
    def strobe(self, color, frequency, duration):
        """
        Erzeugt einen Blink- oder Flash-Effekt.
        
        Args:
            color (tuple): Die Farbe des Blitzes als RGB-Wert.
            frequency (float): Die Anzahl der Blitze pro Sekunde.
            duration (float): Die Gesamtdauer des Effekts in Sekunden.
        """
        start_time = time.time()
        interval = 1.0 / frequency  # Zeit zwischen den Blitzen

        while time.time() - start_time < duration:
            # LEDs einschalten
            self.pixels.fill(color)
            self.pixels.show()
            time.sleep(interval / 2)  # Halbe Zeit an

            # LEDs ausschalten
            self.pixels.fill((0, 0, 0))
            self.pixels.show()
            time.sleep(interval / 2)  # Halbe Zeit aus




# Beispielhafte Verwendung der Klasse
if __name__ == "__main__":
    ring_pixel_sizes = [40, 32, 24, 16, 12, 8, 1]  # Beispielgrößen
    num_pixels = sum(ring_pixel_sizes)
    pe = PIXEL_EFFECTS(num_pixels, ring_pixel_sizes)
    
        
    # Beispiel für die Start Sequenz
    pe.in_and_out("in", 125, red, False)
    pe.in_and_out("out", 500, red, True)
    pe.all_on(green)
    time.sleep(1)
    
    pe.twinkle(5, red)
    
    # Funktionsaufruf:
    pe.heartbeat_effect(red, 2)  # 10 Sekunden lang ein roter Herzschlag-Effekt
    pe.heartbeat_effect(orange, 2)  # 10 Sekunden lang ein roter Herzschlag-Effekt
    pe.heartbeat_effect(yellow, 2)  # 10 Sekunden lang ein roter Herzschlag-Effekt
    
    pe.theater_chase(orange, 10)
    
    pe.color_wipe(green, blue)
    pe.theater_chase(green, 10)
    
    # Rainbow-Effekt für 10 Sekunden ausführen
    pe.rainbow_inside_out(duration=10)

    # Uhrzeiger-Effekt (läuft für 5 Sekunden)
    pe.clock_hand_effect((255, 255, 0), duration=2.15, speed=0.05, inverted=False)
    
    # Invertierter Uhrzeiger-Effekt (läuft für 5 Sekunden)
    pe.clock_hand_effect((255, 0, 255), duration=5, speed=0.05, inverted=True)


    pe.random_lights(yellow, 2,100)
    pe.random_lights(green, 2,100)
    pe.random_lights(blue, 2,100)

    pe.strobe(red,4,3)