#include <WiFi.h>
#include <AsyncTCP.h>
#include <AsyncWebServer_ESP32_ENC.h>
#include <Preferences.h>

const bool TEST = false;

const int intervalInSecs = 60;
const char* ssid = "SSID";
const char* password = "PASSWORD";
const char* host = "maker.ifttt.com";
const char* apiKey = "API_KEY";

Preferences preferences;

// https://www.acetank.com/wp-content/uploads/2014/03/Oval-110-135-165-220-275.pdf
const int depthGalMap[45] = { 0, 1, 4, 7, 11, 15, 20, 25, 30, 36, 41, 47, 52, 58, 64, 69, 75,
                              81, 87, 92, 98, 104, 110, 115, 121, 127, 133, 138, 144, 150, 155,
                              161, 167, 173, 178, 184, 189, 194, 199, 204, 208, 212, 215, 218, 219 };

// History is a moving window of the past 30 days
const int HISTORY_CAP = 30;
String history[HISTORY_CAP];
int historyOldest = 0;
int historyLatest = 0;
int historyCount = 0;
void pushHistory(String item)
{
  if (historyCount < HISTORY_CAP)
  {
    historyLatest = historyCount;
    historyCount++;
  }
  else
  {
    historyLatest = historyOldest;
    historyOldest = (historyOldest + 1) % HISTORY_CAP;
  }
  history[historyLatest] = item;
  String save;
  for (int i = 0; i < historyCount; i++)
  {
    save += history[i];
  }
  // Persist
  preferences.putString("history", save);
}
void clearHistory()
{
  for (int i = 0; i < HISTORY_CAP; i++)
  {
    history[i] = "";
  }
  preferences.putString("history", "");
}

AsyncWebServer server(80);

const char* PARAM_INPUT_1 = "input1";

const int trigPin = 5;
const int echoPin = 18;

//define sound speed in cm/uS
#define SOUND_SPEED 0.034
#define CM_TO_INCH 0.393701

long duration;
float distanceCm;
float distanceInch;
float fillInches;
float fillGallons;
float threshold = 10.00;
String dataBuf;
String dataLabelBuf;

const char* ntpServer = "pool.ntp.org";
const long  gmtOffset_sec = 3600;
const int   daylightOffset_sec = 3600;

// Main HTML web page
char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html><head>
  <script src="https://code.highcharts.com/highcharts.js"></script>
  <title>Heating Oil Alerts</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  </head><body>
  220 gallon tank<br>
  Current: <b>%CURRENT_FILL% Gallons</b><br>
  <form action="/get">
    Alert When Below: <input type="number" min="0" max="40", step=".01" name="input1" value="%CURRENT_THRESH%">
    <input type="submit" value="Submit">
  </form><br>
  <div id="chart-daily-usage" class="container"></div>
  <script>
  var chartT = new Highcharts.Chart({
  chart:{ renderTo : 'chart-daily-usage' },
  title: { text: 'Daily Usage' },
  series: [{
    showInLegend: false,
    data: [%DATA%]
  }],
  plotOptions: {
    series: { dataLabels: { enabled: true } }
  },  
  xAxis: { categories: [%DATALABELS%] },
  yAxis: { title: { text: 'Gallons Used' } },
  credits: { enabled: false }
});
</script>
</body></html>)rawliteral";

// Parse strings in history, making 2 comma-separated 
// strings for html chart data & labels
void DailyHistory()
{
  dataBuf = "";
  dataLabelBuf = "";
  for (int i = 1; i < historyCount; i++)
  {
    int offset = (historyOldest + i) % HISTORY_CAP;
    dataLabelBuf += "'";
    dataLabelBuf += history[offset].substring(0, 8);
    dataLabelBuf += "'";
    float val = history[offset].substring(8).toFloat() - history[offset-1].substring(8).toFloat();
    // Don't log usage for fill days. If the dif is positive, call it zero used.
    val = min(val, 0.00);
    char diffBuf[4];
    dtostrf(val, 4, 2, diffBuf);
    dataBuf += diffBuf;
    if (i + 1 < historyCount)
    {
      dataLabelBuf += ',';
      dataBuf += ',';
    }
  }
  // Serial.print("Data: ");
  // Serial.println(dataBuf);
  // Serial.print("Labels: ");
  // Serial.println(dataLabelBuf);
}

// html text substitutions
String processor(const String& var)
{
  if(var == "CURRENT_FILL"){
    return String(fillGallons);
  }
  else if(var == "CURRENT_THRESH"){
    return String(threshold);
  }
  else if(var == "DATA"){
    return dataBuf;
  }
  else if(var == "DATALABELS"){
    return dataLabelBuf;
  }
  return String();
}

void notFound(AsyncWebServerRequest *request) 
{
  request->send(404, "text/plain", "Not found");
}

void setup() 
{
  // Serial port for debugging purposes
  Serial.begin(115200);

  pinMode(trigPin, OUTPUT); // Sets the trigPin as an Output
  pinMode(echoPin, INPUT); // Sets the echoPin as an Input
  
  // persistence for chart
  preferences.begin("oil-monitor", false);
  String save = preferences.getString("history", "");

  // parse saved history
  String el = save.substring(0, 13);
  int index = 0;
  while (el != 0)
  {
    pushHistory(el);
    index += 13;
    el = save.substring(index, index + 13);
  }

  // Connect to Wi-Fi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.println("WiFi connected");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());
  configTime(0, 0, ntpServer);

    // Send web page with input fields to client
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html, processor);
  });

  // Send a GET request to <ESP_IP>/get?input1=<inputMessage>
  server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage;
    String inputParam;
    // GET threshold value <URL>/get?input1=<inputMessage>
    if (request->hasParam(PARAM_INPUT_1)) {
      inputMessage = request->getParam(PARAM_INPUT_1)->value();
      inputParam = PARAM_INPUT_1;
      threshold = inputParam.toFloat();
    }
    else {
      inputMessage = "No message sent";
      inputParam = "none";
    }
    Serial.println(inputMessage);
    request->send(200, "text/html", "Set minimum gallons threshold to: " + inputMessage +
                                    "<br><a href=\"/\">Return</a>");
  });
  server.onNotFound(notFound);
  server.begin();
}

// Linear interpolation
float lerp(float a, float b, float x)
{
  return a + x * (b - a);
}

// Table look up. Inches==index.
float inchesToGallons(float inches)
{
  int inchIndex = inches;
  if (inchIndex <= 0)
  {
    return depthGalMap[0];
  }
  if (inchIndex >= 44)
  {
    return depthGalMap[44];
  }
  return lerp(depthGalMap[inchIndex], depthGalMap[inchIndex+1], (inches - inchIndex));
}

// Send an update by triggering a webhook on ifttt
void send(String val) 
{
  Serial.print("connecting to ");
  Serial.println(host);
  WiFiClient client;
  const int httpPort = 80;
  if (!client.connect(host, httpPort)) {
    Serial.println("connection failed");
    return;
  }

  String url = "/trigger/fuel_threshold/with/key/";
  url += apiKey;

  Serial.print("Requesting URL: ");
  Serial.println(url);
  client.print(String("POST ") + url + " HTTP/1.1\r\n" +
                  "Host: " + host + "\r\n" +
                  "Content-Type: application/x-www-form-urlencoded\r\n" +
                  "Content-Length: 13\r\n\r\n" +
                  "value1=" + val + "\r\n");
}

void loop() 
{
  // Clears the trigger pin
  digitalWrite(trigPin, LOW);
  delayMicroseconds(2);
  // Sets the trigger pin on HIGH state for 10 micro seconds
  digitalWrite(trigPin, HIGH);
  delayMicroseconds(10);
  digitalWrite(trigPin, LOW);

  // Reads the echoPin, returns the sound wave travel time in microseconds
  duration = pulseIn(echoPin, HIGH);

  // Calculate the distance
  distanceCm = duration * SOUND_SPEED/2;

  // Convert to inches
  distanceInch = distanceCm * CM_TO_INCH;
  fillInches = 44.00 - distanceInch;
  fillGallons = inchesToGallons(fillInches);

  // Test values for when no proximity sensor is connected
  // fillGallons = random(3500) / 100.00;

  // Prints the distance in the Serial Monitor
  if (TEST)
  {
    Serial.print("Fill Level (inches): ");
    Serial.println(fillInches);
    Serial.print("Fill Level (gallons): ");
    Serial.println(fillGallons);
  }
  else if (fillGallons < threshold)
  {
    // Lower the threshold and trigger the webhook for alert
    threshold -= 0.25;
    send(String(fillGallons));
  }

  if (Serial.available())
  {
    while (Serial.available()==0){} // wait for user input
    int removeCount = Serial.parseInt(); //Read user input and hold it in a variable
    if (removeCount > 0)
    {
      String historyBackup[HISTORY_CAP];
      for(int i = removeCount; i < HISTORY_CAP; i++)
      {
        pushHistory(historyBackup[i]);
      }
    }
  }

  // Save once daily; used in chart
  struct tm timeinfo;
  if (getLocalTime(&timeinfo)) {
    char galBuf[6];
    char dateBuf[9];
    char historyBuf[13];
    dtostrf(fillGallons, 6, 2, galBuf);
    
    strftime(dateBuf, 9, "%m/%d/%y", &timeinfo); // DATE
    // strftime(dateBuf, 9, "%H/%M/%S", &timeinfo); // TIME for testing; changes frequently
    
    sprintf(historyBuf, "%s%s", dateBuf, galBuf);
    if (historyCount == 0)
    {
      // Serial.print("First Recording: ");
      // Serial.println(historyBuf);
      pushHistory(historyBuf);
    }
    else
    {
      if (history[historyLatest].substring(0, 8) != dateBuf)
      {
        // Serial.print("Recording: ");
        // Serial.println(historyBuf);
        pushHistory(historyBuf);
        DailyHistory();
      }
    }
  }
  else
  {
    Serial.println("Could not get date");
  }

  delay(intervalInSecs * 1000);
}