# crickit/tap_tempo.py

# Demonstrate synchronization of rhythmic output to touch activations on the
# Crickit.  Note: the CPB itself can also detect touch inputs, so the essence of
# the example could be adapted to work without the Crickit.

# Related documentation:
# http://docs.circuitpython.org/projects/crickit/en/latest/
# http://docs.circuitpython.org/projects/seesaw/en/latest/
# https://learn.adafruit.com/adafruit-crickit-creative-robotic-interactive-construction-kit

# ----------------------------------------------------------------
# Import standard Python modules.
import time, math

# Import the Crickit interface library.
from adafruit_crickit import crickit

# Import the board-specific input/output library for the CPB itself
from adafruit_circuitplayground import cp

# Configure the NeoPixel LED array for bulk update.
cp.pixels.auto_write = False

# Turn down the NeoPixel brightness, otherwise it is somewhat blinding.
cp.pixels.brightness = 0.5

# ----------------------------------------------------------------
# Each instance of the following class creates an rhythmic color display on a
# single NeoPixel.  The instance should be polled periodically from the event
# loop to update the display.

class NeoOscillator:
    def __init__(self, led_index):
        self.led = led_index
        self.phase = 0                    # oscillator phase angle between 0 and 2 pi
        self.phase_rate = 0.5 * math.pi   # oscillation rate in phase/second
        self.intensity = 0
        
    def set_tempo(self, bpm):
        """Set the oscillator tempo in beats per minute."""
        # (beats/minute) * (minutes/second) * (radians/beat) -> radians/second
        self.phase_rate = bpm * (1.0/60.0) * (2*math.pi)

    def tap_event(self):
        """Process a synchronizing tap event by adjusting the phase rate."""

        # The output has been set up so the perceived start of the beat is in the middle of the phase
        # cycle.  Calculate the error in phase and apply feedback to the phase rate.
        # If the oscillator is running ahead of the tap, slow down the rate, else speed it up.
        phase_error = math.pi - self.phase
        self.phase_rate += 0.25 * phase_error
        
    def poll(self, elapsed):
        """Polling function to be called as frequently as possible from the event loop
        with the nanoseconds elapsed since the last cycle."""

        # advance the phase based on the elapsed time
        self.phase = math.fmod(self.phase + self.phase_rate * (1e-9*elapsed), 2*math.pi)

        # calculate new RGB values
        self.intensity = min(max(int(-255*math.sin(self.phase)), 0), 255)
        cp.pixels[self.led] = (self.intensity, self.intensity, self.intensity)

# ----------------------------------------------------------------
# Each instance of the following class processes touch inputs from a single
# capacitive touch input on the Crickit.  The instance should be polled
# periodically from the event loop to update the state.

class CrickitTouch:
    def __init__(self, touch_input):
        self.input  = touch_input  # the object to consult for touch state
        self.state  = False        # most recently observed input state

        # flags which are set for just one polling cycle to indicate events.
        self.tapped = False

    def poll(self, elapsed):
        """Polling function to be called as frequently as possible from the event loop
        with the nanoseconds elapsed since the last cycle."""

        # Reset any transient flags.
        self.tapped = False

        # Read the touch input and detect events.
        new_input = self.input.value
        if new_input != self.state:

            if new_input is True:
                self.tapped = True
                print("Tapped.")

            self.state = new_input

# ----------------------------------------------------------------
# Global state
moving = False
touched_last = False
last_timestamp = time.monotonic_ns()

# Initialize the oscillators and slightly perturb the initial tempos.
oscillators = [NeoOscillator(led) for led in range(10)]
for n, osc in enumerate(oscillators):
    osc.set_tempo(60 + 0.2 * n)

# Initialize the touch inputs.
touches = [CrickitTouch(crickit.touch_1), CrickitTouch(crickit.touch_4)]

# ----------------------------------------------------------------
# Enter the main event loop.

while True:

    # Calculate the time elapsed since last cycle (in nanoseconds).
    now = time.monotonic_ns()
    elapsed = now - last_timestamp
    last_timestamp = now

    # Update the oscillation outputs.
    for osc in oscillators:
        osc.poll(elapsed)

    # Send new data to the physical LED chain.
    cp.pixels.show()

    # Update the touch detectors.
    for touch in touches:
        touch.poll(elapsed)

    # If tapped, apply a phase correction to a set of oscillators.
    if touches[0].tapped:
        for osc in oscillators[0:5]:
            osc.tap_event()
            # print(f"phase: {osc.phase} rate: {osc.phase_rate} intensity: {osc.intensity}")
            
    if touches[1].tapped:
        for osc in oscillators[5:]:
            osc.tap_event()

