/***************************************************
  Main of FingerprintDoorbell 
 ****************************************************/
#define MQTT_SOCKET_TIMEOUT 1
#include <WiFi.h>
#include <DNSServer.h>
#include <espmDns.h>
#include <time.h>
#include <ESPAsyncWebServer.h>
#include <SPIFFS.h>
#include <PubSubClient.h>
#include "FingerprintManager.h"
#include "SettingsManager.h"
#include "global.h"
#include "Ticker.h"
#include "Keypad.h"
#include <Wire.h>
#include <SparkFunSX1509.h>
#include <ArduinoOTA.h>
#include <sunrise.hpp>

// SX1509 I2C address (set by ADDR1 and ADDR0 (00 by default):
const byte SX1509_ADDRESS = 0x3E; // SX1509 I2C address
SX1509 io;                        // Create an SX1509 object to be used throughout

const byte ROWS = 4; //four rows
const byte COLS = 3; //three columns
bool openingDoor = false;
bool ringingBell = false;
char keys[ROWS][COLS] = {
{'1','2','3'},
{'4','5','6'},
{'7','8','9'},
{'*','0','#'}
};
byte rowPins[ROWS] = {1, 6, 5, 3}; //connect to the row pinouts of the kpd
byte colPins[COLS] = {2, 0, 4}; //connect to the column pinouts of the kpd

Keypad keypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );

enum class Mode { scan, enroll, wificonfig, maintenance, cooldown };

const char* VersionInfo = "0.6";

// ===================================================================================================================
// Caution: below are not the credentials for connecting to your home network, they are for the Access Point mode!!!
// ===================================================================================================================
const char* WifiConfigSsid = "FingerprintDoorbell-Config"; // SSID used for WiFi when in Access Point mode for configuration
const char* WifiConfigPassword = "12345678"; // password used for WiFi when in Access Point mode for configuration. Min. 8 chars needed!
IPAddress   WifiConfigIp(192, 168, 4, 1); // IP of access point in wifi config mode
#define TZ_INFO "WEST-1DWEST-2,M3.5.0/02:00:00,M10.5.0/03:00:00" // Western European Time
const int buzzerOutputPin = 8; // pin connected to the buzzer (when using hardware connection instead of mqtt to ring the bell)
const int doorOpenerOutputPin = 9; //USE 6 HERE pin connected to the door opener (when using hardware connection instead of mqtt to open the door)
const int   doorbellOutputPin = 10; // pin connected to the doorbell (when using hardware connection instead of mqtt to ring the bell)
const int KeyboardPin = 4;  //pin connected to an analog keyboard (see voltage ranges for the nubers in seperate array)
const int KEY_POLLING_MS = 50;
const uint8_t NUM_PIN_MAX_DIGITS = {10};
#ifdef CUSTOM_GPIOS
  const int   customOutput1 = 18; // not used internally, but can be set over MQTT
  const int   customOutput2 = 26; // not used internally, but can be set over MQTT
  const int   customInput1 = 21; // not used internally, but changes are published over MQTT
  const int   customInput2 = 22; // not used internally, but changes are published over MQTT
  bool customInput1Value = false;
  bool customInput2Value = false;
#endif

const int logMessagesCount = 5;
String logMessages[logMessagesCount]; // log messages, 0=most recent log message
bool shouldReboot = false;
unsigned long wifiReconnectPreviousMillis = 0;
unsigned long mqttReconnectPreviousMillis = 0;
uint32_t sunrise = 0;
uint32_t sunset = 0;

String enrollId;
String enrollName;
Mode currentMode = Mode::scan;

FingerprintManager fingerManager;
SettingsManager settingsManager;
bool needMaintenanceMode = false;

Ticker keyboardTick;
Ticker openDoorTick;
Ticker ringBellTick;
Ticker cooldownTick;

const byte DNS_PORT = 53;
DNSServer dnsServer;
AsyncWebServer webServer(80); // AsyncWebServer  on port 80
AsyncEventSource events("/events"); // event source (Server-Sent events)

WiFiClient espClient;
PubSubClient mqttClient(espClient);
long lastMsg = 0;
char msg[50];
int value = 0;
bool mqttConfigValid = true;


Match lastMatch;

void timeDoorOpener(uint8_t _state = HIGH);
void timeBellRing(uint8_t _state= HIGH);

void addLogMessage(const String& message) {
  // shift all messages in array by 1, oldest message will die
  for (int i=logMessagesCount-1; i>0; i--)
    logMessages[i]=logMessages[i-1];
  logMessages[0]=message;
}

String getLogMessagesAsHtml() {
  String html = "";
  for (int i=logMessagesCount-1; i>=0; i--) {
    if (logMessages[i]!="")
      html = html + logMessages[i] + "<br>";
  }
  return html;
}

String getTimestampString(){
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo, 1000)){
    Serial.println("Failed to obtain time");
    return "no time";
  }
  
  char buffer[25];
  strftime(buffer,sizeof(buffer),"%Y-%m-%d %H:%M:%S", &timeinfo);
  String datetime = String(buffer);
  return datetime;
}

/* wait for maintenance mode or timeout 5s */
bool waitForMaintenanceMode() {
  needMaintenanceMode = true;
  unsigned long startMillis = millis();
  while (currentMode != Mode::maintenance) {
    if ((millis() - startMillis) >= 5000ul) {
      needMaintenanceMode = false;
      return false;
    }
    delay(50);
  }
  needMaintenanceMode = false;
  return true;
}

// Replaces placeholder in HTML pages
String processor(const String& var){
  if(var == "LOGMESSAGES"){
    return getLogMessagesAsHtml();
  } else if (var == "FINGERLIST") {
    return fingerManager.getFingerListAsHtmlOptionList();
  } else if (var == "HOSTNAME") {
    return settingsManager.getWifiSettings().hostname;
  } else if (var == "VERSIONINFO") {
    return VersionInfo;
  } else if (var == "WIFI_SSID") {
    return settingsManager.getWifiSettings().ssid;
  } else if (var == "WIFI_PASSWORD") {
    if (settingsManager.getWifiSettings().password.isEmpty())
      return "";
    else
      return "********"; // for security reasons the wifi password will not left the device once configured
  } else if (var == "PIN_CODE") {
    return settingsManager.getAppSettings().chosenPin;
  } else if (var == "MQTT_SERVER") {
    return settingsManager.getAppSettings().mqttServer;
  } else if (var == "MQTT_USERNAME") {
    return settingsManager.getAppSettings().mqttUsername;
  } else if (var == "MQTT_PASSWORD") {
    return settingsManager.getAppSettings().mqttPassword;
  } else if (var == "MQTT_ROOTTOPIC") {
    return settingsManager.getAppSettings().mqttRootTopic;
  } else if (var == "NTP_SERVER") {
    return settingsManager.getAppSettings().ntpServer;
  }

  return String();
}


// send LastMessage to websocket clients
void notifyClients(String message) {
  String messageWithTimestamp = "[" + getTimestampString() + "]: " + message;
  Serial.println(messageWithTimestamp);
  addLogMessage(messageWithTimestamp);
  events.send(getLogMessagesAsHtml().c_str(),"message",millis(),1000);
  
  String mqttRootTopic = settingsManager.getAppSettings().mqttRootTopic;
  if(mqttClient.connected()) {
    mqttClient.publish((String(mqttRootTopic) + "/lastLogMessage").c_str(), message.c_str());
  }
}

void updateClientsFingerlist(String fingerlist) {
  Serial.println("New fingerlist was sent to clients");
  events.send(fingerlist.c_str(),"fingerlist",millis(),1000);
}


bool doPairing() {
  String newPairingCode = settingsManager.generateNewPairingCode();

  if (fingerManager.setPairingCode(newPairingCode)) {
    AppSettings settings = settingsManager.getAppSettings();
    settings.sensorPairingCode = newPairingCode;
    settings.sensorPairingValid = true;
    settingsManager.saveAppSettings(settings);
    notifyClients("Pairing successful.");
    return true;
  } else {
    notifyClients("Pairing failed.");
    return false;
  }

}


bool checkPairingValid() {
  AppSettings settings = settingsManager.getAppSettings();

   if (!settings.sensorPairingValid) {
     if (settings.sensorPairingCode.isEmpty()) {
       // first boot, do pairing automatically so the user does not have to do this manually
       return doPairing();
     } else {
      Serial.println("Pairing has been invalidated previously.");   
      return false;
     }
   }

  String actualSensorPairingCode = fingerManager.getPairingCode();
  //Serial.println("Awaited pairing code: " + settings.sensorPairingCode);
  //Serial.println("Actual pairing code: " + actualSensorPairingCode);

  if (actualSensorPairingCode.equals(settings.sensorPairingCode))
    return true;
  else {
    if (!actualSensorPairingCode.isEmpty()) { 
      // An empty code means there was a communication problem. So we don't have a valid code, but maybe next read will succeed and we get one again.
      // But here we just got an non-empty pairing code that was different to the awaited one. So don't expect that will change in future until repairing was done.
      // -> invalidate pairing for security reasons
      AppSettings settings = settingsManager.getAppSettings();
      settings.sensorPairingValid = false;
      settingsManager.saveAppSettings(settings);
    }
    return false;
  }
}

uint32_t WiFi_scanNetworks(void ){
  uint32_t _numNetworks = 0;

  int8_t res;
  res = WiFi.scanNetworks();
  if(res == WIFI_SCAN_FAILED){
    addLogMessage(F("NetworkScanFailed"));
  }else if(res == WIFI_SCAN_RUNNING){
    while(WiFi.scanComplete() == WIFI_SCAN_RUNNING){
      delay(100);
    }
    _numNetworks = WiFi.scanComplete();
  }else if(res >=0 ) {
    _numNetworks = res;
  }
  return _numNetworks;
}


bool initWifi() {
  uint32_t _numNetworks = 0;
  // Connect to Wi-Fi
  WifiSettings wifiSettings = settingsManager.getWifiSettings();
  WiFi.mode(WIFI_STA);
  WiFi.config(INADDR_NONE, INADDR_NONE, INADDR_NONE, INADDR_NONE);
  WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL);
  WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN);
  WiFi.persistent(false);
  WiFi.mode(WIFI_OFF);
  WiFi.mode(WIFI_STA);
  WiFi.disconnect(true); // delete old config
  WiFi.begin();
  delay(500); // 500ms seems to work in most cases, may depend on AP
  WiFi.disconnect(true); // delete old config

  WiFi.setHostname(wifiSettings.hostname.c_str()); //define hostname
  _numNetworks = WiFi_scanNetworks();  // scan in case this gets called before any scans

  if (_numNetworks == 0) {
    addLogMessage(F("No networks found"));
    WiFi.begin(wifiSettings.ssid.c_str(), wifiSettings.password.c_str());
  } else {
#ifdef WM_DEBUG_LEVEL
    DEBUG_WM(n, F("networks found"));
#endif
    int bestConnection = -1;
    // Find best RSSI AP for given SSID
    for (int i = 0; i < _numNetworks; i++) {
      if (wifiSettings.ssid == WiFi.SSID(i)) {
        addLogMessage(String(F("SSID ")) + wifiSettings.ssid + String(F(" found with RSSI: ")) +
                  String(WiFi.RSSI(i)) + String(F("(")) +
                  String(constrain((100.0 + WiFi.RSSI(i)) * 2, 0, 100)) +
                  String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(i) +
                  String(F(" and channel: ")) + String(WiFi.channel(i)));
        if (bestConnection == -1) {
          bestConnection = i;
        } else {
          if (WiFi.RSSI(i) > WiFi.RSSI(bestConnection)) {
            bestConnection = i;
          }
        }
      }
    }
    if (bestConnection == -1) {
      addLogMessage(String(F("No network found with SSID: "))+wifiSettings.ssid.c_str());
      WiFi.begin(wifiSettings.ssid.c_str(), wifiSettings.password.c_str());
    } else {
      addLogMessage(String(F("Trying to connect to SSID ")) + wifiSettings.ssid + String(F(" found with RSSI: ")) +
                  String(WiFi.RSSI(bestConnection)) + String(F("(")) +
                  String(constrain((100.0 + WiFi.RSSI(bestConnection)) * 2, 0, 100)) +
                  String(F(" %) and BSSID: ")) + WiFi.BSSIDstr(bestConnection) +
                  String(F(" and channel: ")) + String(WiFi.channel(bestConnection)));
      WiFi.begin(wifiSettings.ssid.c_str(), wifiSettings.password.c_str(), 0,
                        WiFi.BSSID(bestConnection));
    }
  }
  //WiFi.begin(wifiSettings.ssid.c_str(), wifiSettings.password.c_str());
  int counter = 0;
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Waiting for WiFi connection...");
    counter++;
    if (counter > 30)
      return false;
  }
  Serial.println("Connected!");
  setenv("TZ", TZ_INFO, 1);             // Zeitzone  muss nach dem reset neu eingestellt werden
  tzset();
  configTzTime(TZ_INFO, settingsManager.getAppSettings().ntpServer.c_str(),"nas.local","fritz.box"); // ESP32 Systemzeit mit NTP Synchronisieren
  // Print ESP32 Local IP Address
  Serial.println(WiFi.localIP());

  return true;
}

void initWiFiAccessPointForConfiguration() {
  WiFi.softAPConfig(WifiConfigIp, WifiConfigIp, IPAddress(255, 255, 255, 0));
  WiFi.softAP(WifiConfigSsid, WifiConfigPassword);

  // if DNSServer is started with "*" for domain name, it will reply with
  // provided IP to all DNS request
  dnsServer.start(DNS_PORT, "*", WifiConfigIp);

  Serial.print("AP IP address: ");
  Serial.println(WifiConfigIp); 
}

void onOTAStart() {
  // Log when OTA has started
  Serial.println("OTA update started!");
  // <Add your own code here>
}

void onOTAProgress(size_t current, size_t final) {
  static unsigned long ota_progress_millis = 0;
  // Log every 1 second
  if (millis() - ota_progress_millis > 1000) {
    ota_progress_millis = millis();
    Serial.printf("OTA Progress Current: %u bytes, Final: %u bytes\n", current, final);
  }
}

void onOTAEnd(bool success) {
  // Log when OTA has finished
  if (success) {
    Serial.println("OTA update finished successfully!");
  } else {
    Serial.println("There was an error during OTA update!");
  }
  // <Add your own code here>
}



void startWebserver(){
  
  // Initialize SPIFFS
  if(!SPIFFS.begin(true)){
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }

  
  
  // webserver for normal operating or wifi config?
  if (currentMode == Mode::wificonfig)
  {
    // =================
    // WiFi config mode
    // =================

    webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
      request->send(SPIFFS, "/wificonfig.html", String(), false, processor);
    });

    webServer.on("/save", HTTP_GET, [](AsyncWebServerRequest *request){
      if(request->hasArg("hostname"))
      {
        Serial.println("Save wifi config");
        WifiSettings settings = settingsManager.getWifiSettings();
        settings.hostname = request->arg("hostname");
        settings.ssid = request->arg("ssid");
        if (request->arg("password").equals("********")) // password is replaced by wildcards when given to the browser, so if the user didn't changed it, don't save it
          settings.password = settingsManager.getWifiSettings().password; // use the old, already saved, one
        else
          settings.password = request->arg("password");
        settingsManager.saveWifiSettings(settings);
        shouldReboot = true;
      }
      request->redirect("/");
    });


    webServer.onNotFound([](AsyncWebServerRequest *request){
      AsyncResponseStream *response = request->beginResponseStream("text/html");
      response->printf("<!DOCTYPE html><html><head><title>FingerprintDoorbell</title><meta http-equiv=\"refresh\" content=\"0; url=http://%s\" /></head><body>", WiFi.softAPIP().toString().c_str());
      response->printf("<p>Please configure your WiFi settings <a href='http://%s'>here</a> to connect FingerprintDoorbell to your home network.</p>", WiFi.softAPIP().toString().c_str());
      response->print("</body></html>");
      request->send(response);
    });

  }
  else
  {
    // =======================
    // normal operating mode
    // =======================
    events.onConnect([](AsyncEventSourceClient *client){
      if(client->lastId()){
        Serial.printf("Client reconnected! Last message ID it got was: %u\n", client->lastId());
      }
      //send event with message "ready", id current millis
      // and set reconnect delay to 1 second
      client->send(getLogMessagesAsHtml().c_str(),"message",millis(),1000);
    });
    webServer.addHandler(&events);

    
    // Route for root / web page
    webServer.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
      request->send(SPIFFS, "/index.html", String(), false, processor);
    });

    webServer.on("/enroll", HTTP_GET, [](AsyncWebServerRequest *request){
      if(request->hasArg("startEnrollment"))
      {
        enrollId = request->arg("newFingerprintId");
        enrollName = request->arg("newFingerprintName");
        currentMode = Mode::enroll;
      }
      request->redirect("/");
    });

    webServer.on("/editFingerprints", HTTP_GET, [](AsyncWebServerRequest *request){
      if(request->hasArg("selectedFingerprint"))
      {
        if(request->hasArg("btnDelete"))
        {
          int id = request->arg("selectedFingerprint").toInt();
          waitForMaintenanceMode();
          fingerManager.deleteFinger(id);
          currentMode = Mode::scan;
        }
        else if (request->hasArg("btnRename"))
        {
          int id = request->arg("selectedFingerprint").toInt();
          String newName = request->arg("renameNewName");
          fingerManager.renameFinger(id, newName);
        }
      }
      request->redirect("/");  
    });

    webServer.on("/settings", HTTP_GET, [](AsyncWebServerRequest *request){
      if(request->hasArg("btnSaveSettings"))
      {
        Serial.println("Save settings");
        AppSettings settings = settingsManager.getAppSettings();
        settings.chosenPin = request->arg("pincode");
        settings.mqttServer = request->arg("mqtt_server");
        settings.mqttUsername = request->arg("mqtt_username");
        settings.mqttPassword = request->arg("mqtt_password");
        settings.mqttRootTopic = request->arg("mqtt_rootTopic");
        settings.ntpServer = request->arg("ntpServer");
        settingsManager.saveAppSettings(settings);
        request->redirect("/");  
        shouldReboot = true;
      } else {
        request->send(SPIFFS, "/settings.html", String(), false, processor);
      }
    });


    webServer.on("/pairing", HTTP_GET, [](AsyncWebServerRequest *request){
      if(request->hasArg("btnDoPairing"))
      {
        Serial.println("Do (re)pairing");
        doPairing();
        request->redirect("/");  
      } else {
        request->send(SPIFFS, "/settings.html", String(), false, processor);
      }
    });



    webServer.on("/factoryReset", HTTP_GET, [](AsyncWebServerRequest *request){
      if(request->hasArg("btnFactoryReset"))
      {
        notifyClients("Factory reset initiated...");
        
        if (!fingerManager.deleteAll())
          notifyClients("Finger database could not be deleted.");
        
        if (!settingsManager.deleteAppSettings())
          notifyClients("App settings could not be deleted.");

        if (!settingsManager.deleteWifiSettings())
          notifyClients("Wifi settings could not be deleted.");
        
        request->redirect("/");  
        shouldReboot = true;
      } else {
        request->send(SPIFFS, "/settings.html", String(), false, processor);
      }
    });


    webServer.on("/deleteAllFingerprints", HTTP_GET, [](AsyncWebServerRequest *request){
      if(request->hasArg("btnDeleteAllFingerprints"))
      {
        notifyClients("Deleting all fingerprints...");
        
        if (!fingerManager.deleteAll())
          notifyClients("Finger database could not be deleted.");
        
        request->redirect("/");  
        
      } else {
        request->send(SPIFFS, "/settings.html", String(), false, processor);
      }
    });


    webServer.onNotFound([](AsyncWebServerRequest *request){
      request->send(404);
    });

    
  } // end normal operating mode


  // common url callbacks
  webServer.on("/reboot", HTTP_GET, [](AsyncWebServerRequest *request){
    request->redirect("/");
    shouldReboot = true;
  });

  webServer.on("/bootstrap.min.css", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/bootstrap.min.css", "text/css");
  });


  // Enable Over-the-air updates at http://<IPAddress>/update
  // Start server
  webServer.begin();
  // Init time by NTP Client

  notifyClients("System booted successfully!");

}


void mqttCallback(char* topic, byte* message, unsigned int length) {
  Serial.print("Message arrived on topic: ");
  Serial.print(topic);
  Serial.print(". Message: ");
  String messageTemp;
  
  for (int i = 0; i < length; i++) {
    Serial.print((char)message[i]);
    messageTemp += (char)message[i];
  }
  Serial.println();

  // Check incomming message for interesting topics
  if (String(topic) == String(settingsManager.getAppSettings().mqttRootTopic) + "/ignoreTouchRing") {
    if(messageTemp == "on"){
      fingerManager.setIgnoreTouchRing(true);
    }
    else if(messageTemp == "off"){
      fingerManager.setIgnoreTouchRing(false);
    }
  }

  #ifdef CUSTOM_GPIOS
    if (String(topic) == String(settingsManager.getAppSettings().mqttRootTopic) + "/customOutput1") {
      if(messageTemp == "on"){
        digitalWrite(customOutput1, HIGH); 
      }
      else if(messageTemp == "off"){
        digitalWrite(customOutput1, LOW); 
      }
    }
    if (String(topic) == String(settingsManager.getAppSettings().mqttRootTopic) + "/customOutput2") {
      if(messageTemp == "on"){
        digitalWrite(customOutput2, HIGH); 
      }
      else if(messageTemp == "off"){
        digitalWrite(customOutput2, LOW); 
      }
    }
  #endif  

}

void connectMqttClient() {
  if (!mqttClient.connected() && mqttConfigValid) {
    Serial.print("(Re)connect to MQTT broker...");
    // Attempt to connect
    bool connectResult;
    
    // connect with or witout authentication
    String lastWillTopic = settingsManager.getAppSettings().mqttRootTopic + "/lastLogMessage";
    String lastWillMessage = "FingerprintDoorbell disconnected unexpectedly";
    if (settingsManager.getAppSettings().mqttUsername.isEmpty() || settingsManager.getAppSettings().mqttPassword.isEmpty())
      connectResult = mqttClient.connect(settingsManager.getWifiSettings().hostname.c_str(),lastWillTopic.c_str(), 1, false, lastWillMessage.c_str());
    else
      connectResult = mqttClient.connect(settingsManager.getWifiSettings().hostname.c_str(), settingsManager.getAppSettings().mqttUsername.c_str(), settingsManager.getAppSettings().mqttPassword.c_str(), lastWillTopic.c_str(), 1, false, lastWillMessage.c_str());

    if (connectResult) {
      // success
      Serial.println("connected");
      // Subscribe
      String mqttRootTopic = settingsManager.getAppSettings().mqttRootTopic;
      mqttClient.subscribe((String(mqttRootTopic) + "/ignoreTouchRing").c_str(), 1); // QoS = 1 (at least once)
      #ifdef CUSTOM_GPIOS
        mqttClient.subscribe((settingsManager.getAppSettings().mqttRootTopic + "/customOutput1").c_str(), 1); // QoS = 1 (at least once)
        mqttClient.subscribe((settingsManager.getAppSettings().mqttRootTopic + "/customOutput2").c_str(), 1); // QoS = 1 (at least once)
      #endif
      mqttClient.publish((String(mqttRootTopic) + "/hostname").c_str(), settingsManager.getWifiSettings().hostname.c_str(), true);
      mqttClient.publish((String(mqttRootTopic) + "/IP").c_str(), WiFi.localIP().toString().c_str(), true);
      mqttClient.publish((String(mqttRootTopic) + "/NTP-Server").c_str(), settingsManager.getAppSettings().ntpServer.c_str(), true);
      char buffer[35];
      snprintf(buffer, sizeof buffer, "%d", WiFi.RSSI());
      mqttClient.publish((String(mqttRootTopic) + "/RSSI [dBm]").c_str(),buffer,true);
    } else {
      if (mqttClient.state() == 4 || mqttClient.state() == 5) {
        mqttConfigValid = false;
        notifyClients("Failed to connect to MQTT Server: bad credentials or not authorized. Will not try again, please check your settings.");
      } else {
        notifyClients(String("Failed to connect to MQTT Server, rc=") + mqttClient.state() + ", try again in 30 seconds");
      }
    }
  }
}

void timeDoorOpener(uint8_t _state){
  if(_state){
    openingDoor = true;
  } else {
    openingDoor = false;
  }
  io.digitalWrite(doorOpenerOutputPin, _state);
  openDoorTick.once(2,timeDoorOpener,(uint8_t) LOW);  //switch back off after one second
}

void timeBellRing(uint8_t _state){
  switch(_state){
    case 1:
      io.digitalWrite(doorbellOutputPin, HIGH);
      io.digitalWrite(buzzerOutputPin, HIGH);
      ringBellTick.once(0.2,timeBellRing,(uint8_t) 2);  //switch back off after one second
      break;
    case 2:
      io.digitalWrite(doorbellOutputPin, LOW);
      io.digitalWrite(buzzerOutputPin, LOW);
      ringBellTick.once(0.5,timeBellRing,(uint8_t) 3);  //switch back off after one second
      break;
    case 3:
      io.digitalWrite(doorbellOutputPin, HIGH);
      io.digitalWrite(buzzerOutputPin, HIGH);
      ringBellTick.once(0.2,timeBellRing,(uint8_t) 4);  //switch back off after one second
      break;
    case 4:
      io.digitalWrite(doorbellOutputPin, LOW);
      io.digitalWrite(buzzerOutputPin, LOW);
      break;
  }
  
  
}

void continueScanMode(void){
  currentMode = Mode::scan;
  fingerManager.setLedRingReady();
}

void setCooldown(uint16_t time){
  currentMode = Mode::cooldown;
  cooldownTick.once_ms(time,continueScanMode);
}

void openDoor(Match _match){
  String mqttRootTopic = settingsManager.getAppSettings().mqttRootTopic;
  timeDoorOpener();
  if(mqttClient.connected()){
    tm timeinfo;
    char buffer[35];
    if(!getLocalTime(&timeinfo)){
        strcpy(buffer, "00-00-0000T00:00:00+0000");
    }else{
        strftime(buffer, sizeof(buffer), "%FT%T%z", &timeinfo);
    }
    mqttClient.publish((String(mqttRootTopic) + "/timestamp").c_str(),buffer,true);
    mqttClient.publish((String(mqttRootTopic) + "/ring").c_str(), "off",true);
    mqttClient.publish((String(mqttRootTopic) + "/matchId").c_str(), String(_match.matchId).c_str(),true);
    mqttClient.publish((String(mqttRootTopic) + "/matchName").c_str(), _match.matchName.c_str(),true);
    mqttClient.publish((String(mqttRootTopic) + "/matchConfidence").c_str(), String(_match.matchConfidence).c_str(),true);
  }
  setCooldown(2000); // wait some time before next scan to let the LED blink
  Serial.println("MQTT message sent: Open the door!");
}

void ringBell(void){
  String mqttRootTopic = settingsManager.getAppSettings().mqttRootTopic;
  tm timeinfo;
  char buffer[35];
  if(!getLocalTime(&timeinfo)){
      strcpy(buffer, "00-00-0000T00:00:00+0000");
  }else{
      strftime(buffer, sizeof(buffer), "%FT%T%z", &timeinfo);
  }
  if(timeinfo.tm_hour > 7 && timeinfo.tm_hour < 20){  //only ring the bell from 8:00 to 20:00
    timeBellRing();
    if(mqttClient.connected()){
      mqttClient.publish((String(mqttRootTopic) + "/ring").c_str(), "off",true);
      mqttClient.publish((String(mqttRootTopic) + "/matchName").c_str(), "No ring at night",true);
    }
    return;
  }else{
    if(mqttClient.connected()){
      mqttClient.publish((String(mqttRootTopic) + "/ring").c_str(), "on",true);
      mqttClient.publish((String(mqttRootTopic) + "/matchName").c_str(), "Ring The Bell",true);
    }
  }
  if(mqttClient.connected()){
    mqttClient.publish((String(mqttRootTopic) + "/timestamp").c_str(),buffer,true);
    mqttClient.publish((String(mqttRootTopic) + "/matchId").c_str(), "-1",true);
    mqttClient.publish((String(mqttRootTopic) + "/matchConfidence").c_str(), "-1",true);
  }
}




void doScan()
{
  Match match = fingerManager.scanFingerprint();
  String mqttRootTopic = settingsManager.getAppSettings().mqttRootTopic;
  switch(match.scanResult)
  {
    case ScanResult::noFinger:
      delay(50); // wait some time before next scan to let ESP rest)
      // standard case, occurs every iteration when no finger touchs the sensor
      if (match.scanResult != lastMatch.scanResult) {
        Serial.println("no finger");
        /*if(mqttClient.connected()){   //uncomment this to clear message after 2 secs (cooldown)
          mqttClient.publish((String(mqttRootTopic) + "/timestamp").c_str(),"");
          mqttClient.publish((String(mqttRootTopic) + "/ring").c_str(), "off");
          mqttClient.publish((String(mqttRootTopic) + "/matchId").c_str(), "-1");
          mqttClient.publish((String(mqttRootTopic) + "/matchName").c_str(), "");
          mqttClient.publish((String(mqttRootTopic) + "/matchConfidence").c_str(), "-1");
        }*/
      }
      break; 
    case ScanResult::matchFound:
      if (match.scanResult != lastMatch.scanResult) {
        if (checkPairingValid()) {
          openDoor(match);
        } else {
          notifyClients("Security issue! Match was not sent by MQTT because of invalid sensor pairing! This could potentially be an attack! If the sensor is new or has been replaced by you do a (re)pairing in settings page.");
        }
      }
      notifyClients( String("Match Found: ") + match.matchId + " - " + match.matchName  + " with confidence of " + match.matchConfidence );
      break;
    case ScanResult::noMatchFound:
      if (match.scanResult != lastMatch.scanResult) {
        ringBell();
      } else {
        setCooldown(5000); // wait some time before next scan to let the LED blink
      }
      notifyClients(String("No Match Found (Code ") + match.returnCode + ")");
      break;
    case ScanResult::error:
      notifyClients(String("ScanResult Error (Code ") + match.returnCode + ")");
      break;
  };
  lastMatch = match;

}

void doEnroll()
{
  int id = enrollId.toInt();
  if (id < 1 || id > 200) {
    notifyClients("Invalid memory slot id '" + enrollId + "'");
    return;
  }

  NewFinger finger = fingerManager.enrollFinger(id, enrollName);
  if (finger.enrollResult == EnrollResult::ok) {
    notifyClients("Enrollment successfull. You can now use your new finger for scanning.");
    updateClientsFingerlist(fingerManager.getFingerListAsHtmlOptionList());
  }  else if (finger.enrollResult == EnrollResult::error) {
    notifyClients(String("Enrollment failed. (Code ") + finger.returnCode + ")");
  }
}



void reboot()
{
  io.digitalWrite(doorOpenerOutputPin, LOW);
  notifyClients("System is rebooting now...");
  delay(1000);
    
  mqttClient.disconnect();
  espClient.stop();
  dnsServer.stop();
  webServer.end();
  WiFi.disconnect();
  ESP.restart();
}

void keyboardPoller(void){
  static uint8_t pinpos = 0;
  static int32_t resetTimer = 0;
  static uint8_t pin[NUM_PIN_MAX_DIGITS] = {0};
  uint8_t key = keypad.getKey();
  if(key){
    //String keyStr(key);
    //notifyClients("Key: "+keyStr);
    //Serial.printf("KeyVal: %c\n", key);
    pin[pinpos++] = key;
    resetTimer = 0;
    io.digitalWrite(buzzerOutputPin, HIGH);
    delay(50);
    io.digitalWrite(buzzerOutputPin, LOW);
  }
  if(pinpos){
    resetTimer++;
    if(resetTimer > 2000/KEY_POLLING_MS){
      resetTimer = 0;
      pinpos = 0;
      Serial.println("RESET");
    }
    if(pinpos == settingsManager.getAppSettings().chosenPin.length()){
      bool pinOK = true;
      resetTimer = 0;
      String PinStr = settingsManager.getAppSettings().chosenPin;
      for(uint8_t i=0;i<pinpos;i++){
        uint8_t coosenPinDigit = PinStr.charAt(i);
        if(pin[i] != coosenPinDigit){
          pinOK= false;
        }
      }
      if(pinOK){
        Serial.println("OPEN!!");
        Match match;
        match.matchName = "KEYPAD";
        openDoor(match);
      }else{
        pinOK = true;
        PinStr = "************";
        for(uint8_t i=0;i<pinpos;i++){
          uint8_t coosenPinDigit = PinStr.charAt(i);
          if(pin[i] != coosenPinDigit){
            pinOK= false;
          }
        }
        if(pinOK){
          ESP.restart();
        }
      }
      pinpos = 0;
    }
  }
}

void checkForNight(void){
  bool night = true;
  tm timeinfo;
    if(getLocalTime(&timeinfo)){
      if(timeinfo.tm_min + timeinfo.tm_hour*60 > sunrise && timeinfo.tm_min + timeinfo.tm_hour*60 < sunset){
        night = false;
        mqttClient.publish((String(settingsManager.getAppSettings().mqttRootTopic) + "/night").c_str(), "false",true);
      }else{
        night = true;
        mqttClient.publish((String(settingsManager.getAppSettings().mqttRootTopic) + "/night").c_str(), "true",true);
      }
      fingerManager.setNightMode(night);
    }else{
      mqttClient.publish((String(settingsManager.getAppSettings().mqttRootTopic) + "/night").c_str(), "N/A",true);
      fingerManager.setNightMode(true);
    }
}


void setup()
{
  keyboardTick.attach_ms(KEY_POLLING_MS,keyboardPoller);
  // open serial monitor for debug infos
  Serial.begin(115200);
  //while (!Serial);  // For Yun/Leo/Micro/Zero/...
  //delay(2000);
  Serial.println("Hello");
  #ifdef CUSTOM_GPIOS
    pinMode(customOutput1, OUTPUT); 
    pinMode(customOutput2, OUTPUT); 
    pinMode(customInput1, INPUT_PULLDOWN);
    pinMode(customInput2, INPUT_PULLDOWN);
  #endif  
  Wire.begin(8, 9); // SDA, SCL);
  // Call io.begin(<address>) to initialize the SX1509. If it
  // successfully communicates, it'll return 1.
  if (io.begin(SX1509_ADDRESS) == false)
  {
    Serial.println("Failed to communicate. Check wiring and address of SX1509.");
    //while (1); // If we fail to communicate, loop forever.
  }

  // Call io.pinMode(<pin>, <mode>) to set an SX1509 pin as
  // an output:
  io.pinMode(doorbellOutputPin, OUTPUT);
  io.pinMode(doorOpenerOutputPin, OUTPUT);
  io.pinMode(buzzerOutputPin, OUTPUT);
  io.digitalWrite(doorbellOutputPin, LOW);
  io.digitalWrite(doorOpenerOutputPin, LOW);    
  io.digitalWrite(buzzerOutputPin, HIGH);
  delay(300);
  io.digitalWrite(buzzerOutputPin, LOW);
  Serial.println("Hello2");
  settingsManager.loadWifiSettings();
  settingsManager.loadAppSettings();
  Serial.println("Hello3");
  fingerManager.connect();
  Serial.println("Hello4");
  if (!checkPairingValid())
    notifyClients("Security issue! Pairing with sensor is invalid. This could potentially be an attack! If the sensor is new or has been replaced by you do a (re)pairing in settings page. MQTT messages regarding matching fingerprints will not been sent until pairing is valid again.");
  Serial.println("Hello5");
  if (fingerManager.isFingerOnSensor() || !settingsManager.isWifiConfigured())
  {
    // ring touched during startup or no wifi settings stored -> wifi config mode
    currentMode = Mode::wificonfig;
    Serial.println("Started WiFi-Config mode");
    fingerManager.setLedRingWifiConfig();
    initWiFiAccessPointForConfiguration();
    startWebserver();

  } else {
    Serial.println("Started normal operating mode");
    currentMode = Mode::scan;
    if (initWifi()) {
      startWebserver();
      if (settingsManager.getAppSettings().mqttServer.isEmpty()) {
        mqttConfigValid = false;
        notifyClients("Error: No MQTT Broker is configured! Please go to settings and enter your server URL + user credentials.");
      } else {
        delay(2000);
        IPAddress mqttServerIp;
        if (WiFi.hostByName(settingsManager.getAppSettings().mqttServer.c_str(), mqttServerIp))
        {
          mqttConfigValid = true;
          Serial.println("IP used for MQTT server: " + mqttServerIp.toString());
          mqttClient.setServer(mqttServerIp , 1883);
          mqttClient.setCallback(mqttCallback);
          connectMqttClient();
        }
        else {
          mqttConfigValid = false;
          notifyClients("MQTT Server '" + settingsManager.getAppSettings().mqttServer + "' not found. Please check your settings.");
        }
      }
      if (MDNS.begin(settingsManager.getWifiSettings().hostname.c_str())) {
          Serial.println("mDNS responder started");
          // Add service to MDNS-SD
          MDNS.addService("http", "tcp", 80);
      }
      ArduinoOTA.setHostname(settingsManager.getWifiSettings().hostname.c_str());
        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();
      if (fingerManager.connected)
        fingerManager.setLedRingReady();
      else
        fingerManager.setLedRingError();
    }  else {
      fingerManager.setLedRingError();
      //shouldReboot = true;
    }

  }
  Serial.println("Hello6");
}

unsigned long lastSunsetCkeck = 0, lastNightCheck = 0;

void loop()
{
  unsigned long currentMillis = millis();
  ArduinoOTA.handle();
  // shouldReboot flag for supporting reboot through webui
  if (shouldReboot) {
    reboot();
  }
  if(currentMillis - lastSunsetCkeck > (24*60*60*1000) || lastSunsetCkeck == 0){  //check every 24 hours
    lastSunsetCkeck = currentMillis;
    sunrise = calculateSunrise(47.58, 10.26,2);
    sunset = calculateSunset(47.58, 10.26,2);
    if(mqttClient.connected()){
      char buff[6];
      snprintf(buff, sizeof(buff), "%02d:%02d", sunrise/60, sunrise%60);
      mqttClient.publish((String(settingsManager.getAppSettings().mqttRootTopic) + "/sunrise").c_str(), buff,true);
      snprintf(buff, sizeof(buff), "%02d:%02d", sunset/60, sunset%60);
      mqttClient.publish((String(settingsManager.getAppSettings().mqttRootTopic) + "/sunset").c_str(), buff,true);
    }
  }
  if(currentMillis - lastNightCheck > 10*60*1000 || lastNightCheck == 0){    //check every 10 minutes
    if(!WiFi.isConnected() && currentMode != Mode::wificonfig && lastNightCheck != 0) {
      shouldReboot = true;
    }
    lastNightCheck = currentMillis;
    checkForNight();
  }
  
  // Reconnect handling
  if (currentMode != Mode::wificonfig)
  {
    // reconnect WiFi if down for 30s
    if ((WiFi.status() != WL_CONNECTED) && (currentMillis - wifiReconnectPreviousMillis >= 30000ul)) {
      Serial.println("Reconnecting to WiFi...");
      WiFi.disconnect();
      WiFi.reconnect();
      wifiReconnectPreviousMillis = currentMillis;
    }

    // reconnect mqtt if down
    if (!settingsManager.getAppSettings().mqttServer.isEmpty()) {
      if (!mqttClient.connected() && (currentMillis - mqttReconnectPreviousMillis >= 30000ul)) {
        connectMqttClient();
        mqttReconnectPreviousMillis = currentMillis;
      }
      mqttClient.loop();
    }
  }


  // do the actual loop work
  switch (currentMode)
  {
  case Mode::cooldown:
    if(openingDoor){
      io.digitalWrite(buzzerOutputPin, HIGH);
      fingerManager.setLedRingOk();
      delay(300);
      io.digitalWrite(buzzerOutputPin, LOW);
      openingDoor = false;
    }
  break;
  case Mode::scan:
    if (fingerManager.connected)
      doScan();
    break;
  
  case Mode::enroll:
    doEnroll();
    currentMode = Mode::scan; // switch back to scan mode after enrollment is done
    break;
  
  case Mode::wificonfig:
    dnsServer.processNextRequest(); // used for captive portal redirect
    break;

  case Mode::maintenance:
    // do nothing, give webserver exclusive access to sensor (not thread-safe for concurrent calls)
    break;

  }

  // enter maintenance mode (no continous scanning) if requested
  if (needMaintenanceMode)
    currentMode = Mode::maintenance;

  #ifdef CUSTOM_GPIOS
    // read custom inputs and publish by MQTT
    bool i1;
    bool i2;
    i1 = (digitalRead(customInput1) == HIGH);
    i2 = (digitalRead(customInput2) == HIGH);

    String mqttRootTopic = settingsManager.getAppSettings().mqttRootTopic;
    if (i1 != customInput1Value) {
        if (i1)
          mqttClient.publish((String(mqttRootTopic) + "/customInput1").c_str(), "on");      
        else
          mqttClient.publish((String(mqttRootTopic) + "/customInput1").c_str(), "off");      
    }

    if (i2 != customInput2Value) {
        if (i2)
          mqttClient.publish((String(mqttRootTopic) + "/customInput2").c_str(), "on");      
        else
          mqttClient.publish((String(mqttRootTopic) + "/customInput2").c_str(), "off");  
    }

    customInput1Value = i1;
    customInput2Value = i2;

  #endif  

}

