#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include <ESP32Servo.h>

// PINS
#define JOYSTICK_SW_PIN 19
#define IR_RECEIVE_PIN  15
#define LED_PIN         17
#define LED_COUNT       12
#define SERVO_PIN       23

#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <WiFiClientSecure.h>
#include <WiFiClient.h>

#include <IRremote.hpp>

#include "SPIFFS.h"
#include "AudioFileSourceSPIFFS.h"
#include "AudioFileSourceID3.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"

// WiFi credentials
char    ssid[]                   = "Your Network ID Here";
char    pass[]                   = "Your Network Password Here"; // leave blank if open network

// solution to current problem in string form
String  current_problem_solution = "";

// IR reciever variables
String  msg                      = "";
long    previos_millis           = millis();
long    press_delay              = 300;
char    prev_char                = 'a'; // a is a placeholder for a character than cannot be achieved using buttons.

// NeoPixel variables
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
int     lit_led_count            = 0;
int     win_var                  = 0;
int     win_pixel_hue            = 0;
int     number_of_win_animations = 0;
int     brightness               = 100;
int     red_flash_count          = 0;
long    win_millis               = millis();
long    red_flash_millis         = millis();
long    win_delay                = 50;
long    red_flash_duration       = 130;
boolean is_winning               = false;

// MP3 variables
AudioGeneratorMP3     *mp3;
AudioFileSourceSPIFFS *file;
AudioOutputI2S        *out;
AudioFileSourceID3    *id3;
String                filepath;

// Servo variables
int     balance                  = 180;
int     degrees_per_win          = 10;
Servo   myservo;

// Joystick switch variables
long    sw_millis                = millis();
long    sw_delay                 = 2000;

// metadata callback for MP3
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string){
  (void)cbData;
  Serial.printf("ID3 callback for: %s = '", type);

  if ( isUnicode ) {
    string += 2;
  }
  
  while ( *string ) {
    char a = *(string++);
    if ( isUnicode ) {
      string++;
    }
    Serial.printf("%c", a);
  }
  Serial.printf("'\n");
  Serial.flush();
}

// handling answer submission from IR remote.
void handle_submission(){
  Serial.println(msg);
  Serial.println(current_problem_solution);
  if ( msg == current_problem_solution && msg != "") {
    correct_answer();
  } else {
    wrong_answer();
  }
  msg = "";
}

// correct asnwer
void correct_answer() {
  Serial.println("Correct answer");
  
  current_problem_solution = get_problem();
  
  lit_led_count += 4;
  update_strip();
  
  if ( lit_led_count >= 12 ) {
    
    filepath = String("/win.mp3");
    file = new AudioFileSourceSPIFFS(filepath.c_str());
    id3 = new AudioFileSourceID3(file);
    mp3->begin(id3, out);
    
    is_winning = true;

    balance = 180 - update_balance(degrees_per_win);
    myservo.write(balance);
    
    lit_led_count = 0;
  } else {
    filepath = String("/correct.mp3");
    file = new AudioFileSourceSPIFFS(filepath.c_str());
    id3 = new AudioFileSourceID3(file);
    mp3->begin(id3, out);
  }
}

// wrong asnwer
void wrong_answer() {
  Serial.println("Wrong answer");
  
  filepath = String("/wrong.mp3");
  file = new AudioFileSourceSPIFFS(filepath.c_str());
  id3 = new AudioFileSourceID3(file);
  mp3->begin(id3, out);

  red_flash_count = 3;
}

// update LED strip
void update_strip() {
  if ( lit_led_count > 12 ) {
    lit_led_count = 12;
  }
  for ( int i = 0 ; i < lit_led_count ; i++ ) {
    strip.setPixelColor(i, strip.Color(0, 255, 0));
  }
  strip.show();
}

// LED strip win animation
void play_win_animation() {
  if ( millis() - win_millis > win_delay ) {
    if ( win_var >= 3 ) {
      number_of_win_animations++;
      win_var = 0;
    } else {
      strip.clear();
      for (int c = win_var; c < strip.numPixels(); c += 3) {
        int      hue   = win_pixel_hue + c * 65536L / strip.numPixels();
        uint32_t color = strip.gamma32(strip.ColorHSV(hue));
        strip.setPixelColor(c, color);
      }
      strip.show();
      win_pixel_hue += 65536 / 90;
      win_var++;
      win_millis = millis(); 
    }
  }
}

// Set all LED's to red
void flash_strip_red() {
  for ( int i = 0 ; i < LED_COUNT ; i++) {
    strip.setPixelColor(i, strip.Color(255, 0, 0));
  }
  strip.show();
}

// handle button press ( new game )
void handle_button_press() {
  current_problem_solution = get_problem();
  lit_led_count = 0;
  strip.clear();
  strip.show();
  update_strip();
}

// gets problem from make
String get_problem() {
  WiFiClientSecure *client = new WiFiClientSecure;
    if ( client ) {
      client -> setInsecure(); {
        HTTPClient https;
        Serial.print("[HTTPS] begin...\n");
        // Change Your URL in the line below to the Get Problem Scenarios webhook.
        if ( https.begin(*client, "Your URL") ) {
          Serial.print("[HTTPS] GET...\n");
          int httpCode = https.GET();
          if ( httpCode > 0) {
            Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
            if ( httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY ) {
              String payload = https.getString();
              Serial.println(payload);
              return payload;
            }
          } else {
            Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
            return "";
          }
          https.end();
        } else {
          Serial.printf("[HTTPS] Unable to connect\n");
          return "";
        }
      }
      delete client;
    } else {
      Serial.println("Unable to create client");
      return "";
    }
    Serial.println();
}

// update balance
int update_balance(int data) {
  WiFiClientSecure *client = new WiFiClientSecure;
    if ( client ) {
      client -> setInsecure(); {
        HTTPClient https;
        Serial.print("[HTTPS] begin...\n");
        // Change Your URL in the line below to the Update Balance Scenarios webhook.
        if ( https.begin(*client, String("Your URL?inc=") + data ) ) {
          Serial.print("[HTTPS] GET...\n");
          int httpCode = https.GET();
          if ( httpCode > 0) {
            Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
            if ( httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY ) {
              String payload = https.getString();
              Serial.println(payload);
              return payload.toInt();
            }
          } else {
            Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
            return 0;
          }
          https.end();
        } else {
          Serial.printf("[HTTPS] Unable to connect\n");
          return 0;
        }
      }
      delete client;
    } else {
      Serial.println("Unable to create client");
      return 0;
    }
    Serial.println();
}

// Servo Motor helper function to draw lines

void servo_helper() {
  int deg = 0;
  while ( deg <= 180 ) {
    myservo.write(deg);
    deg += degrees_per_win;
    delay(3000);
  }
}

void setup() {
  // setup Serial
  Serial.begin(115200);

  // setup Wifi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, pass);
  Serial.print("Waiting for WiFi to connect...");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(100);
  }
  Serial.println(" connected");
  
  // setup IR Reciever
  Serial.println(F("START " __FILE__ " from " __DATE__ "\r\nUsing library version " VERSION_IRREMOTE));
  IrReceiver.begin(IR_RECEIVE_PIN, ENABLE_LED_FEEDBACK);
  Serial.print(F("Ready to receive IR signals of protocols: "));
  printActiveIRProtocols(&Serial);
  Serial.printf("at pin %d\n", IR_RECEIVE_PIN);

  // setup joystick switch
  pinMode(JOYSTICK_SW_PIN, INPUT_PULLUP);

  // setup LED strip
  strip.begin();
  strip.show();
  strip.setBrightness(brightness);

  // setup MP3
  SPIFFS.begin();
  audioLogger = &Serial;
  file = new AudioFileSourceSPIFFS("/correct.mp3");
  id3 = new AudioFileSourceID3(file);
  id3->RegisterMetadataCB(MDCallback, (void*)"ID3TAG");
  out = new AudioOutputI2S();
  mp3 = new AudioGeneratorMP3();
  mp3->begin(id3, out);

  // setup Servo
  ESP32PWM::allocateTimer(0);
  ESP32PWM::allocateTimer(1);
  ESP32PWM::allocateTimer(2);
  ESP32PWM::allocateTimer(3);
  myservo.setPeriodHertz(50);    // standard 50 hz servo
  myservo.attach(SERVO_PIN, 500, 2500);
  // the commented out function below simply itterates through the servo's range
  // given the degrees_per_win variable, this is useful to draw the lines on the box.
  // servo_helper();
  balance = 180 - update_balance(0);
  myservo.write(balance);
}

void loop() {
  // MP3
  if ( mp3->isRunning() ) {
    if ( !mp3->loop() ) mp3->stop();
  }

  // Play winning animation concurently
  if ( is_winning ) {
    strip.setBrightness(255);
    if ( number_of_win_animations < 20 ){
      play_win_animation();
    } else {
      is_winning = false;
      number_of_win_animations = 0;
      strip.clear();
      strip.show();
      strip.setBrightness(brightness);
    }
  }

  // Play red flash on incorrect answer
  if ( red_flash_count > 0 && ( millis() - red_flash_millis > red_flash_duration ) ) {
    if ( red_flash_count % 2 == 1 ) {
      flash_strip_red(); 
    } else {
      strip.clear();
      strip.show();
    }
    red_flash_count--;
    red_flash_millis = millis();
  } else if ( millis() - red_flash_millis > red_flash_duration ) {
    strip.clear();
    update_strip();
  }
  
  // Catch button press
  int sw = digitalRead(JOYSTICK_SW_PIN);
  if ( sw == 0 && ( millis() - sw_millis > sw_delay ) ) {
      handle_button_press();
      sw_millis = millis();
  }
  
  // IR Reciever
  if ( IrReceiver.decode() ) {
    // Debuging prints for IR reciever
    IrReceiver.printIRResultShort(&Serial);
    IrReceiver.printIRSendUsage(&Serial);
    Serial.println();

    // Cases for IR commands
    char number_to_append;
    switch (IrReceiver.decodedIRData.command) {
          case 0x45: // Corresponds to '1'
            number_to_append = '1';
            break;
          case 0x46: // Corresponds to '2'
            number_to_append = '2';
            break;
          case 0x47: // Corresponds to '3'
            number_to_append = '3';
            break;
          case 0x44: // Corresponds to '4'
            number_to_append = '4';
            break;
          case 0x40: // Corresponds to '5'
            number_to_append = '5';
            break;
          case 0x43: // Corresponds to '6'
            number_to_append = '6';
            break;
          case 0x07: // Corresponds to '7'
            number_to_append = '7';
            break;
          case 0x15: // Corresponds to '8'
            number_to_append = '8';
            break;
          case 0x09: // Corresponds to '9'
            number_to_append = '9';
            break;
          case 0x19: // Corresponds to '0'
            number_to_append = '0';
            break;
          case 0x1C: // Corresponds to 'Ok'
            number_to_append = 'a';
            handle_submission();
            break;
          default:
            number_to_append = 'a';
            break;
        }
        // after trying a bunch of methods, a delay between presses only if previos button differs
        // seems to be quite smooth.
        if ( millis() - previos_millis > press_delay || number_to_append != prev_char ) {
          if ( number_to_append != 'a' ) {
            msg += number_to_append; 
          }
          prev_char = number_to_append;
          previos_millis = millis();
        }
    IrReceiver.resume(); // Enable receiving of the next value 
  }
}
