#include <NeoPixelBus.h>
#include <WiFi.h>
#include "soc/cpu.h"
#include <atomic>

enum Mode
{
  None,
  Twinkle,
  CandyCane,
  Worm
};

//NeoPixelBus<NeoRgbFeature, Neo400KbpsMethod> strip(PixelCount, PixelPin);
// IMPORTANT: To reduce NeoPixel burnout risk, add 1000 uF capacitor across
// pixel power leads, add 300 - 500 Ohm resistor on first pixel's data input
// and minimize distance between Arduino and first pixel.  Avoid connecting
// on a live circuit...if you must, connect GND first.
#define PIN 21
volatile int stripLEDs = 600;
volatile int numberOfBands = 20;
volatile int brightnessFactor = 15;
int stripLEDsConfig = 600;
int numberOfBandsConfig = 20;
int brightnessFactorConfig = 15;

NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod> strip(stripLEDs, PIN);

const char* ssid     = "*******";
const char* password = "******";

WiFiServer server(23);
WiFiClient  client;
bool clientActive = false;
volatile bool rotateModeOn = false;
volatile long rotateFreqInSecs = 0;
volatile bool goNoStop = true;
volatile bool doApply = false;
Mode      mode = Mode::None;
Mode      modeConfig = Mode::Worm;



#define traceOut Serial

// for def all command processors
void ProcessHelp(int Argc, char* Args[]);
void ProcessStop(int Argc, char* Args[]);
void ProcessGo(int Argc, char* Args[]);
void ProcessRestart(int Argc, char* Args[]);
void ProcessSetStripSize(int Argc, char* Args[]);
void ProcessSetNumberOfBands(int Argc, char* Args[]);
void ProcessSetBrightness(int Argc, char* Args[]);
void ProcessApply(int Argc, char* Args[]);
void ProcessStatus(int Argc, char* Args[]);
void ProcessSetMode(int Argc, char* Args[]);
void ProcessSetRotateMode(int Argc, char* Args[]);



typedef void (*CommandProcessor)(int Argc, char* Args[]);
struct CmdDesc
{
  char*             CmdStr;
  CommandProcessor  Processor;
  char*             HelpStr;
};

CmdDesc   cmdDescs[] =
{
  {"?", ProcessHelp, "   - Get this display"},
  {"s", ProcessStop, "   - Stop the display rotation"},
  {"g", ProcessGo,   "   - Start the display rotation"},
  {"r", ProcessRestart, "   - Restart the controller"},
  {"setstripsize", ProcessSetStripSize, "   - setStripSize <number of leds>"},
  {"setbandcount", ProcessSetNumberOfBands, "   - setBandCount <number of bands>"},
  {"brightness", ProcessSetBrightness, "   - brightness <divisor>"},
  {"mode", ProcessSetMode, "   - mode <band | twinkle | worm>"},
  {"apply", ProcessApply, "   - apply the set config"},
  {"status", ProcessStatus, "   - status - display system status"},
  {"rotate", ProcessSetRotateMode, "   - rotate on <secs> | off"},
};
static const int numberOfCmds = sizeof(cmdDescs) / sizeof(CmdDesc);

String NextToken(String& In, int& StartAt)
{
  while (StartAt < In.length())
  {
    if (!isWhitespace(In[StartAt]))
      break;

    StartAt++;
  }

  int endAt = StartAt;
  while (endAt < In.length())
  {
    if (isWhitespace(In[endAt]))
      break;

    endAt++;
  }

  auto result = In.substring(StartAt, endAt);
  result.toLowerCase();
  StartAt = endAt + 1;
  return result;
}

void ProcessCommand(String& CmdStr)
{
  static const int MaxCmdSize = 128;
  static const int MaxArgs = 20;

  if (CmdStr.length() > MaxCmdSize)
  {
    client.printf("Error: command too long\n\r");
    return;
  }

  Serial.printf("Cmd: '%s' ", CmdStr.c_str());
  for (int i = 0; i < CmdStr.length(); i++)
  {
    Serial.printf("0x%02X ", CmdStr[i]);
  }

  Serial.printf("\n");

  char stringStorage[MaxCmdSize];
  char* freePtr = &stringStorage[0];
  char* args[MaxArgs];
  int argc = 0;

  int start = 0;
  while (start < CmdStr.length())
  {
    auto arg = NextToken(CmdStr, start);
    if (arg.length() > 0)
    {
      printf("Arg (%i): '%s'\n", start, arg.c_str());
      memcpy(freePtr, arg.c_str(), arg.length()+1);
      args[argc] = freePtr;
      freePtr += arg.length()+1;
      argc++;
    }
  }

  for (int ix = 0; ix < argc; ix++)
  {
    printf("Arg(%i) = '%s'\n", ix, args[ix]);
  }

  // Lookup and dispatch cmd
  if (argc > 0)
  {
    for (int ix = 0; ix < numberOfCmds; ix++)
    {
      if (strcmp(cmdDescs[ix].CmdStr, args[0]) == 0)
      {
        cmdDescs[ix].Processor(argc, args);
        return;
      }
    }
    client.printf("'%s' is not a valid command\n\r", args[0]);
  }
}

// Command processors
void ProcessHelp(int Argc, char* Args[])
{
  client.printf("Help:\n\r");
  for (int ix = 0; ix < numberOfCmds; ix++)
  {
    client.printf("   %s %s\n\r", cmdDescs[ix].CmdStr, cmdDescs[ix].HelpStr);
  }
}

void ProcessStop(int Argc, char* Args[])
{
  goNoStop = false;
  client.printf("\n\rStopping\n\r");
}

void ProcessGo(int Argc, char* Args[])
{
  goNoStop = true;
  client.printf("Starting\n\r");
}

void ProcessRestart(int Argc, char* Args[])
{
  client.printf("Restarting\n\r");
  client.flush();
  ESP.restart();
}

void ProcessSetStripSize(int Argc, char* Args[])
{
  if (Argc != 2)
  {
    client.printf("Syntax errror\n\r");
    return;
  }
  stripLEDsConfig = String(Args[1]).toInt();
}

void ProcessSetNumberOfBands(int Argc, char* Args[])
{
  if (Argc != 2)
  {
    client.printf("Syntax errror\n\r");
    return;
  }
  numberOfBandsConfig = String(Args[1]).toInt();
}

void ProcessSetBrightness(int Argc, char* Args[])
{
  if (Argc != 2)
  {
    client.printf("Syntax errror\n\r");
    return;
  }
  brightnessFactorConfig = String(Args[1]).toInt();
}

void ProcessApply(int Argc, char* Args[])
{
  doApply = true;
}

void ProcessStatus(int Argc, char* Args[])
{
  client.printf("Status:\n\r");
  client.printf("   Strip Size: Active: %u; Config: %u\n\r", stripLEDs, stripLEDsConfig);
  client.printf("   Number of bands: Active: %u; Config: %u\n\r", numberOfBands, numberOfBandsConfig);
  client.printf("   Brightness: Active: %u; Config: %u\n\r", brightnessFactor, brightnessFactorConfig);
  client.printf("   Mode: Active: %u; Config: %u\n\r", mode, modeConfig);
  if (rotateModeOn)
  {
    client.printf("   Rotate: 'on' %lu secs cycle duration\n\r", rotateFreqInSecs);
  }
  else
  {
    client.printf("   Rotate: 'off'\n\r");
  }
  client.printf("\n\r");
}

void ProcessSetMode(int Argc, char* Args[])
{
  if (Argc != 2)
  {
    client.printf("Syntax errror\n\r");
    return;
  }

  if (strcmp(Args[1], "band") == 0)
  {
    modeConfig = Mode::CandyCane;
  }
  else if (strcmp(Args[1], "twinkle") == 0)
  {
    modeConfig = Mode::Twinkle;
  }
  else if (strcmp(Args[1], "worm") == 0)
  {
    modeConfig = Mode::Worm;
  }
  else
  {
    client.printf("Syntax errror\n\r");
    return;
  }

  client.printf("mode staged - 'apply' when ready\n\r");
}

void ProcessSetRotateMode(int Argc, char* Args[])
{
  if (Argc < 2)
  {
    client.printf("Syntax errror\n\r");
    return;
  }

  if (strcmp(Args[1], "on") == 0)
  {
    rotateModeOn = false;
    if (Argc > 2)
    {
      rotateFreqInSecs = atoi(Args[2]);
    }
    rotateModeOn = true;
  }
  else  if (strcmp(Args[1], "off") == 0)
  {
    rotateModeOn = false;
  }
  else
  {
    client.printf("Syntax errror\n\r");
    return;
  }
}

// WiFi Task
void ForceWiFiConnect(void*)
{
    String cmd;

    Serial.printf("ForceWiFiConnect: active\n");
    while (true)
    {
        if (WiFi.status() != WL_CONNECTED)
        {
            digitalWrite(LED_BUILTIN, LOW);
            client.stop();
            WiFi.disconnect();
            Serial.print("Attempting to connect to SSID: ");
            traceOut.println(ssid);

            WiFi.begin(ssid, password);

            // wait 10 seconds for connection:
            delay(4000);
            if (WiFi.status() == WL_CONNECTED)
            {
                traceOut.print("IP Address: ");
                traceOut.println(WiFi.localIP());
                server.begin();
                clientActive = false;
                digitalWrite(LED_BUILTIN, HIGH);
                cmd = "";
            }
        }
        else
        {
            if (!client)
            {
                if (clientActive)
                {
                    clientActive = false;
                    traceOut.println("**Client dropped");
                    cmd = "";
                }
                client = server.available();
                if (client)
                {
                    traceOut.println("**Client connected: Before flush");
                    client.flush();
                    clientActive = true;
                    traceOut.println("**Client connected");
                    client.printf("**Welcome **\r\n");
                    cmd = "";
                }
            }
            else
            {
                if (client.available() > 0)
                {
                  char cmdChar = client.read();
                  if (cmdChar == '\n')
                  {
                    ProcessCommand(cmd);
                    cmd = "";
                  }
                  else
                  {
                    if (cmdChar != '\r')
                    {
                      cmd += cmdChar;
                    }
                  }
                }
            }
        }
        delay(100);
        yield();
    }
}

void InitStrip()
{
  stripLEDs = stripLEDsConfig;
  numberOfBands = numberOfBandsConfig;
  brightnessFactor = brightnessFactorConfig;
  mode = modeConfig;

  int colorBandWidth = stripLEDs / numberOfBands;

  strip.ClearTo(RgbColor(0, 0, 0));
  strip.Show(); // Initialize all pixels to 'off'

  if (mode == Mode::CandyCane)
  {
    for (int i = 0; i < stripLEDs; i += 2 * colorBandWidth)
    {
      for (int y = 0; y < colorBandWidth; y++)
      {
        if ((i+y) < stripLEDs)
        {
          // red band
          strip.SetPixelColor(i+y, RgbColor(0xFF / brightnessFactor, 0, 0));
        }
      }
      for (int y = colorBandWidth; y < (2*colorBandWidth); y++)
      {
        if ((i+y) < stripLEDs)
        {
          // white band
          strip.SetPixelColor(i+y, RgbColor(255/brightnessFactor, 255/brightnessFactor, 255/brightnessFactor));
        }
      }
    }
  }
  else if (mode == Mode::Worm)
  {
    for (int i = 0; i < colorBandWidth; i++)
    {
      uint8_t val = (uint8_t)(colorBandWidth * (1 - ((float)i / colorBandWidth)));
      //printf("ix = %u; val = %u\n", i, val);
      strip.SetPixelColor((colorBandWidth - i) -1, RgbColor(val, 0, 0));
    }
  }
  else if (mode == Mode::Twinkle)
  {
    for (int i =0; i < stripLEDs; i++)
    {
      if (!goNoStop || doApply)
        break;

      strip.SetPixelColor(i, RgbColor(random(1, 256) / random(1, 50), random(1, 256) / random(1, 50), random(1, 256) / random(1, 50)));
    }
  }
  strip.Show();
//  while (true) {}
}


void setup()
{
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
  Serial.begin(119200);
  while (!Serial) {}

  strip.Begin();
  strip.Show(); // Initialize all pixels to 'off'
  InitStrip();

  Serial.printf("Main TaskId: %08X; #Tasks: %u\n", (uint32_t)xTaskGetCurrentTaskHandle(), uxTaskGetNumberOfTasks());

  TaskHandle_t xHandle;
  if (xTaskCreate(ForceWiFiConnect, "WifiTask", 4096, nullptr, configMAX_PRIORITIES / 2, &xHandle) != pdPASS )
  {
    Serial.printf("WiFiTask could not be created\n");
  }
  Serial.printf("After WiFiTask created: Main TaskId: %08X; #Tasks: %u\n", (uint32_t)xTaskGetCurrentTaskHandle(), uxTaskGetNumberOfTasks());

}

void loop()
{
  if ((mode == Mode::CandyCane) || (mode == Mode::Worm))
  {
    if (goNoStop)
    {
      for (int i =0; i < stripLEDs; i++)
      {
        if (!goNoStop || doApply)
          break;

        strip.RotateRight(1);
        strip.Show();

        if (mode == Mode::Worm)
        {
          delay(0);
        }
      }
    }
  }
  else if (mode == Mode::Twinkle)
  {
    int skipSize = random(1, 20);
    for (int i =0; i < stripLEDs; i += skipSize)
    {
      if (!goNoStop || doApply)
        break;

      strip.SetPixelColor(i, RgbColor(random(1, 256) / random(1, 50), random(1, 256) / random(1, 50), random(1, 256) / random(1, 50)));
    }
    strip.Show();
    delay(random(100));
  }

  if (doApply)
  {
    doApply = false;
    InitStrip();
  }

  if (rotateModeOn)
  {
   static long      nextRotateTime = 0;

   if (nextRotateTime < millis())
    {
      static Mode  modes[] = {Mode::Worm, Mode::Twinkle, Mode::CandyCane};
      static const int nbrOfModes = sizeof(modes) / sizeof(Mode);
      static uint8_t ix = 0;

      modeConfig = modes[ix];
      InitStrip();
      ix++;
      if (ix == nbrOfModes)
      {
        ix = 0;
      }
      printf("{%lu}: New Mode = %u\n", millis(), mode);
      nextRotateTime = millis() + (rotateFreqInSecs * 1000);    // switch again in 10secs
    }
  }
}
