#include <WiFi.h>
#include <PubSubClient.h>
#include <ESPmDNS.h>

//#define DEBUG 1

//OTA library
#include <WiFiUdp.h>
#include <ArduinoOTA.h>

// Convert to HSL to RBG color
#include <HSBColor.h>

// Audio frequency equlizer
#include <arduinoFFT.h>

// Equlizer sampling

#define SAMPLES         1024          // Must be a power of 2
#define SAMPLING_FREQ   40000         // Hz, must be 40000 or less due to ADC conversion time. Determines maximum frequency that can be analysed by the FFT Fmax=sampleF/2.
#define AMPLITUDE       1000          // Depending on your audio source level, you may need to alter this value. Can be used as a 'sensitivity' control.
#define AUDIO_IN_PIN    35           // Signal in on this pin
#define LED_PIN         4             // LED strip data
#define LED_COUNT       150             // LED Count
#define BTN_PIN         3             // Connect a push button to this pin to change patterns
#define LONG_PRESS_MS   200           // Number of ms to count as a long press
#define COLOR_ORDER     GRB           // If colours look wrong, play with this
#define CHIPSET         WS2812B       // LED strip type
#define MAX_MILLIAMPS   2000          // Careful with the amount of power here if running off USB port
const int BRIGHTNESS_SETTINGS[3] = {5, 70, 200};  // 3 Integer array for 3 brightness settings (based on pressing+holding BTN_PIN)
#define LED_VOLTS       5             // Usually 5 or 12
#define NUM_BANDS       16            // To change this, you will need to change the bunch of if statements describing the mapping from bins to bands
#define NOISE           500           // Used as a crude noise filter, values below this are ignored
const uint8_t kMatrixWidth = 16;                          // Matrix width
const uint8_t kMatrixHeight = 150;                         // Matrix height
#define NUM_LEDS       (kMatrixWidth * kMatrixHeight)     // Total number of LEDs
#define BAR_WIDTH      (kMatrixWidth  / (NUM_BANDS - 1))  // If width >= 8 light 1 LED width per bar, >= 16 light 2 LEDs width bar etc
#define TOP            (kMatrixHeight - 0)                // Don't allow the bars to go offscreen
#define SERPENTINE     true                               // Set to false if you're LEDS are connected end to end, true if serpentine

// Sampling and FFT stuff
unsigned int sampling_period_us;
byte peak[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};              // The length of these arrays must be >= NUM_BANDS
int oldBarHeights[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
int bandValues[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
double vReal[SAMPLES];
double vImag[SAMPLES];
unsigned long newTime;
arduinoFFT FFT = arduinoFFT(vReal, vImag, SAMPLES, SAMPLING_FREQ);

// Light Modes
bool light_mode=true;
bool music_mode=false;
bool rainbow_mode=false;

//Pixel color 
long firstPixelHue = 1;

typedef struct lightState{
  double hue;
  double saturation;
  double brightness;
};
lightState nextState;

// LED strip library
#include <Adafruit_NeoPixel.h>

// Which pin on the Arduino is connected to the NeoPixels?
// On a Trinket or Gemma we suggest changing this to 1:
#define LED_PIN    15

// How many NeoPixels are attached to the Arduino?
#define LED_COUNT 150
// Declare our NeoPixel strip object:
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

Adafruit_NeoPixel stripArray[] = {
  Adafruit_NeoPixel(LED_COUNT, 15, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 2, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 4, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 5, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 18, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 19, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 21, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 22, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 23, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 13, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 12, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 14, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 27, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 26, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 25, NEO_GRB + NEO_KHZ800),
  Adafruit_NeoPixel(LED_COUNT, 33, NEO_GRB + NEO_KHZ800),
};


// Update these with values suitable for your network.
 char* ssid = "N/A5";
 char* password = "@!123123!@";
 char* mqtt_server = "192.168.23.53";
#define mqtt_port 8883
#define MQTT_USER "areezo"
#define MQTT_PASSWORD "areezoesp2866"
#define MQTT_SERIAL_PUBLISH_CH "/icircuit/ESP32/serialdata/tx"
#define MQTT_SERIAL_RECEIVER_CH "/icircuit/ESP32/serialdata/rx"
const char *heartbeat_TOPIC = "heartbeat/";

byte MAC_array[6];
String node_mac ;
String var_topic ;
const byte ledPin = 2; 
String rec_message ;
byte heartbeat=0;

bool newstate=false;

WiFiClient wifiClient;

PubSubClient client(wifiClient);

#define INTERVAL_Heartbeat 13000
unsigned long time_Heartbeat = 0;


void setup_wifi() {
    delay(10);
    // We start by connecting to a WiFi network
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
    }
    randomSeed(micros());
    Serial.println("");
    Serial.println("WiFi connected");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());


}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Create a random client ID
    String clientId = "ESP32Client-";
    clientId += String(random(0xffff), HEX);
    // Attempt to connect
    if (client.connect(clientId.c_str(),MQTT_USER,MQTT_PASSWORD)) {
      Serial.println("connected");
      //Once connected, publish an announcement...
      client.publish("STATE_TOPIC", "hello world| i'm esp32");
      // ... and resubscribe
      client.subscribe(var_topic.c_str());
      client.subscribe(heartbeat_TOPIC);
      Serial.println(var_topic);
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void callback(char* topic, byte *payload, unsigned int length) {
#ifdef DEBUG
    Serial.println("-------new message from broker-----");
    Serial.print("channel:");
    Serial.println(topic);
    Serial.print("data:");  
    Serial.write(payload, length);
#endif
    char newPayload[40];
    memcpy(newPayload, payload, length);
    newPayload[length] = '\0';
    
    if (payload[0] == 'h'){
    nextState.hue = atof(&newPayload[1]);
    nextState.saturation = atof(&newPayload[7]);
    nextState.brightness = atof(&newPayload[13]);
#ifdef DEBUG
     Serial.println(nextState.hue);
     Serial.println(nextState.saturation);
      Serial.println(nextState.brightness);
#endif
      newstate=true;
    }

    
    for (int i=0;i<length;i++) {
      char receivedChar = (char)payload[i];
      rec_message+=receivedChar;
    }
    if (rec_message == "0"  )
    {
        //digitalWrite(ledPin, LOW);
        light_mode = true;
        music_mode = false;
        rainbow_mode = false;
    }
    else if (rec_message=="1")
    {
        //digitalWrite(ledPin, HIGH);
        light_mode = true;
        music_mode = false;
        rainbow_mode = false;
    }
    else if (rec_message=="2")
    {
        light_mode = false;
        music_mode = true;
        rainbow_mode = false;
    }
    else if (rec_message=="3")
    {
        light_mode = false;
        music_mode = false;
        rainbow_mode = true;
    }
    else if (rec_message=="Ack")
    {
      
        heartbeat=0;
    }
#ifdef DEBUG
    Serial.println();
#endif
    rec_message="";
}

void setup() {
// Start OTA setup
  
  Serial.begin(115200);
  Serial.println("Booting");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }

  // Port defaults to 3232
  // ArduinoOTA.setPort(3232);

  // Hostname defaults to esp3232-[MAC]
  // ArduinoOTA.setHostname("myesp32");

  // No authentication by default
  // ArduinoOTA.setPassword("admin");

  // Password can be set with it's md5 value as well
  // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
  // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");

  ArduinoOTA
    .onStart([]() {
      String type;
      if (ArduinoOTA.getCommand() == U_FLASH)
        type = "sketch";
      else // U_SPIFFS
        type = "filesystem";

      // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
      Serial.println("Start updating " + type);
    })
    .onEnd([]() {
      Serial.println("\nEnd");
    })
    .onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    })
    .onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
      else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
      else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
      else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
      else if (error == OTA_END_ERROR) Serial.println("End Failed");
    });

  ArduinoOTA.begin();
// End of OTA

  
  
  Serial.setTimeout(500);// Set time out for 
  setup_wifi();
  
  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
  
  client.setServer(mqtt_server, mqtt_port);
  client.setCallback(callback);
  pinMode(ledPin, OUTPUT);


  
  WiFi.macAddress(MAC_array);
  node_mac += String(MAC_array[0], HEX);
  node_mac += ":" ;
  node_mac += String(MAC_array[1], HEX);
  node_mac += ":" ;
  node_mac += String(MAC_array[2], HEX);
  node_mac += ":" ;
  node_mac += String(MAC_array[3], HEX);
  node_mac += ":" ;
  node_mac += String(MAC_array[4], HEX);
  node_mac += ":" ;
  node_mac += String(MAC_array[5], HEX);
  var_topic="/" + node_mac+ "/";
  
  reconnect();

  // FFT Samplinling
  sampling_period_us = round(1000000 * (1.0 / SAMPLING_FREQ));

  // strip LED setup
  strip.begin();           // INITIALIZE NeoPixel strip object (REQUIRED)
  strip.show();            // Turn OFF all pixels ASAP
  strip.setBrightness(255); // Set BRIGHTNESS to about 1/5 (max = 255)
}

// Sampling Audio
void Audio_sample()
{
     // Reset bandValues[]
  for (int i = 0; i<NUM_BANDS; i++){
    bandValues[i] = 0;
  }

  // Sample the audio pin
  for (int i = 0; i < SAMPLES; i++) {
    newTime = micros();
    vReal[i] = analogRead(AUDIO_IN_PIN); // A conversion takes about 9.7uS on an ESP32
    vImag[i] = 0;
    while ((micros() - newTime) < sampling_period_us) { /* chill */ }
  }

  // Compute FFT
  FFT.DCRemoval();
  FFT.Windowing(FFT_WIN_TYP_HAMMING, FFT_FORWARD);
  FFT.Compute(FFT_FORWARD);
  FFT.ComplexToMagnitude();

  // Analyse FFT results
  for (int i = 2; i < (SAMPLES/2); i++){       // Don't use sample 0 and only first SAMPLES/2 are usable. Each array element represents a frequency bin and its value the amplitude.
    if (vReal[i] > NOISE) {                    // Add a crude noise filter

    /*8 bands, 12kHz top band
      if (i<=3 )           bandValues[0]  += (int)vReal[i];
      if (i>3   && i<=6  ) bandValues[1]  += (int)vReal[i];
      if (i>6   && i<=13 ) bandValues[2]  += (int)vReal[i];
      if (i>13  && i<=27 ) bandValues[3]  += (int)vReal[i];
      if (i>27  && i<=55 ) bandValues[4]  += (int)vReal[i];
      if (i>55  && i<=112) bandValues[5]  += (int)vReal[i];
      if (i>112 && i<=229) bandValues[6]  += (int)vReal[i];
      if (i>229          ) bandValues[7]  += (int)vReal[i];*/

    //16 bands, 12kHz top band
      if (i<=2 )           bandValues[0]  += (int)vReal[i];
      if (i>2   && i<=3  ) bandValues[1]  += (int)vReal[i];
      if (i>3   && i<=5  ) bandValues[2]  += (int)vReal[i];
      if (i>5   && i<=7  ) bandValues[3]  += (int)vReal[i];
      if (i>7   && i<=9  ) bandValues[4]  += (int)vReal[i];
      if (i>9   && i<=13 ) bandValues[5]  += (int)vReal[i];
      if (i>13  && i<=18 ) bandValues[6]  += (int)vReal[i];
      if (i>18  && i<=25 ) bandValues[7]  += (int)vReal[i];
      if (i>25  && i<=36 ) bandValues[8]  += (int)vReal[i];
      if (i>36  && i<=50 ) bandValues[9]  += (int)vReal[i];
      if (i>50  && i<=69 ) bandValues[10] += (int)vReal[i];
      if (i>69  && i<=97 ) bandValues[11] += (int)vReal[i];
      if (i>97  && i<=135) bandValues[12] += (int)vReal[i];
      if (i>135 && i<=189) bandValues[13] += (int)vReal[i];
      if (i>189 && i<=264) bandValues[14] += (int)vReal[i];
      if (i>264          ) bandValues[15] += (int)vReal[i];
    }
  }
}

// Process Audio data to LED
void process_audio(uint32_t color)
{
     // Process the FFT data into bar heights
  for (byte band = 0; band < NUM_BANDS; band++) {

    // Scale the bars for the display
    int barHeight = bandValues[band] / AMPLITUDE;
    if (barHeight > TOP) barHeight = TOP;

    // Small amount of averaging between frames
    barHeight = ((oldBarHeights[band] * 1) + barHeight) / 2;

//    if (band==1)
//    {
    for(int i=0; i< barHeight; i++) { // For each pixel in strip...
        stripArray[band].setPixelColor(i, color);         //  Set pixel's color (in RAM)
      }
    for(int i=barHeight; i<stripArray[band].numPixels(); i++) { // For each pixel in strip...
        stripArray[band].setPixelColor(i, strip.Color(0,   0,   0));         //  Set pixel's color (in RAM)
      }

#ifdef DEBUG
    if (band==3)
    {  
      Serial.println(barHeight);
    }
#endif

    // Save oldBarHeights for averaging later    
    oldBarHeights[band] = barHeight;

    stripArray[band].show();
  }
}


void publishSerialData(char *serialData){
  if (!client.connected()) {
    reconnect();
  }
  client.publish(MQTT_SERIAL_PUBLISH_CH, serialData);
}

void colorWipe(uint32_t color, int wait) {
 for(int s=0; s<sizeof(stripArray)/24; s++) { // 24 is the size of the strip class 
  for(int i=0; i<strip.numPixels(); i++) { // For each pixel in strip...
    
    stripArray[s].setPixelColor(i, color); 
    
    //strip.setPixelColor(i, color);         //  Set pixel's color (in RAM)
    //strip.show();                          //  Update strip to match
    //delay(wait);                           //  Pause for a moment
  }
  stripArray[s].show();
  //Serial.print(sizeof(stripArray));
 }
}

void Rainbow_led() {
 for(int s=0; s<sizeof(stripArray)/24; s++) { // 24 is the size of the strip class 
  for(int i=0; i<strip.numPixels(); i++) { // For each pixel in strip...
    
      int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels());
      stripArray[s].setPixelColor(i, strip.gamma32(strip.ColorHSV(firstPixelHue)));
    
    //strip.setPixelColor(i, color);         //  Set pixel's color (in RAM)
    //strip.show();                          //  Update strip to match
    //delay(wait);                           //  Pause for a moment
  }
  stripArray[s].show();
  //Serial.print(sizeof(stripArray));
 }
 
 if (firstPixelHue >= 65536) {
  firstPixelHue=1;
 }
 
 //firstPixelHue+=1;

}

int o=0;
void oneled(uint32_t color, int wait) {
 for(int s=0; s<sizeof(stripArray)/24; s++) { // 24 is the size of the strip class 
  //for(int o=0; o<strip.numPixels(); o++) {
//  for(int i=0; i<strip.numPixels(); i++) { // For each pixel in strip...
//    if (i==o)
//      stripArray[s].setPixelColor(i, color); 
//    else
//      stripArray[s].setPixelColor(i, strip.Color(0,   0,  0)); 
//    
//    //strip.setPixelColor(i, color);         //  Set pixel's color (in RAM)
//    //strip.show();                          //  Update strip to match
//    //delay(wait);                           //  Pause for a moment
//    
//  }
if (o==0){
  stripArray[s].setPixelColor(0, color); 
  stripArray[s].setPixelColor(1, color); 
  stripArray[s].setPixelColor(2, color); 
  stripArray[s].setPixelColor(3, strip.Color(0,   0,  0)); 
  stripArray[s].setPixelColor(4, strip.Color(0,   0,  0)); 
  stripArray[s].setPixelColor(5, strip.Color(0,   0,  0));   
}
else{
  stripArray[s].setPixelColor(0, strip.Color(0,   0,  0)); 
  stripArray[s].setPixelColor(1, strip.Color(0,   0,  0)); 
  stripArray[s].setPixelColor(2, strip.Color(0,   0,  0));   
  stripArray[s].setPixelColor(3, color); 
  stripArray[s].setPixelColor(4, color); 
  stripArray[s].setPixelColor(5, color); 
}
  stripArray[s].show();
 // }
  //Serial.print(sizeof(stripArray));
 }

 o++;
 if (o==2)//strip.numPixels())
  o=0;
}

void loop() {
  ArduinoOTA.handle();
  
   client.loop();
//   if (Serial.available() > 0) {
//     char mun[501];
//     memset(mun,0, 501);
//     Serial.readBytesUntil( '\n',mun,500);
//     publishSerialData(mun);
//   }

// convert HSL to RBG and set a color
    int newRGB[3];
    H2R_HSBtoRGBfloat(nextState.hue, nextState.saturation, nextState.brightness, newRGB);
    
    if (light_mode){
        if (newstate)
        {
        
        colorWipe(strip.Color(newRGB[0],   newRGB[1],  newRGB[2]), 0); // Red
       // colorWipe(strip.Color(255,   255,  255), 0);
        newstate=false;
        }
    }
    else if (music_mode){
        Audio_sample();
        process_audio(strip.Color(newRGB[0],   newRGB[1],  newRGB[2]));
    }
    else if (rainbow_mode){
      Rainbow_led();
      if(millis() > time_Heartbeat + INTERVAL_Heartbeat / 10){
          //firstPixelHue++;
          firstPixelHue+=10;
      }
    }
  // oneled(strip.Color(0,   0,  255), 0); // Red
   
//client.publish(STATE_TOPIC,"aa");
  
   
   if(millis() > time_Heartbeat + INTERVAL_Heartbeat){
        time_Heartbeat = millis();
        
        Serial.println("I'm message number one!");
 
    
    if (heartbeat==1)
    {
      ESP.restart();
    }
    
    client.publish(heartbeat_TOPIC,"HeartBeat");
    heartbeat=1;
    }
    
   
 }
 
