/******************************************************************************
 * Addressable LED Strip Light Saber V2
 * By Joe Larson, the 3D Printing Professor
 * http://3dpprofessor.com
 ******************************************************************************/

#include "FastLED.h"
//#include <Wire.h>
//#include <MPU6065.h>

/***************************************
 *        Hardware definitions         *
 ***************************************/
#define NUM_LEDS 50
#define LED_DATA_PIN 3
#define POT_PIN A3
/***************************************/
#define Brightness 255
CRGB leds[NUM_LEDS];
int curmode = 1;
CRGBPalette16 gPal;

void setup() 
{
  // put your setup code here, to run once:
  Serial.begin(9600);
  Serial.println("LED Strip Light Saber V1.2");
  Serial.println("By Joe Larson the 3D Printing Professor");
  Serial.println("http://3dpprofessor.com");

  pinMode(POT_PIN, INPUT);

  randomSeed(analogRead(A0));

  FastLED.addLeds<WS2812B, LED_DATA_PIN, GRB>(leds, NUM_LEDS); // Added "GRB" for my manufacturer who decided to mix up the colors
  FastLED.setBrightness(Brightness);
}

const int numModes = 4;
void loop() 
{
  curmode = getMode();
  
  switch (curmode) 
  {
    case 1:
      Serial.println("Solid Color Mode");
      colorPulse();
      break;
    case 2:
      Serial.println("Scroll Mode");
      paletteScroll();
      break;
    case 3:
      Serial.println("Fire Mode");
      fireBlade();
      break;
    case 4:
      Serial.println("Lava Lamp/Fireworks");
      LavaLampBlade();
      break;

    default:
      Serial.println("Blade is starting in dead zone. Move the Pot a bit.");
      delay(200);
  }
}

void retractBlade()
{
  for (int i = NUM_LEDS-1; i >= 0; i--) 
  {
    leds[i] = CRGB (0, 0, 0);
    FastLED.show();
    delay(10);
  }
}

/***************************************/
void colorPulse() 
{
  CRGB color = CHSV(getFineMode(255), 255, 255);
  Serial.println("Transition");
  retractBlade();
  for (int i = 0; i <= NUM_LEDS; i++) 
  {
    color = getBrightColor();
    fillWithWhiteGradientTo(i, color);
    FastLED.show();
    delay(20);
  }
        
  while (!isTimeToChange())
  {
    color = getBrightColor();

    // Active delay
    int delayamount = random(8)*500;
    while (delayamount > 0 && !isTimeToChange()) 
    {
      fillWithWhiteGradientTo(NUM_LEDS, getBrightColor());
      FastLED.show();
      delay (20);
      delayamount -= 20;
    }
    
    for (int pulse = 0; pulse < 12 && !isTimeToChange(); pulse++)
    {
      color = getBrightColor();
      int pulsefactor = 4 * pulse;
      fillWithWhiteGradientTo(NUM_LEDS, color+CRGB(pulsefactor, pulsefactor, pulsefactor));
      FastLED.show();
      delay(10);
    }
    for (int pulse = 12; pulse >= 0 && !isTimeToChange(); pulse--)
    {
      color = getBrightColor();
      int pulsefactor = 4 * pulse;
      fillWithWhiteGradientTo(NUM_LEDS, color+CRGB(pulsefactor, pulsefactor, pulsefactor));
      FastLED.show();
      delay(30);
    }
  }
  retractBlade();
}

CRGB getBrightColor()
{
  return CHSV(getFineMode(255), 255, 255);
}

void fillWithWhiteGradientTo(int num, CRGB color)
{
  // There's probably a FastLED library to do this, add a little white gradient
  // to the base of the blade, but I don't know what it is.
  int fade = 100;
  for (int i = 0; i < num; i++) 
  {
    leds[i] = color + CRGB (fade, fade, fade);
    fade -= 6;
    if (fade < 0) fade = 0;    
  }
}

/***************************************/
TBlendType currentBlending = LINEARBLEND;
void paletteScroll()
{
  // Borrowed from: https://wokwi.com/arduino/libraries/FastLED/ColorPalette

  uint8_t startIndex = 0;
  int chancetopulse = 0;
  int pulsefactor = 0;
  int bladelength = 1;
  word curFineMode = -1;
  while (!isTimeToChange())
  {
    // Choose the palate
    if (curFineMode != getFineMode(4))
    {
      curFineMode = getFineMode(4);
      retractBlade();
      bladelength = 1;
    }
    switch (curFineMode)
    {
      case 3:
        //Serial.println("Candy Cane Scroll");
        gPal = CRGBPalette16(
          CRGB::Red, CRGB::White, CRGB::Red, CRGB::White,
          CRGB::Red, CRGB::White, CRGB::Red, CRGB::White,
          CRGB::Red, CRGB::White, CRGB::Red, CRGB::White,
          CRGB::Red, CRGB::White, CRGB::Red, CRGB::White);
        currentBlending = NOBLEND;
        chancetopulse = 0;
        break;
      case 2:
        //Serial.println("Halloween Scroll");
        gPal = CRGBPalette16(
          CRGB::Orange, CRGB::Black, CRGB::OrangeRed, CRGB::Black,
          CRGB::Orange, CRGB::Black, CRGB::OrangeRed, CRGB::Black,
          CRGB::Orange, CRGB::Black, CRGB::OrangeRed, CRGB::Black,
          CRGB::Orange, CRGB::Black, CRGB::OrangeRed, CRGB::Black);
        currentBlending = NOBLEND;
        chancetopulse=10;
        break;
      case 1:
        //Serial.println("Camo Scroll");
        gPal = CRGBPalette16
        (
          CRGB::ForestGreen, CRGB::Black, CRGB::DarkGreen, CRGB::Tan,
          CRGB::Black, CRGB::GreenYellow, CRGB::Black, CRGB::ForestGreen, 
          CRGB::Gray, CRGB::Black, CRGB::DarkGreen,CRGB::LawnGreen,
          CRGB::Black, CRGB::Tan, CRGB::Black, CRGB::ForestGreen
        );
        currentBlending = LINEARBLEND;
        chancetopulse=0;
        break;
      default:
        // Rainbow Blade inspired by the short animation Star Wars Visions : The Twins.
        curFineMode = 0;
        gPal = CRGBPalette16
        (
          0xFF0000, 0xFF5540, 0xD52A00, 0xFFAA40,
          0xABAB00, 0x95FF40, 0x00FF00, 0x40FF6A,
          0x00AB55, 0x4095EA, 0x0000FF, 0x5540FF,
          0x2A00D5, 0xBF40C0, 0xD5002B, 0xFF4055
        );
        currentBlending = LINEARBLEND;
        chancetopulse = 50;
        break;
    }
    startIndex = startIndex + 1; /* motion speed */

    if( chancetopulse && random8() < chancetopulse ) {
      pulsefactor += 32;
    }

    FillLEDsFromPaletteColors(bladelength, startIndex, pulsefactor);
    FastLED.show();
    pulsefactor /= 1.2;
    if (bladelength < NUM_LEDS)
    {
      for (int i=0; (i < 4) && (bladelength < NUM_LEDS); i++)
      {
        delay(20);
        bladelength += 1;
        FillLEDsFromPaletteColors(bladelength, startIndex, pulsefactor);
        FastLED.show(); 
      }
    } else {
      bladelength = NUM_LEDS;
       FastLED.delay(40);
    }
  }
  retractBlade();
}

void FillLEDsFromPaletteColors(int num, uint8_t shift, uint8_t pulsefactor)
{
    uint8_t brightness = 255;

    for( int i = 0; i < num; i++) {
//        leds[i] = ColorFromPalette(gPal, 4*i-colorIndex, 255, currentBlending) + CRGB(pulsefactor, pulsefactor, pulsefactor);
        leds[i] = ColorFromPalette(gPal, ((float)i/15)*i-shift, 255, currentBlending) + CRGB(pulsefactor, pulsefactor, pulsefactor);
    }
}
/***************************************/
// Borrowed from Fire2012 by Mark Kriegsman, July 2012
// as part of "Five Elements" shown here: http://youtu.be/knWiGsmgycY
// https://wokwi.com/arduino/libraries/FastLED/Fire2012
#define COOLING  55
#define SPARKING 100

// Array of temperature readings at each simulation cell
static byte heat[NUM_LEDS];

void fireBlade()
{
  while (!isTimeToChange())
  {
    checkFirePalatte();
    fireHeatAndDrift();
    fireSpark();
    fireShow();
    delay(20);
  }
  // Burnout
  for (int i = 0; i < NUM_LEDS; i++) 
  {
    fireHeatAndDrift();
    fireShow();
    delay(20);
  }
}

void checkFirePalatte()
{
  switch (getFineMode(5))
  {
    case 1:
      gPal = LavaColors_p;
      break;
    case 2:
      gPal = CRGBPalette16( 
       CRGB::Black,CRGB::DarkGoldenrod,CRGB::Black,CRGB::CRGB::DarkOrange,
       CRGB::Orange, CRGB::Gold, CRGB::Yellow, CRGB::FairyLightNCC,
       CRGB::OrangeRed, CRGB::Yellow, CRGB::Orange, CRGB::OrangeRed,
       CRGB::LightGoldenrodYellow,CRGB::OrangeRed,CRGB::Red,CRGB::FairyLightNCC
      );
      break;
    case 3:
      gPal = CRGBPalette16( 
       CRGB::Black,CRGB::DarkGreen,CRGB::Black,CRGB::DarkOliveGreen,
       CRGB::Green, CRGB::ForestGreen, CRGB::OliveDrab, CRGB::Green,
       CRGB::SeaGreen, CRGB::MediumAquamarine, CRGB::LimeGreen, CRGB::CRGB::LawnGreen,
       CRGB::LightGreen,CRGB::LawnGreen,CRGB::MediumAquamarine,CRGB::ForestGreen
      );
      break;
    case 4:
      gPal = CRGBPalette16(
        CRGB::Black, CRGB::MidnightBlue, CRGB::Black, CRGB::Navy,
        CRGB::DarkBlue, CRGB::MediumBlue, CRGB::SeaGreen, CRGB::Teal,
        CRGB::CadetBlue, CRGB::Blue, CRGB::DarkCyan, CRGB::CornflowerBlue,
        CRGB::Aquamarine,CRGB::SeaGreen,CRGB::Aqua,CRGB::LightSkyBlue
      );
      break;
    default:
      gPal = HeatColors_p;
      break;
  }
}

void fireHeatAndDrift()
{
  // Step 1.  Cool down every cell a little
  for( int i = 0; i < NUM_LEDS; i++) 
  {
    heat[i] = qsub8( heat[i],  random8(0, ((COOLING * 10) / NUM_LEDS) + 2));
  }

  // Step 2.  Heat from each cell drifts 'up' and diffuses a little
  for( int k= NUM_LEDS - 1; k >= 2; k--) 
  {
    heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2] ) / 3;
  }
}

void fireSpark()
{
  // Step 3.  Randomly ignite new 'sparks' of heat near the bottom
  if( random8() < SPARKING ) {
    int y = random8(7);
    heat[y] = qadd8( heat[y], random8(160,255) );
  }
}

void fireShow()
{
  // Step 4.  Map from heat cells to LED colors
  for( int j = 0; j < NUM_LEDS; j++) {
    // Scale the heat value from 0-255 down to 0-240
    // for best results with color palettes.
    byte colorindex = scale8( heat[j], 240);
    leds[j] = ColorFromPalette( gPal, colorindex);
  }
      blur1d(leds, NUM_LEDS, 128);
  FastLED.show();
}
/***************************************/
int numParticles = 5;
void LavaLampBlade()
{
  int pPos[numParticles];
  int heat[NUM_LEDS];

  while (!isTimeToChange())
  {
    if (getFineMode(2) == 1)
    {
      fireworks();
    }
    else
    {
      CRGB DarkColor = CHSV((getFineMode(511)+128)%255, 100, 50);
      CRGB LightColor = CHSV(getFineMode(511), 255, 255);
      gPal = CRGBPalette16(
        DarkColor,DarkColor,DarkColor,DarkColor,
        LightColor,LightColor,LightColor,LightColor,
        LightColor,LightColor,LightColor,LightColor,
        LightColor,LightColor,LightColor,LightColor
        );

      // Move the "blobs"
      for (int p = 0; p < numParticles; p++)
      {
        pPos[p] = beatsin8(p+1, 0, NUM_LEDS-1);
      }

      for (int i = 0; i < NUM_LEDS; i++)
      {
        heat[i] = 0;
      }
      for (int p = 0; p < numParticles; p++)
      {
        heat[pPos[p]] = 200;
        for (int spread = 1; spread < 2*(numParticles-p)+1; spread++) 
        {
          if ((pPos[p] + spread) <= NUM_LEDS)
            heat[pPos[p] + spread] += 200/spread;
          if ((pPos[p] - spread) >= 0)
            heat[pPos[p] - spread] += 200/spread;
        }
      }
      for (int i = 0; i < NUM_LEDS; i++)
      {
        if (heat[i] > 240)
        {
          heat[i] = 240;
        }
        leds[i] = ColorFromPalette(gPal, heat[i]);
      }
      FastLED.show();
      delay(20);
    }
  }
}
/******************************************************************************/
const int numsparks = NUM_LEDS/3;
uint8_t fworkType[numsparks];
float fworkHeat[numsparks];
CRGB fworkColor[numsparks];
float fworkLocation[numsparks];
float fworkVelocity[numsparks];

void fireworks()
{
  while (!isTimeToChange())
  {
    if (getFineMode(2) == 0)
      return;
    fadeToBlackBy(leds, NUM_LEDS, 96);
    //fill_solid(leds, NUM_LEDS, CRGB::Black);   

    if (sparkCount() == 0 || random(100) <= 2) // randomly fire off a new one
    {
      newSpark(1, CRGB(29, 48, 48), 0, 1.5 + (float)random(13)/4.0);
    }
    updateFireworks();
    showFireworks();

    FastLED.show();
    delay(50);
  }
}

void updateFireworks() 
{
  for (int i = 0; i < numsparks; i++)
  {
    switch (fworkType[i])
    {
      case 0:
        break;
      case 1: // rising
        fworkLocation[i] += constrain(fworkVelocity[i], -1.0, 1.0);
        fworkVelocity[i] -= .08; // gravity
        if (fworkVelocity[i] < 1.0) { // Explode
          fworkType[i] = 0;
          int numNew = 6 + random16(6);
          float newDiamter = ((float)random16(4) / 2.0) + 2.0;
          CRGB newColor = CHSV(random(255), 192, 255);
          for (int j = 0; j < numNew; j++)
          {
            float newVel = (newDiamter / 2.0) - ((newDiamter/(float)numNew) * (float)(j));
            newSpark(2, newColor, fworkLocation[i]+newVel, newVel + (float)random16(10)/20.0);
          }
        }
        break;
      case 2: // exploding
        fworkLocation[i] += fworkVelocity[i];
        fworkHeat[i] -= 0.07;
        if (fworkHeat[i] <= 0)
        {
          fworkType[i] = 0;
        }
        else if (fworkHeat[i] < 0.5)
        {
          fworkColor[i] -= CRGB(128,128,128);
          fworkVelocity[i] /= 3;
          if (random8(100) < 50)
            fworkVelocity[i] += 0.05;
          else 
            fworkVelocity[i] -= 0.05;
          
          if (random8(100) < 50)
            fworkColor[i] += CRGB(25,25,25);
          else
            fworkColor[i] -= CRGB(25,25,25);
        }
        break;
    }
  }
}
void newSpark(uint8_t type, CRGB newCol, int newLoc, float newVel)
{
  int newfirework = nextdead();
  if (newfirework < 0)
  {
    //Serial.println("Out of new sparks for now");
    return;
  }
  fworkType[newfirework] = type;
  fworkHeat[newfirework] = 1.0;
  fworkColor[newfirework] = newCol;
  fworkLocation[newfirework] = newLoc;
  fworkVelocity[newfirework] = newVel;
}

void showFireworks()
{
  for (int i = 0; i < numsparks; i++)
  {
    //Serial.print(fworkVelocity[i]); Serial.print(",");
    if (fworkType[i] > 0)
    {
      if ((fworkLocation[i] < 0)|| (fworkLocation[i] >= NUM_LEDS))
      {
        fworkType[i] = 0; // just kill it if it goes out of bounds.
      }
      else
      {
        if (getFineMode(4) == 3)
          leds[NUM_LEDS - (int)fworkLocation[i]] += fworkColor[i];
        else
          leds[(int)fworkLocation[i]] += fworkColor[i];

      }
    }
  }
  //Serial.println();
}

int sparkCount()
{
  int retval = 0;

  for (int i = 0; i < numsparks; i++)
    if (fworkType[i] > 0)
      retval++;

  return retval;
}

int nextdead()
{
  for (int i = 0; i < numsparks; i++)
  {
    if (fworkType[i] == 0)
      return i;
  }
  return -1;
}
/******************************************************************************/
bool isTimeToChange()
{
  return getMode() != curmode;
}

const float range = 1024.0 / numModes;
int getMode() 
{
  /*****************************************************************************
   * Getmode changes the pot's 0-1024 input into one of numModes zone.
   * But it's tricky (because of course it is). Zone 1 is the first 255, so that
   * the pot data can be used to change through all the colors. Then the rest of
   * the zones are divided evenly among the remaining range with a "dead zone"
   * of 2 spaces between each, to overcome the "shake" that pots sometimes
   * return when straddling two inputs. If it's in that dead zone, it just echos
   * curmode value.
   *****************************************************************************/
  int val = analogRead(POT_PIN);
  
  int zone = (val/range)+1;
  
  if (val%(int)range < 2) // If it's close to the division edge, return "dead"
  {
    Serial.println("Returning curmode");
    return curmode;
  }
  return zone;
}

int getFineMode(int numZones)
{
  /*****************************************************************************
   * Returns between 0 and numZones-1 based on the pot's position within the
   * coarser "zone" returned in GetMode above.
   *****************************************************************************/
  int val = analogRead(POT_PIN);
  if (val%(int)range < 2) val -= 2; // Compensate for the dead zone
  int zone = val/range;
  int fineVal = val - (range * zone);
  return fineVal*((float)numZones/range);
}