/*
 * Gray_tensegramid
 * Published at:
 *   https://wokwi.com/arduino/projects/311872509197681216
 * Description: 
 *   Art project. A pyramid with tensegrity components
 * 
 * Why I made this:
 *   For the love of the blinky
 * 
 *
 * Code by: Gray Mack
 * License: Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) 
 *          https://creativecommons.org/licenses/by-nc-sa/4.0/
 * Created: 10/08/2021
 * Board: (can run arduino uno but ported to) Seeeduino XIAO samd21 M0 core 
 * 
 * Connections: 
 *    NeoPixel Ws2812b strip, button, potentimeter. See the pin constants section
 *    This will use up to about 903 pixels
 *    (arduino)-----[0]-[1]-[2]-...
 * 
 * Neopixel layout: Base pattern: 45 pixels * 4 sides
 *                                               0..15
 * Secondary pattern LEDs                        16..17
 * 
 * 
 * 10/08/2021 initial code creation
 * 05/18/2022 adding light sensor shutoff, possibly battery sensor
 */



// ----[ included libraries ]------------------------------------
#include <FastLED.h> // LED pixel library for neopixels
#include <Bounce2.h> // button debounce library

// ----[ pin definitions ]------------------------------------
#ifndef __AVR__
#define XAIO
#    pragma message "building for Seeeduino M0"
#endif

#ifdef XAIO
#define PIN_BUTTON_SPEED 1 // input button to ground
#define PIN_BUTTON_PATTERN 2
#define PIN_POTENTIOMETER_COLOR A3 // 1k to 100k potentiometer wiper pin. one side connected to GND and the other to 5v
#define PIN_POTENTIOMETER_BRIGHTNESS_SHUTOFF A4 // daylight off mode based on brightness sensor and this control
#define PIN_BRIGHTNESS_SENSOR A5
#define PIN_PIXEL_DATA_BASE 10
#define PIN_PIXEL_DATA_PYRAMID 9
#else
#define PIN_BUTTON_SPEED 4 // input button to ground
#define PIN_PIXEL_DATA_BASE 3
#define PIN_PIXEL_DATA_PYRAMID 5
#define PIN_BUTTON_PATTERN 6
#define PIN_POTENTIOMETER_COLOR A0 // 1k to 100k potentiometer wiper pin. one side connected to GND and the other to 5v
#define PIN_POTENTIOMETER_BRIGHTNESS_SHUTOFF A1 // daylight off mode based on brightness sensor and this control
#define PIN_BRIGHTNESS_SENSOR A2
#endif

// ----[ constants ]------------------------------------
#define SERIAL_SPEED 115200

#define NUM_LEDS_PTOP_EDGE 41
#define NUM_LEDS_PMID_EDGE 35
#define NUM_LEDS_DLOWER_EDGE 41
#define NUM_LEDS_PBASE_EDGE 52
#define PYRAMID_SIDES 4
#define NUM_LEDS_DIAMOND ((NUM_LEDS_PTOP_EDGE + NUM_LEDS_PMID_EDGE + NUM_LEDS_DLOWER_EDGE)*PYRAMID_SIDES + 1)
#define NUM_LEDS_BASE (NUM_LEDS_PBASE_EDGE*PYRAMID_SIDES)
#define NUM_LEDS (NUM_LEDS_DIAMOND + NUM_LEDS_BASE)

#define STATUS_PIXEL 0
#define LEDS_BEGIN_PTOP_EDGES 1
#define LEDS_END_PTOP_EDGE1 (LEDS_BEGIN_PTOP_EDGES + NUM_LEDS_PTOP_EDGE-1)
#define LEDS_END_PTOP_EDGES (LEDS_BEGIN_PTOP_EDGES + NUM_LEDS_PTOP_EDGE*PYRAMID_SIDES-1) 
#define LEDS_BEGIN_PMID_EDGES (LEDS_END_PTOP_EDGES+1)
#define LEDS_END_PMID_EDGES1 (LEDS_BEGIN_PMID_EDGES+NUM_LEDS_PMID_EDGE-1)
#define LEDS_END_PMID_EDGES (LEDS_BEGIN_PMID_EDGES+NUM_LEDS_PMID_EDGE*PYRAMID_SIDES-1)
#define LEDS_BEGIN_DLOWER_EDGES (LEDS_END_PMID_EDGES+1)
#define LEDS_END_DLOWER_EDGES1 (LEDS_BEGIN_DLOWER_EDGES+NUM_LEDS_DLOWER_EDGE-1)
#define LEDS_END_DLOWER_EDGES (LEDS_BEGIN_DLOWER_EDGES+NUM_LEDS_DLOWER_EDGE*PYRAMID_SIDES-1)
#define LEDS_BEGIN_PBASE_EDGES (LEDS_END_DLOWER_EDGES+1)
#define LEDS_END_PBASE_EDGE1 (LEDS_BEGIN_PBASE_EDGES+NUM_LEDS_PBASE_EDGE-1)
#define LEDS_END_PBASE_EDGES (LEDS_BEGIN_PBASE_EDGES+NUM_LEDS_PBASE_EDGE*PYRAMID_SIDES-1)

#define NUM_BASE_PATTERNS 7
#define ADC_MAX_VALUE 1023 // UNO has a 10bit ADC so 0 to 2^10-1 range, other devices may have 12bit ADC
#define POT_CONTROLS_COLOR true

#define NUM_MIRROR_ENUMS 3
enum Mirroring { MirroringOff, DuplicateEdges, MirrorEdges };
      // DuplicateEdges means all 4 sides show the same lights
      // MirrorEdges means side 0,2 show the same but sides 1,3 show mirror if side 0
      
#define NUM_SECTION_ENUMS 4
enum Section { SectionPTop, SectionPMid, SectionDLower, SectionPBase };

const int AMBIENT_LIGHT_CHECK_EVERY_MSEC = 500;
#define BRIGHTNESS_FOR_A_WHILE_TICKS (30000/AMBIENT_LIGHT_CHECK_EVERY_MSEC)



// ----[ global variables ]------------------------------------
CRGB leds[NUM_LEDS]; // Define the array of leds for FastLED
int PotentimeterValueColor = 0;
int PotentiometerValueLightMinder = 0;
int BrightnessSensorValue = 0;
Bounce2::Button SpeedButton = Bounce2::Button();
Bounce2::Button PatternButton = Bounce2::Button();
int WhichPattern = 0;
bool StartingPattern = true;
CRGB PrimaryColor = CRGB::Blue;
CHSV PrimaryColorHsv = CHSV(HUE_BLUE, 255, 255);
CRGB SecondaryColor = CRGB::White - CRGB::Blue;
CHSV SecondaryColorHsv = CHSV(255-HUE_BLUE, 255, 255);
CRGB TrinaryColor = CRGB::Red;
CHSV TrinaryColorHsv = CHSV(HUE_RED, 255, 255);
unsigned long AnimateRunningDotFrameEveryMsec = 50; // 1000msec/50 => 20 times per second
unsigned long AnimateRunnersFrameEveryMsec = 10;    // 1000msec/10 => 100 times per second
unsigned long AnimateRandomPixelFrameEveryMsec = 25;  // 1000msec/25 => 40 times per second
unsigned int TicksToGoIntoLowPowerMode = 0;

struct LightEntityConntainer {
   Section HeadSection;
   float HeadPosition;
   //int IntHeadPosition;
   //int8_t Direction;
   uint8_t TailLength;
   float Speed;
   CHSV ColorHsv;
   int BulletPosition;
   int Data;
};

const int LightEntityConntainerCount = 20;
const int SmallerLightEntityCount = 12; // in cases where we don't want to use them all
static LightEntityConntainer LightEntities[LightEntityConntainerCount];

// ----[ code ]------------------------------------

void setup() 
{
   pinMode(LED_BUILTIN, OUTPUT); 
   Serial.begin(SERIAL_SPEED);
   Serial.println("Arduino reset");
   LEDS.addLeds<WS2812, PIN_PIXEL_DATA_PYRAMID, GRB>(leds, 0, NUM_LEDS_DIAMOND); // my pixels are RGB but wokwi pixels are GRB
   LEDS.addLeds<WS2812, PIN_PIXEL_DATA_BASE,    GRB>(leds, LEDS_BEGIN_PBASE_EDGES, NUM_LEDS_BASE);
   LEDS.setBrightness(255); // half brightness
   LEDS.setMaxPowerInVoltsAndMilliamps(5, 4000);
#ifdef XAIO
   analogReadResolution(10);
#endif
   Serial.println("analog read");
   PotentimeterValueColor = analogRead(PIN_POTENTIOMETER_COLOR);
   PotentiometerValueLightMinder = analogRead(PIN_POTENTIOMETER_BRIGHTNESS_SHUTOFF);
   BrightnessSensorValue = analogRead(PIN_BRIGHTNESS_SENSOR);
   Serial.println("button");
   SpeedButton.attach(PIN_BUTTON_SPEED, INPUT_PULLUP); // use internal pull-ups
   SpeedButton.interval(5); // debounce interval Msec
   SpeedButton.setPressedState(LOW); // indicate that the low state corresponds to physically pressing the button
   PatternButton.attach(PIN_BUTTON_PATTERN, INPUT_PULLUP); // use internal pull-ups
   PatternButton.interval(5); // debounce interval Msec
   PatternButton.setPressedState(LOW); // indicate that the low state corresponds to physically pressing the button
   Serial.println("LEDs");

   // powerup sequence led tests
   leds[3] = CRGB(249, 0, 0);
   Serial.println("LEDs2");
   FastLED.show(); //start the leds
   Serial.println("LEDs3");

   Serial.println("setup done, millis=");
   Serial.println(millis());

   test();
   //TestBrightness();
   //TestSetPixelWithMirror();
}


void loop()
{
   // we try to keep loop simple so it tells the story of what we are doing through function calls
   // call the reentrant functions as often as we can, each will only act when its timer is ready, otherwise they just return with no action
   PatternSwitcher(WhichPattern, StartingPattern, false); // is it time to change patterns?

   switch(WhichPattern) // handle 0 to NUM_BASE_PATTERNS-1 patterns
   {
      default: // anything else goes back to the first pattern
      case 0: AnimateBasicRunningDotAndThrob();
         break;
      case 1: AnimateRandomPixel(); 
         break;
      case 2: AnimateRunners(); 
         break;
      case 3: AnimateRotator();
         break;
      case 4: AnimateBaseRepeatersWithPulseAndFlare();
         break;
      case 5: AnimateColoredRunningDots();
         break;
      case 6: AnimateRandomPixelWithFade();

   } // to add a pattern, increment NUM_BASE_PATTERNS

   ReadButtons();
   ReadPotentimeter(); // every 0.5sec
   ConsoleWriteStatus(); // every 60sec
   CheckForLowPowerMode();
   if(TicksToGoIntoLowPowerMode > BRIGHTNESS_FOR_A_WHILE_TICKS)
      LowPowerMode();
}





//-------------------------------------------------------------------------
//-------------------------------------------------------------------------
//----[ animations functions ]------------------------------------------

// Pattern 0 --------
void AnimateBasicRunningDotAndThrob()
{
   const int RunningDotTailLength = 5;
   const bool TailBothSides = true;
   const int TailDirection = -1;
   static unsigned long AnimationMillis = 0;
   static int ledDotPosition = LEDS_BEGIN_PTOP_EDGES;

   #define PBASE_EDGE_TOP (LEDS_BEGIN_PTOP_EDGES)
   #define THROB_TOP_RADIUS 8
   #define PBASE_EDGE_MIDDLE (LEDS_BEGIN_PBASE_EDGES + NUM_LEDS_PBASE_EDGE / 2)
   #define THROB_BASE_RADIUS 8
   #define DLOWER_EDGE_MIDDLE (LEDS_BEGIN_DLOWER_EDGES + NUM_LEDS_DLOWER_EDGE / 2)
   #define THROB_LOWER_RADIUS 3
   
   static float topThrobAmount = 0;
   static float topThrobSpeed = 32;
   static float baseThrobAmount = 0;
   static float baseThrobSpeed = 0.8;
   static float lowerThrobAmount = 0;
   static float lowerThrobSpeed = 10.0;
   static CHSV changingColor(0, 255, 255);
      
   if(millis() - AnimationMillis < AnimateRunningDotFrameEveryMsec) return;    // function will do nothing and just return until its time to change the LEDs. This pattern works even upon millis rollover in 54 days because of the unsigned subtraction and comparison.
   
   AnimationMillis = millis(); // update the time so that we won't be called again until its time
   
   if(StartingPattern)
   {
      StartingPattern = false;
      ledDotPosition = GetStartPixel(SectionPMid);
   }
   ClearAllPixels();
   int dotstart = GetStartPixel(SectionPMid);
   int dotend = GetEndPixel(SectionPMid, MirrorEdges);
   ledDotPosition = WrapPosition(ledDotPosition+TailDirection, dotstart, dotend);
   AnimateRunningDotComponent(TailDirection, SectionPMid, dotstart, dotend, MirrorEdges, RunningDotTailLength, TailBothSides, ledDotPosition, PrimaryColorHsv);

   //baseThrobAmount += 0.1570795; // @50ms: 20calls/sec *18 = 360deg (convert 18 to radians and divide by 2 since its rectified)
   Throb2(PBASE_EDGE_TOP, THROB_TOP_RADIUS, topThrobAmount, topThrobSpeed, SectionPTop, DuplicateEdges, TrinaryColorHsv);
   Throb2(PBASE_EDGE_MIDDLE, THROB_BASE_RADIUS, baseThrobAmount, baseThrobSpeed, SectionPBase, DuplicateEdges, SecondaryColorHsv);
   changingColor.hue++;
   Throb2(DLOWER_EDGE_MIDDLE, THROB_LOWER_RADIUS, lowerThrobAmount, lowerThrobSpeed, SectionDLower, DuplicateEdges, changingColor);
   
   FastLED.show();
}


// Pattern 1 --------
void AnimateRandomPixel()
{ // light one random pixel at a time to a random color
   static unsigned long AnimationMillis = 0;
   static int previousPix = LEDS_BEGIN_PTOP_EDGES;
   
   if(millis() - AnimationMillis < AnimateRandomPixelFrameEveryMsec) return; 
   // function will do nothing and just return until its time change the LEDs.
   
   AnimationMillis = millis(); // update the time so that we won't be called again until its time

   if(StartingPattern)
   {
      StartingPattern = false;
      ClearAllPixels();
   }
   int newPix = random(LEDS_BEGIN_PTOP_EDGES, LEDS_END_PBASE_EDGES+1);
   CRGB newColor = CHSV(random(0, 256), 255, 255); // This is a FastLED function that lets us convert 0-255 value to a color in the hue rainbow and we want a random color https://github.com/FastLED/FastLED/wiki/FastLED-HSV-Colors
   leds[previousPix] = CRGB::Black;
   leds[newPix] = newColor;
   FastLED.show();
   previousPix = newPix;
}


// Pattern 2 --------
void AnimateRunners()
{ // 10 light runs down the pixels bucket brigade style with random features
   static unsigned long AnimationMillis = 0;

   const int RunnerCount = 10;
   static Section section[RunnerCount];
   static Mirroring mirror[RunnerCount];
   static int ledDotPosition[RunnerCount];
   static int8_t direction[RunnerCount];
   
   
   static int tailLength[RunnerCount];
   static bool tailBothSides[RunnerCount];
   static int cycleDelays[RunnerCount];
   static CHSV runnerColor[RunnerCount];
   static int cycle = 0;
      
   if(millis() - AnimationMillis < AnimateRunnersFrameEveryMsec) return;    // function will do nothing and just return until its time change the LEDs. This pattern works even upon millis rollover in 54 days because of the unsigned subtraction and comparison.
   
   AnimationMillis = millis(); // update the time so that we won't be called again until its time
   
   if(StartingPattern)
   {
      StartingPattern = false;
      for(int r=0; r<RunnerCount; r++)
      {
         section[r] = (Section)random(NUM_SECTION_ENUMS);
         mirror[r] = (Mirroring)random(NUM_MIRROR_ENUMS);
         ledDotPosition[r] = GetStartPixel(section[r]);
         direction[r] = random(2) ? -1 : 1;
         tailLength[r] = random(6);
         tailBothSides[r] = random(2) ? true : false;
         cycleDelays[r] = random(1, 31);
         runnerColor[r] = CHSV(random8(), 255, 255);
      }
   }
            
   cycle++;
   ClearAllPixels();
   for(int r=0; r<RunnerCount; r++)
   {
      int dotstart = GetStartPixel(section[r]);
      int dotend = GetEndPixel(section[r], mirror[r]);
      if(cycle % cycleDelays[r] == 0)
      {
         ledDotPosition[r] = WrapPosition(ledDotPosition[r]+direction[r], dotstart, dotend);
      }
      AnimateRunningDotComponent(direction[r], section[r], dotstart, dotend, mirror[r], tailLength[r], tailBothSides[r], ledDotPosition[r], runnerColor[r]);
   }
   FastLED.show();
}


// light runs down the pixels bucket brigade style, optionally with tail
// expects a clean array each call, does not clear the tail
void AnimateRunningDotComponent(int8_t direction, Section section, int lowestPixel, int highestPixel, Mirroring mirror, int tailLength, bool tailBothSides, int position, const CHSV colorHsv)
{
   //SetPixelWithMirror(position, lowestPixel, highestPixel, mirror, colorHsv);
   SetPixelWithMirror2(position, section, mirror, colorHsv);
   for(int t=1; t<=tailLength; t++)
   {
      int tailPos = WrapPosition(position-(direction*t), lowestPixel, highestPixel);
      //color = blend(PrimaryColor, CRGB::White, (fract8)95);
      //nscale8(&color, 1, 100);
      //color.fadeToBlackBy(128);
      CRGB fade = CHSV(colorHsv.hue, colorHsv.sat, colorHsv.val / t);
      //hsv2rgb_rainbow(PrimaryColorHsv, fade);
      SetPixelWithMirror2(tailPos, section, mirror, fade);
      if(tailBothSides)
      {
         tailPos = WrapPosition(position+(direction*t), lowestPixel, highestPixel);
         SetPixelWithMirror2(tailPos, section, mirror, fade);
      }
   }
}

void AnimateRunningDotComponentRepeatEvery(int8_t direction, Section section, int lowestPixel, int highestPixel, Mirroring mirror, int tailLength, bool tailBothSides, int position, const CHSV colorHsv, uint8_t repeatEvery)
{
   int echoPos = position;
   int cycles = (highestPixel - lowestPixel + 1) / repeatEvery;
   while(cycles-- > 0)
   {
      AnimateRunningDotComponent(direction, section, lowestPixel, highestPixel, mirror, tailLength, tailBothSides, echoPos, colorHsv);
      echoPos = WrapPosition(echoPos+repeatEvery, lowestPixel, highestPixel);
   }
}


// Pattern 3 --------
void AnimateRotator()
{
 // light chasing spinner around on both tiers and when it hits corner, fire up pyramid
   static unsigned long AnimationMillis = 0;
   static int ledDotPosition[2] = { LEDS_BEGIN_PBASE_EDGES, LEDS_BEGIN_PBASE_EDGES };
   static int cycleDelays[2];
   static int cycle = 0;
   const int rotateDir1 = +1;
   const int rotateDir2 = -1;
   
   if(millis() - AnimationMillis < AnimateRunnersFrameEveryMsec /*reused*/) return; 
   
   AnimationMillis = millis(); // update the time so that we won't be called again until its time

   if(StartingPattern)
   {
      StartingPattern = false;
      ledDotPosition[0] = GetStartPixel(SectionPBase);
      ledDotPosition[1] = GetStartPixel(SectionPBase);
      cycleDelays[0] = 10;
      cycleDelays[1] = 3;
   }

   cycle++;
   ClearAllPixels();
   if((cycle % cycleDelays[0]) == 0)
   {
      ledDotPosition[0] = WrapPosition(ledDotPosition[0]+rotateDir1, GetStartPixel(SectionPBase), GetEndPixel(SectionPBase, MirroringOff));
   }
   AnimateRotateComponent(ledDotPosition[0], rotateDir1, PrimaryColorHsv);
   if((cycle % cycleDelays[1]) == 0)
   {
      ledDotPosition[1] = WrapPosition(ledDotPosition[1]+rotateDir2, GetStartPixel(SectionPBase), GetEndPixel(SectionPBase, MirroringOff));
   }
   AnimateRotateComponent(ledDotPosition[1], rotateDir2, SecondaryColorHsv);
   FastLED.show();
}

void AnimateRotateComponent(int &ledDotPosition, int8_t direction, const CHSV &colorHsv)
{
   int dotstart = GetStartPixel(SectionPBase);
   int dotend = GetEndPixel(SectionPBase, MirroringOff);
   AnimateRunningDotComponent(direction, SectionPBase, dotstart, dotend, MirroringOff, 5, false, ledDotPosition, colorHsv);

   int dotstart2 = GetStartPixel(SectionPMid);
   int dotend2 = GetEndPixel(SectionPMid, MirroringOff);
   int pmidPos = (ledDotPosition-dotstart) * NUM_LEDS_PMID_EDGE / NUM_LEDS_PBASE_EDGE + dotstart2;
   AnimateRunningDotComponent(direction, SectionPMid, dotstart2, dotend2, MirroringOff, 3, false, pmidPos, colorHsv);
   switch(ledDotPosition)
   {
      case LEDS_END_PBASE_EDGE1:
         fill_solid(&leds[LEDS_BEGIN_PTOP_EDGES+NUM_LEDS_PTOP_EDGE], NUM_LEDS_PTOP_EDGE, colorHsv);
         break;
      case LEDS_END_PBASE_EDGE1+NUM_LEDS_PBASE_EDGE:
         fill_solid(&leds[LEDS_BEGIN_PTOP_EDGES+NUM_LEDS_PTOP_EDGE*2], NUM_LEDS_PTOP_EDGE, colorHsv);
         break;
      case LEDS_END_PBASE_EDGE1+2*NUM_LEDS_PBASE_EDGE:
         fill_solid(&leds[LEDS_BEGIN_PTOP_EDGES+NUM_LEDS_PTOP_EDGE*3], NUM_LEDS_PTOP_EDGE, colorHsv);
         break;
      case LEDS_END_PBASE_EDGE1+3*NUM_LEDS_PBASE_EDGE:
         fill_solid(&leds[LEDS_BEGIN_PTOP_EDGES], NUM_LEDS_PTOP_EDGE, colorHsv);
         break;
   }
}

// Pattern 4 --------
void AnimateBaseRepeatersWithPulseAndFlare()
{
 // light chasers run down the pixels bucket brigade style on base
   static unsigned long AnimationMillis = 0;
   static int ledDotPositionBase = LEDS_BEGIN_PBASE_EDGES;
   static int ledDotPositionMid = LEDS_BEGIN_PMID_EDGES;
   static int ledDotPositionLower = LEDS_BEGIN_DLOWER_EDGES;
   static int ledDotPositionTop = LEDS_BEGIN_PTOP_EDGES;
   const int baseRepeaterDirection = +1;
   const int midRepeaterDirection = -1;
   const int topRepeaterDirection = -1;
      
   if(millis() - AnimationMillis < AnimateRunningDotFrameEveryMsec /*reused*/) return;    // function will do nothing and just return until its time change the LEDs. This pattern works even upon millis rollover in 54 days because of the unsigned subtraction and comparison.
   
   AnimationMillis = millis(); // update the time so that we won't be called again until its time
   
   if(StartingPattern)
   {
      StartingPattern = false;
      ledDotPositionBase = GetStartPixel(SectionPBase);
      ledDotPositionMid = GetStartPixel(SectionPMid);
   }
   ClearAllPixels();
   int dotstart = GetStartPixel(SectionPBase);
   int dotend = GetEndPixel(SectionPBase, MirroringOff);
   ledDotPositionBase = WrapPosition(ledDotPositionBase+baseRepeaterDirection, dotstart, dotend);
   AnimateRunningDotComponentRepeatEvery(baseRepeaterDirection, SectionPBase, dotstart, dotend, MirroringOff, 0, false, ledDotPositionBase, SecondaryColorHsv, 5);

   dotstart = GetStartPixel(SectionPMid);
   dotend = GetEndPixel(SectionPMid, MirroringOff);
   ledDotPositionMid = WrapPosition(ledDotPositionMid+midRepeaterDirection, dotstart, dotend);
   AnimateRunningDotComponentRepeatEvery(midRepeaterDirection, SectionPMid, dotstart, dotend, MirroringOff, 2, true, ledDotPositionMid, PrimaryColorHsv, 10);

   dotstart = GetStartPixel(SectionPTop);
   dotend = GetEndPixel(SectionPTop, DuplicateEdges);
   ledDotPositionTop = WrapPosition(ledDotPositionTop+topRepeaterDirection, dotstart, dotend);
   AnimateRunningDotComponent(topRepeaterDirection, SectionPTop, dotstart, dotend, DuplicateEdges, 1, false, ledDotPositionTop, TrinaryColorHsv);

   dotstart = GetStartPixel(SectionDLower);
   dotend = GetEndPixel(SectionDLower, DuplicateEdges);
   ledDotPositionLower = WrapPosition(ledDotPositionLower+topRepeaterDirection, dotstart, dotend);
   AnimateRunningDotComponent(topRepeaterDirection, SectionDLower, dotstart, dotend, DuplicateEdges, 1, false, ledDotPositionLower, TrinaryColorHsv);

   FastLED.show();  
}


// Pattern 5 --------
void AnimateColoredRunningDots()
{
   // Random speed and length dots travel around
   static unsigned long AnimationMillis = 0;

   if(millis() - AnimationMillis < AnimateRunningDotFrameEveryMsec) return;    // function will do nothing and just return until its time change the LEDs. This pattern works even upon millis rollover in 54 days because of the unsigned subtraction and comparison.
   AnimationMillis = millis(); // update the time so that we won't be called again until its time
   if(StartingPattern)
   {
      StartingPattern = false;
      for(uint8_t i=0; i<SmallerLightEntityCount; i++)
         InitLightEntity(i);
   }

   ClearAllPixels();
   for(uint8_t s=0; s<SmallerLightEntityCount; s++)
      RenderLightEntity(s);
   FastLED.show();
}

// Pattern 6 ---------
void AnimateRandomPixelWithFade()
{
   const int FadeAmount = 10;
   
   // random dots that fade out and then random somewhere else
   static unsigned long AnimationMillis = 0;

   if(millis() - AnimationMillis < AnimateRunningDotFrameEveryMsec) return;    // function will do nothing and just return until its time change the LEDs. This pattern works even upon millis rollover in 54 days because of the unsigned subtraction and comparison.
   AnimationMillis = millis(); // update the time so that we won't be called again until its time
   uint8_t i;
   if(StartingPattern)
   {
      StartingPattern = false;
      for(i=0; i<LightEntityConntainerCount; i++)
      {
         LightEntities[i].ColorHsv.val = 0;
      }
      ClearAllPixels();
         
   }
   
   for(i=0; i<LightEntityConntainerCount; i++)
   {
     if(LightEntities[i].ColorHsv.val == 0) // place new one
     {
       LightEntities[i].BulletPosition = random(LEDS_BEGIN_PTOP_EDGES, LEDS_END_PBASE_EDGES+1);
       LightEntities[i].ColorHsv = CHSV(random(0, 256), 255, 255);
       LightEntities[i].Data = random(FadeAmount);
       LightEntities[i].Speed = 25500.0; // brightness
     }
     LightEntities[i].Speed -= LightEntities[i].Data;
     if(LightEntities[i].Speed < 0) LightEntities[i].Speed = 0;
     LightEntities[i].ColorHsv.val = LightEntities[i].Speed / 100;
     leds[LightEntities[i].BulletPosition] = LightEntities[i].ColorHsv;
   }
   
   FastLED.show();
}
   
void AnimateSnakeFight()
{
   // Random speed and length snakes shoot at each other, 
   // if hit in the head, no change, if hit in the tail then loose one segment
   // base just clicks back and forth every 4th pix
   static unsigned long AnimationMillis = 0;
//x   static byte baseTimer = 0;

   if(millis() - AnimationMillis < AnimateRunningDotFrameEveryMsec) return;    // function will do nothing and just return until its time change the LEDs. This pattern works even upon millis rollover in 54 days because of the unsigned subtraction and comparison.
   AnimationMillis = millis(); // update the time so that we won't be called again until its time
   if(StartingPattern)
   {
      StartingPattern = false;
//x      baseTimer = 0;
      for(uint8_t i=0; i<SmallerLightEntityCount; i++)
         InitLightEntity(i);
   }

   ClearAllPixels();
   for(uint8_t s=0; s<SmallerLightEntityCount; s++)
      RenderLightEntity(s);
/*x
   if(++baseTimer > 20)
   {
      baseTimer = 0;
   }
   byte baseStart = baseTimer > 10 ? 0 : 1;
   for(int base=GetStartPixel(SectionPBase)+baseStart; base<=GetEndPixel(SectionPBase, DuplicateEdges); base+=3)
   {
     //leds[base] = TrinaryColor;
     SetPixelWithMirror2(base, SectionPBase, DuplicateEdges, TrinaryColor);
   }
x*/
   FastLED.show();
}








//-------------------------------------------------------------------------
//-------------------------------------------------------------------------
//----[ system helper functions ]------------------------------------------

// Utilize Bounce2 library to debounced and read a button as an event. This will change the speed of the secondary pattern
void ReadButtons()
{
   static uint8_t speedSwitcher = 0;
   SpeedButton.update();
   if(SpeedButton.pressed())
   {
      Serial.println("SpeedButton pressed");
      speedSwitcher++;
      switch(speedSwitcher)
      {  // cycle through a set of flash speeds 100ms->500ms->2000ms->150ms(lights disabled)->back to 100ms
      default: // anything else goes back to first option
         speedSwitcher = 0;
         //fallthrough
      case 0:
         AnimateRunningDotFrameEveryMsec = 50; // 1000msec/50 => 20 times per second
         AnimateRunnersFrameEveryMsec = 10;    // 1000msec/10 => 100 times per second
         AnimateRandomPixelFrameEveryMsec = 25;  // 1000msec/25 => 40 times per second
         //SecondaryPatternAnimationFrameEveryMsec = 500; 
         break;
      case 1: 
         AnimateRunningDotFrameEveryMsec = 200; // 1000msec/50 => 20 times per second
         AnimateRunnersFrameEveryMsec = 40;    // 1000msec/10 => 100 times per second
         AnimateRandomPixelFrameEveryMsec = 100;  // 1000msec/25 => 40 times per second
         //SecondaryPatternAnimationFrameEveryMsec = 2000; 
         break;
      case 2: 
         AnimateRunningDotFrameEveryMsec = 400; // 1000msec/50 => 20 times per second
         AnimateRunnersFrameEveryMsec = 80;    // 1000msec/10 => 100 times per second
         AnimateRandomPixelFrameEveryMsec = 200;  // 1000msec/25 => 40 times per second
         //SecondaryPatternAnimationFrameEveryMsec = 150; 
         break;
      case 3: 
         AnimateRunningDotFrameEveryMsec = 10; // 1000msec/50 => 20 times per second
         AnimateRunnersFrameEveryMsec = 1;     // 1000msec/10 => 100 times per second
         AnimateRandomPixelFrameEveryMsec = 5;  // 1000msec/25 => 40 times per second
         //SecondaryPatternAnimationFrameEveryMsec = 100; 
         break;
      case 4:
         AnimateRunningDotFrameEveryMsec = 20; // 1000msec/50 => 20 times per second
         AnimateRunnersFrameEveryMsec = 5;    // 1000msec/10 => 100 times per second
         AnimateRandomPixelFrameEveryMsec = 10;  // 1000msec/25 => 40 times per second
         //SecondaryPatternAnimationFrameEveryMsec = 100; 
         break;
    }
  }
  PatternButton.update();
  if(PatternButton.released())
  {
     Serial.println("PatternButton pressed");
     PatternSwitcher(WhichPattern, StartingPattern, true); // force switch
  }
}



// Read a potentiometer into the global PotentimeterValueColor. 
// For efficiency, since we intend the user to only adjust this occasionally, 
// we aren't trying for fast realtime reactive reading but it can easily be read faster if needed down to a few milliseconds
// We could also add smoothing or slow the reaction of change, scale its value, only change the value when a button is pushed, etc
void ReadPotentimeter()
{
   const unsigned long READ_POT_EVERY_MSEC = 100UL;
   static unsigned long potReadMillis = 0;
   static uint8_t hue = 0;
   #define LOW_END_POT 10
   #define HIGH_END_POT (ADC_MAX_VALUE-10)
   
   if(millis() - potReadMillis < READ_POT_EVERY_MSEC) return; // function will do nothing and just return until its time
   
   potReadMillis = millis(); // update the time so that we won't be called again until its time

   PotentimeterValueColor = analogRead(PIN_POTENTIOMETER_COLOR);
   if(POT_CONTROLS_COLOR) 
   {
      if(PotentimeterValueColor > LOW_END_POT)
      {
         PotentimeterValueColor = min(PotentimeterValueColor, HIGH_END_POT);
         hue = map(PotentimeterValueColor, LOW_END_POT+1, HIGH_END_POT, 0, 255);
      }
      else // rainbow cycle
      {
         hue++;
      }
      PrimaryColorHsv = CHSV(hue, 255, 255);
      PrimaryColor = CRGB(PrimaryColorHsv);
      SecondaryColorHsv = CHSV((hue+128)%256, 255, 255);
      SecondaryColor = SecondaryColorHsv;
      TrinaryColorHsv = CHSV((hue+64)%256, 255, 255);
      TrinaryColor = TrinaryColorHsv;
      leds[STATUS_PIXEL] = PrimaryColor;
   }
}


//Occasionally provide a readout of the status
void ConsoleWriteStatus()
{
   const unsigned long CONSOLE_WRITE_EVERY_MSEC = 5000UL;  // every 5 seconds
   static unsigned long lastConsoleWritedMillis = 0;
   
   if(millis() - lastConsoleWritedMillis < CONSOLE_WRITE_EVERY_MSEC) return; // function will do nothing and just return until its time
   
   lastConsoleWritedMillis = millis(); // update the time so that we won't be called again until its time

   Serial.print("Pattern: ");
   Serial.print(WhichPattern);
   Serial.print(" Pot: ");
   Serial.println(PotentimeterValueColor);

   Serial.print("Light pot:");
   Serial.print(PotentiometerValueLightMinder);
   Serial.print(" brightness:");
   Serial.print(BrightnessSensorValue);
   Serial.print(" ticks:");
   Serial.print(TicksToGoIntoLowPowerMode);
   if(BrightnessSensorValue > PotentiometerValueLightMinder) 
      Serial.println(" Sleep mode");
   else 
      Serial.println(" Awake mode");
}


void PatternSwitcher(int &currentPattern, bool &patternChanged, bool forceSwitch)
{
   const unsigned long SWITCH_PATTERNS_EVERY_MSEC = 60000UL; // every 1 minute
   static unsigned long LastPatternSwitchMillis = 0;
   
   if(millis() - LastPatternSwitchMillis < SWITCH_PATTERNS_EVERY_MSEC && !forceSwitch) return;
   
   LastPatternSwitchMillis = millis(); // update the time so that we won't be called again until its time

   Serial.println("Next pattern");
   patternChanged = true;
   currentPattern = (currentPattern+1) % NUM_BASE_PATTERNS;
   return;
}


void CheckForLowPowerMode()
{
   static unsigned long AmbinetLightCheckMillis = 0;
         
   if(millis() - AmbinetLightCheckMillis < AMBIENT_LIGHT_CHECK_EVERY_MSEC) return;    // function will do nothing and just return until its time to check. This pattern works even upon millis rollover in 54 days because of the unsigned subtraction and comparison.

   AmbinetLightCheckMillis = millis(); // update the time so that we won't be called again until its time
   
   PotentiometerValueLightMinder = analogRead(PIN_POTENTIOMETER_BRIGHTNESS_SHUTOFF);
   BrightnessSensorValue = analogRead(PIN_BRIGHTNESS_SENSOR);

   digitalWrite(LED_BUILTIN, BrightnessSensorValue > PotentiometerValueLightMinder);
   if(BrightnessSensorValue > PotentiometerValueLightMinder) 
      TicksToGoIntoLowPowerMode += 1;
   else 
      TicksToGoIntoLowPowerMode = 0;
   digitalWrite(LED_BUILTIN, TicksToGoIntoLowPowerMode > 0);
}

void LowPowerMode()
{
   FastLED.clear();
   FastLED.show();
   while(TicksToGoIntoLowPowerMode > BRIGHTNESS_FOR_A_WHILE_TICKS)
   {
     delay(1000);
     Serial.println("***Low Power***");
     CheckForLowPowerMode();
   }
}





//-------------------------------------------------------------------------
//-------------------------------------------------------------------------
//----[ LED helper functions ]------------------------------------------

int GetStartPixel(Section section)
{
   switch(section) {
      case SectionPTop: 
         return LEDS_BEGIN_PTOP_EDGES; 
      case SectionPMid: 
         return LEDS_BEGIN_PMID_EDGES; 
      case SectionDLower:
         return LEDS_BEGIN_DLOWER_EDGES;
      case SectionPBase: 
         return LEDS_BEGIN_PBASE_EDGES; 
      default:
         return 0;
   }

}

int GetEndPixel(Section section, Mirroring mirror)
{
   switch(section) {
      case SectionPTop: 
         return (mirror==MirroringOff)? LEDS_END_PTOP_EDGES : LEDS_END_PTOP_EDGE1; 
      case SectionPMid: 
         return (mirror==MirroringOff)? LEDS_END_PMID_EDGES : LEDS_END_PMID_EDGES1;
      case SectionDLower:
         return (mirror==MirroringOff)? LEDS_END_DLOWER_EDGES : LEDS_END_DLOWER_EDGES1;
      case SectionPBase: 
         return (mirror==MirroringOff)? LEDS_END_PBASE_EDGES : LEDS_END_PBASE_EDGE1;
      default:
         return 0;
   }
}

int WrapPosition(int current, int low, int high)
{
   if(current > high) 
   {
      int number = high-low+1;
      int overshoot = current-high;
      return low+(overshoot-1)%number;
   }
   if(current < low)
   {
      int number = high-low-1;
      int undershoot = low-current;
      return high-(undershoot-1)%number;
   }
   return current;
}


float WrapPosition(float current, int low, int high)
{
   if(current > high) 
   {
      int number = high-low+1;
      float overshoot = current-high;
      return low+fmod(overshoot-1, number);
   }
   if(current < low)
   {
      int number = high-low-1;
      float undershoot = low-current;
      return high-fmod(undershoot-1, number);
   }
   return current;
}
 

void SetPixelWithMirror2(int pixPosition, Section section, Mirroring mirror, CRGB color)
{
   if( ! inRange(pixPosition, section, mirror)) return;
   leds[pixPosition] = color;
   if(mirror == DuplicateEdges)
   {
      int offset1 = 0, offset2 = 0, offset3 = 0;
      if(section == SectionPTop) { offset1 = NUM_LEDS_PTOP_EDGE; offset2 = NUM_LEDS_PTOP_EDGE*2; offset3 = NUM_LEDS_PTOP_EDGE*3; }
      if(section == SectionPMid) { offset1 = NUM_LEDS_PMID_EDGE; offset2 = NUM_LEDS_PMID_EDGE*2; offset3 = NUM_LEDS_PMID_EDGE*3; }
      if(section == SectionDLower) { offset1 = NUM_LEDS_DLOWER_EDGE; offset2 = NUM_LEDS_DLOWER_EDGE*2; offset3 = NUM_LEDS_DLOWER_EDGE*3; }
      if(section == SectionPBase) { offset1 = NUM_LEDS_PBASE_EDGE; offset2 = NUM_LEDS_PBASE_EDGE*2; offset3 = NUM_LEDS_PBASE_EDGE*3; }
      leds[pixPosition+offset1] = color;
      leds[pixPosition+offset2] = color;
      leds[pixPosition+offset3] = color;
   }
   else if(mirror == MirrorEdges)
   {
      int lowestPixel = GetStartPixel(section);
      int reduction = pixPosition-lowestPixel;
      int offset1 = 0, offset2 = 0, offset3 = 0;
      if(section == SectionPTop) { offset1 = lowestPixel+NUM_LEDS_PTOP_EDGE*2-1; offset2 = NUM_LEDS_PTOP_EDGE*2; offset3 = lowestPixel+NUM_LEDS_PTOP_EDGE*4-1; }
      if(section == SectionPMid) { offset1 = lowestPixel+NUM_LEDS_PMID_EDGE*2-1; offset2 = NUM_LEDS_PMID_EDGE*2; offset3 = lowestPixel+NUM_LEDS_PMID_EDGE*4-1; }
      if(section == SectionDLower) { offset1 = lowestPixel+NUM_LEDS_DLOWER_EDGE*2-1; offset2 = NUM_LEDS_DLOWER_EDGE*2; offset3 = lowestPixel+NUM_LEDS_DLOWER_EDGE*4-1; }
      if(section == SectionPBase) { offset1 = lowestPixel+NUM_LEDS_PBASE_EDGE*2-1; offset2 = NUM_LEDS_PBASE_EDGE*2; offset3 = lowestPixel+NUM_LEDS_PBASE_EDGE*4-1; }
      leds[offset1-reduction] = color;
      leds[pixPosition+offset2] = color;
      leds[offset3-reduction] = color;
   }
}


bool inRange(int pos, int lowestPixel, int highestPixel)
{
   return pos >= lowestPixel && pos <= highestPixel;
}

bool inRange(int pos, Section section, Mirroring mirror)
{
   return pos >= GetStartPixel(section) && pos <= GetEndPixel(section, mirror);
}

void ClearAllPixels()
{
  FastLED.clearData();
  leds[STATUS_PIXEL] = PrimaryColor;
}


// light from position fading outward and dim by percent
void Throb2(int position, int radius, float &throbVariable, float &throbSpeed, Section section, Mirroring mirror, CHSV colorHsv)
{

   throbVariable += throbSpeed;
   if(throbVariable > 100) { throbVariable = 100; throbSpeed = -throbSpeed; }
   if(throbVariable < 0) { throbVariable = 0; throbSpeed = abs(throbSpeed); }
   for(int dist = 0; dist < radius; dist++)
   {
      float brightness = map(dist, 0,radius, 100,0) * throbVariable / 10000;
      CHSV faded = CHSV(colorHsv.hue, colorHsv.sat, constrain(int(colorHsv.val * brightness), 0, 255));
//      Serial.print("dist "); Serial.print(dist); Serial.print(" throbVariable "); Serial.print(throbVariable); Serial.print(" bright:"); Serial.print(brightness); Serial.print(" val:"); Serial.println(faded.val);
      SetPixelWithMirror2(position + dist, section, mirror, CRGB(faded));
      SetPixelWithMirror2(position - dist, section, mirror, faded);
   }
}


void InitLightEntity(uint8_t item)
{
   LightEntityConntainer* s = &LightEntities[item];
   s->HeadSection = (Section)random(NUM_SECTION_ENUMS);
   s->HeadPosition = random(GetStartPixel(LightEntities[item].HeadSection), 
                            GetEndPixel(LightEntities[item].HeadSection, MirroringOff));
   s->TailLength = random(10) + 2;
   s->ColorHsv = CHSV(random8(), 255, 255);
   s->Speed = float(random(10)+1)/10.0;
   if(random(2)) s->Speed = -(s->Speed);
   s->BulletPosition = -1;
}

// move the entity to the next space and draw it
void RenderLightEntity(uint8_t item)
{
   LightEntityConntainer* s = &LightEntities[item];
   s->HeadPosition = WrapPosition(s->HeadPosition + s->Speed, GetStartPixel(s->HeadSection), GetEndPixel(s->HeadSection, MirroringOff));

   int headPos = (int)s->HeadPosition;
   leds[headPos] = s->ColorHsv;
   //int8_t tailDirection = s->Speed > 0 ? -1 : 1;
   for(int tail = 0; tail<s->TailLength; tail++)
   {
//      int tailPos = headPos
//      leds[(
   }
}


//-------------------------------------------------------------------------
//-------------------------------------------------------------------------
//----[ Test functions ]------------------------------------------


void test()
{
  for(int i=0; i<NUM_LEDS; i++)
   {
      leds[i] = (i%10) ? CRGB::CRGB::Black : CRGB::Red;
      if(i==NUM_LEDS-1) leds[i]=CRGB::Yellow;
      FastLED.show();
      delay(1);
   }
  Serial.println("Expect 512 leds, have:");
  Serial.println(NUM_LEDS);
}


void TestBrightness()
{
   CHSV col = CHSV(HUE_RED, 255, 255);
   ClearAllPixels();
   for(int b=0; b<=50; b++)
   {
      float brightness = map(b, 0,50, 100,0);
      Serial.print("bright = "); Serial.println(brightness);
      leds[b] = CRGB(CHSV(col.hue, col.sat, col.val*brightness/100));
   }
   FastLED.show();
   delay(10000);
   float throbVariable = 100.0;
   float throbSpeed = 0.0;
   Throb2(32, 32, throbVariable, throbSpeed, SectionPTop, MirroringOff, col);
   FastLED.show();
   delay(20000);
}

void TestSetPixelWithMirror()
{
   int i;
   // SectionPTop
   for(i=GetStartPixel(SectionPTop);i<GetEndPixel(SectionPTop, MirroringOff);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionPTop, MirroringOff, CRGB::Red);
      FastLED.show();
      delay(10);
   }
   for(i=GetStartPixel(SectionPTop);i<GetEndPixel(SectionPTop, DuplicateEdges);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionPTop, DuplicateEdges, CRGB::Green);
      FastLED.show();
      delay(10);
   }
   for(i=GetStartPixel(SectionPTop);i<GetEndPixel(SectionPTop, MirrorEdges);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionPTop, MirrorEdges, CRGB::Blue);
      FastLED.show();
      delay(10);
   }
   
   // SectionPMid
   for(i=GetStartPixel(SectionPMid);i<GetEndPixel(SectionPMid, MirroringOff);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionPMid, MirroringOff, CRGB::Red);
      FastLED.show();
      delay(10);
   }
   for(i=GetStartPixel(SectionPMid);i<GetEndPixel(SectionPMid, DuplicateEdges);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionPMid, DuplicateEdges, CRGB::Green);
      FastLED.show();
      delay(10);
   }
   for(i=GetStartPixel(SectionPMid);i<GetEndPixel(SectionPMid, MirrorEdges);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionPMid, MirrorEdges, CRGB::Blue);
      FastLED.show();
      delay(10);
   }

      // SectionDLower
   for(i=GetStartPixel(SectionDLower);i<GetEndPixel(SectionDLower, MirroringOff);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionDLower, MirroringOff, CRGB::Red);
      FastLED.show();
      delay(10);
   }
   for(i=GetStartPixel(SectionDLower);i<GetEndPixel(SectionDLower, DuplicateEdges);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionDLower, DuplicateEdges, CRGB::Green);
      FastLED.show();
      delay(10);
   }
   for(i=GetStartPixel(SectionDLower);i<GetEndPixel(SectionDLower, MirrorEdges);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionDLower, MirrorEdges, CRGB::Blue);
      FastLED.show();
      delay(10);
   }

   // SectionPBase
      for(i=GetStartPixel(SectionPBase);i<GetEndPixel(SectionPBase, MirroringOff);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionPBase, MirroringOff, CRGB::Red);
      FastLED.show();
      delay(10);
   }
   for(i=GetStartPixel(SectionPBase);i<GetEndPixel(SectionPBase, DuplicateEdges);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionPBase, DuplicateEdges, CRGB::Green);
      FastLED.show();
      delay(10);
   }
   for(i=GetStartPixel(SectionPBase);i<GetEndPixel(SectionPBase, MirrorEdges);i++)
   {
      ClearAllPixels();
      SetPixelWithMirror2(i, SectionPBase, MirrorEdges, CRGB::Blue);
      FastLED.show();
      delay(10);
   }
}