
//https://github.com/esp8266/Arduino
//https://arduino.esp8266.com/stable/package_esp8266com_index.json

//https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html
#include "LittleFS.h"     //https://github.com/esp8266/arduino-esp8266fs-plugin/releases  

#include <Wire.h>
#include <DNSServer.h>            //Local DNS Server used for redirecting all requests to the configuration portal
#include <WiFiManager.h>          //https://github.com/tzapu/WiFiManager/commit/0780e4fe029136773df279a600353c55e76862e8
#include <ESP8266WiFi.h>          //ESP8266 Core WiFi Library (you most likely already have this in your sketch)
#include <ArduinoJson.h>
#include <ESP8266WebServer.h>     //Local WebServer used to serve the configuration portal
#include <LiquidCrystal_I2C.h>    //Modified F Malpartida's NewLiquidCrystal library https://github.com/swordfish6975/New-LiquidCrystal
#include <ESP8266HTTPClient.h>

const char deviceName[] = "afkdata";
const bool debug = false;
//https://arduinojson.org/v6/assistant/
const int SettingsCapacity = JSON_OBJECT_SIZE(2) + 310;
const int PayloadCapacity = JSON_ARRAY_SIZE(31) + 2 * JSON_OBJECT_SIZE(2) + 640;

String Line1 = "";
String Line2 = "";

int SecondLineC = 0;
int SecondLineIndex = 0;
int SecondLineLength = 0;
bool SecondLineEnable = false;
String SecondLine[30]; //max second line cryptos

String URL = "";
String Params = "";
String SettingsJson = "";


int poll = 120;
int secondsC = 301;  //bigger than poll for inital load (web request on boot)
int secondsS = 0;

unsigned long interval = 1000; // the time we need to wait
unsigned long previousMillis = 0; // millis() returns an unsigned long.

LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

ESP8266WebServer server(80);

WiFiManager wifiManager;

const int interruptPin = 15;
volatile bool resetRequired = false;

String JsonToParams()
{
  DynamicJsonDocument doc(SettingsCapacity);
  deserializeJson(doc, SettingsJson);
  JsonObject root = doc.as<JsonObject>();
  String output = "";

  int i = 1;

  for (JsonPair kv : root) {

    output +=  kv.key().c_str();
    output += "=";
    output +=  kv.value().as<char*>();
    if (i < root.size())
      output += "&";

    i++;
  }
  return output;
}

ICACHE_RAM_ATTR void handleInterrupt() {
  resetRequired = true;
}

void configModeCallback (WiFiManager *myWiFiManager) {
  Line1 = padRight("   SETUP");
  Line2 = padRight("         MODE");
  PaintScreen();
  if (debug)
  {
    Serial.println("Entered config mode");
    Serial.println(WiFi.softAPIP());
    Serial.println(myWiFiManager->getConfigPortalSSID());
  }
}

void saveWifiCallback(){
  Serial.println("[CALLBACK] saveCallback fired");
  if (WiFi.status() != WL_CONNECTED)
  {
    if (debug)
    {
      Serial.println("Connection Fail");
    }
    Line1 = "Connection Fail";
    Line2 = "               ";
    PaintScreen();
    delay(5000);
    wifiManager.resetSettings();
    delay(1000);
    Restart();
  }
}

void saveConfigCallback () {
  if (debug)
  {
    Serial.println("Settings Saved");
    Serial.println(String(WiFi.status()));
  }
}

void setup() {
  WiFi.mode(WIFI_STA);
  
  if (debug)
  {
    Serial.begin(9600);
  }

  delay(1000);

  if (!LittleFS.begin()) {
    if (debug)
    {
      Serial.println("An Error has occurred while mounting LittleFS");
    }
    return;
  }

  pinMode(interruptPin, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(interruptPin), handleInterrupt, RISING);

  SettingsJson = ReadFile("settings");
  Params = JsonToParams();
  URL = "http://cryptofolio.info/api/screen?"  + Params; //do this once during setup to not stress eprom.

  Wire.begin(2, 0);

  Wire.beginTransmission(0x27);
  if (Wire.endTransmission() == 0)
  {
    lcd.changeAddress(0x27);
    if (debug)
    {
      Serial.println("backpack detected: 0x27");
    }
  }

  Wire.beginTransmission(0x3F);
  if (Wire.endTransmission() == 0)
  {
    lcd.changeAddress(0x3F);
    if (debug)
    {
      Serial.println("backpack detected: 0x3F");
    }
  }

  lcd.begin(16, 2);              // initialize the lcd

  Line1 = padRight("    WELCOME");
  Line2 = padRight("    AFKDATA");
  PaintScreen();

  const char * menu[] = {"wifi","info"};
  wifiManager.setMenu(menu, 2);
  wifiManager.setShowInfoUpdate(false);
  wifiManager.setShowInfoErase(false);

  wifiManager.setCountry("US");
  wifiManager.setDarkMode(true);
  wifiManager.setAPCallback(configModeCallback);

  wifiManager.setBreakAfterConfig(true);
  wifiManager.setSaveConfigCallback(saveWifiCallback);
  
  wifiManager.autoConnect(deviceName); 

  Line1 = padRight("Connected");
  Line2 = padRight(WiFi.localIP().toString());
  if (debug)
  {
    Serial.println("IP Address: " + Line2);
    WiFi.printDiag(Serial);
  }
  PaintScreen();
  delay(7000);

  Line1 = padRight("  INITIAL");
  Line2 = padRight("         LOAD");
  PaintScreen();

  server.on("/params", []() {
    server.send(200, "application/json", SettingsJson);
  });

  server.on("/submit", handleSubmit);

  server.onNotFound([]() {
    if (!handleFileRead(server.uri()))
      handleNotFound(); // otherwise, respond with a 404 (Not Found) error
  });

  server.begin();

}


void handleSubmit() {
  String output = "";
  StaticJsonDocument<SettingsCapacity>doc;

  for (int i = 0; i < server.args(); i++) {
    if (server.argName(i) != "plain")
    {
      doc[server.argName(i)] = server.arg(i);
    }
    if (debug)
    {
      String message = "";
      message += "Arg Number: " + String(i) + " ";
      message += "Arg Name: " + server.argName(i) + " ";
      message += "Arg Value: " + server.arg(i) + " ";;
      Serial.println("message: " + message);
    }
  }

  Serial.println("JsonOutput" + output);

  serializeJsonPretty(doc, output);

  WriteFile("settings", output);

  server.send(200,  "text/html", "saved - will reboot now");

  Restart();
}


void loop() {

  unsigned long currentMillis = millis();
  if ((unsigned long)(currentMillis - previousMillis) >= interval)
  {

    if (resetRequired)
    {
      WiFi.disconnect(true);   // still not erasing the ssid/pw. Will happily reconnect on next start
      WiFi.begin("", "");      // adding this effectively seems to erase the previous stored SSID/PW
      WriteFile("settings", ReadFile("default_settings"));
      Restart();
    }

    if (secondsC > poll)
    {
      secondsC = 0;
      AddStars();
      if (debug)
      {
        Serial.println("Poll Triggreded");
        Serial.println("secondsC: " + String(secondsC));
        Serial.println("poll: " + String(poll));
        Serial.println("Stars Added");
      }

      HTTPClient http;
      http.setTimeout(30000);

      http.begin(URL); //HTTP
      int httpCode = http.GET();


      if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();

        DynamicJsonDocument doc(PayloadCapacity);
        DeserializationError error = deserializeJson(doc, payload);
        JsonObject root = doc.as<JsonObject>();
        if (error) {
          if (debug)
          {
            Serial.println("parseObject failed");
          }
        }
        poll = root["poll"].as<int>();

        Line1 = root["display"]["Line1"].as<String>();

        if (root["display"]["Line2"].is<String>())
        {
          Line2 = root["display"]["Line2"].as<String>();
        }
        else
        {
          SecondLineC = 6; //force first item in SecondLine array to display
          SecondLineLength = 0;
          for ( const auto& value : root["display"]["Line2"].as<JsonArray>() ) {

            SecondLine[SecondLineLength] = value.as<String>();
            SecondLineLength++;
          }
          SecondLineEnable = true;
        }


        if (debug)
        {
          Serial.println("HTTP_CODE_OK");
          Serial.println("Line1: " + Line1);
          Serial.println("Line2: " + Line2);
          Serial.println("poll: " + String(poll));
        }
      }

      http.end();
    }

    if (SecondLineC > 5)
    {
      SecondLineC = 0;

      if (SecondLineEnable)
      {
        if (SecondLineIndex > (SecondLineLength - 1))
        {
          SecondLineIndex = 0;
        }

        Line2 = SecondLine[SecondLineIndex];
        if (debug)
        {
          Serial.println("Changed Second Line");
          Serial.println("SecondLineIndex: " + String(SecondLineIndex));
          Serial.println("SecondLineLength: " + String(SecondLineLength));
          Serial.println("PRINTED " + SecondLine[SecondLineIndex]);
        }

        SecondLineIndex++;
      }
      PaintScreen();
    }


    secondsC++;
    SecondLineC++;
    secondsS++;

    previousMillis = millis();
  }

  server.handleClient();
}

void PaintScreen()
{
  lcd.setCursor( 0, 0 );
  lcd.print(Line1);
  lcd.setCursor( 0, 1 );
  lcd.print(Line2);
}

void AddStars()
{
  lcd.setCursor( 0, 0 );
  lcd.print("*");
  lcd.setCursor( 15, 0 );
  lcd.print("*");
}


String ReadFile(String Filename)
{
  String Result = "";
  File file = LittleFS.open("/" + Filename, "r");
  if (!file) {
    if (debug)
    {
      Serial.println("Failed to open file for reading");
    }
  }

  while (file.available()) {
    Result += char(file.read());
  }
  if (debug)
  {
    Serial.println("File Content:" + Result);
  }
  file.close();
  return Result;
}

void WriteFile(String Filename, String Content)
{
  File file = LittleFS.open("/" + Filename, "w");
  if (!file) {
    if (debug)
    {
      Serial.println("Failed to open file for reading");
    }
  }
  file.print(Content);
  if (debug)
  {
    Serial.println("File Content:" + Content);
  }
  file.close();
}



void handleNotFound() {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
}


bool handleFileRead(String path) {                       // send the right file to the client (if it exists)
  if (path.endsWith("/")) path += "index.html";          // If a folder is requested, send the index file
  String contentType = getContentType(path);             // Get the MIME type
  String pathWithGz = path + ".gz";
  if (LittleFS.exists(pathWithGz) || LittleFS.exists(path)) { // If the file exists, either as a compressed archive, or normal
    if (LittleFS.exists(pathWithGz))                         // If there's a compressed version available
      path += ".gz";                                         // Use the compressed version
    File file = LittleFS.open(path, "r");                    // Open the file
    server.streamFile(file, contentType);                   // Send it to the client
    file.close();                                          // Close the file again
    return true;
  }
  return false;                                          // If the file doesn't exist, return false
}

String getContentType(String filename) {
  if (filename.endsWith(".htm")) return "text/html";
  else if (filename.endsWith(".html")) return "text/html";
  else if (filename.endsWith(".css")) return "text/css";
  else if (filename.endsWith(".js")) return "application/javascript";
  else if (filename.endsWith(".png")) return "image/png";
  else if (filename.endsWith(".gif")) return "image/gif";
  else if (filename.endsWith(".jpg")) return "image/jpeg";
  else if (filename.endsWith(".ico")) return "image/x-icon";
  else if (filename.endsWith(".xml")) return "text/xml";
  else if (filename.endsWith(".pdf")) return "application/x-pdf";
  else if (filename.endsWith(".zip")) return "application/x-zip";
  else if (filename.endsWith(".gz")) return "application/x-gzip";
  return "text/plain";
}

String padRight(String input)
{
  String result = input;
  int spacesReq = 16 - input.length();

  if (spacesReq < 1)
  {
    return result;
  }
  for (int i = 0; i < spacesReq; i++) {
    result += " ";
  }

  return result;
}


void Restart()
{
  delay(3000);
  ESP.restart();
  delay(5000);
}

const char* wl_status_to_string(wl_status_t status) {
  switch (status) {
    case WL_NO_SHIELD: return "WL_NO_SHIELD";
    case WL_IDLE_STATUS: return "WL_IDLE_STATUS";
    case WL_NO_SSID_AVAIL: return "WL_NO_SSID_AVAIL";
    case WL_SCAN_COMPLETED: return "WL_SCAN_COMPLETED";
    case WL_CONNECTED: return "WL_CONNECTED";
    case WL_CONNECT_FAILED: return "WL_CONNECT_FAILED";
    case WL_CONNECTION_LOST: return "WL_CONNECTION_LOST";
    case WL_DISCONNECTED: return "WL_DISCONNECTED";
  }
}
