/*!
   @file WiFiConnect.cpp

   @mainpage WiFi Connection Manager with Captive Portal

   @section intro_sec Introduction

   This is the documentation for WiFiConnect for the Arduino platform <a href="https://github.com/smurf0969/WiFiConnect">https://github.com/smurf0969/WiFiConnect</a>.
   Further documentation can be found in the [Wiki](https://github.com/smurf0969/WiFiConnect/wiki) or in the project examples folder on GitHub.

   It is a WiFi connection manager for use with the popular ESP8266 and ESP32 chips.

   It contains a captive portal to allow easy connection and changing of WiFi netwoks
   via a web based interface and allows for additional user parameters.

   It can also display messages via a OLED screen see WiFiConnectOLED class.

   ![alt text](https://github.com/smurf0969/WiFiConnect/raw/master/extras/screenshots.png "Screenshots")

   This is a heavily customised version from the origional <a href="https://github.com/tzapu/WiFiManager">WiFiManager</a>
   developed by https://github.com/tzapu .

   @section dependencies Dependencies

   This library depends on <a href="https://github.com/esp8266/Arduino">
    ESP8266 Arduino Core</a> or <a href="https://github.com/espressif/arduino-esp32">ESP32 Arduino Core</a> depending on what chip you are using being present on your system.  
    Please make sure you have installed the latest version before using this library.  

    @section arduino_libraries Arduino Libraries

   - ArduinoJson 6.9.1 [https://github.com/bblanchon/ArduinoJson.git](https://github.com/bblanchon/ArduinoJson.git) or [https://blog.benoitblanchon.fr](https://blog.benoitblanchon.fr)
   - ESP8266 OLED SSD1306 ([Custom version](https://github.com/smurf0969/esp8266-oled-ssd1306/tree/Allow-overriding-default-font))    

   @section author Author

   Written by Stuart Blair.

   @section license License

   GNU General Public License v3.0 licence, all text here must be included in any redistribution and you should receive a copy of the license file.

*/
#include "WiFiConnect.h"

WiFiConnect::WiFiConnect() {
  //_params = (WiFiConnectParam**)malloc(WiFiConnect_MAX_PARAMS * sizeof(WiFiConnectParam*));
  _apName[0]='\0';
  _apPassword[0]='\0';
}
/**************************************************************************/
/*!
    @brief  This function gets overridden by WiFiConnectOLED,
            please view its documentation.
*/
/**************************************************************************/
void  WiFiConnect::displayConnecting(int attempt, int totalAttempts) {
  /*dummy*/
}
/**************************************************************************/
/*!
    @brief  This function gets overridden by WiFiConnectOLED,
            please view its documentation.
*/
/**************************************************************************/
void  WiFiConnect::displayConnected() {
  /*dummy*/
}
/**************************************************************************/
/*!
    @brief  This function gets overridden by WiFiConnectOLED,
            please view its documentation.
*/
/**************************************************************************/
void  WiFiConnect::displayAP() {
  /*dummy*/
}
/**************************************************************************/
/*!
    @brief  This function gets overridden by WiFiConnectOLED,
            please view its documentation.
*/
/**************************************************************************/
void  WiFiConnect::displayParams() {
  /*dummy*/
}
/**************************************************************************/
/*!
    @brief  This function gets overridden by WiFiConnectOLED,
            please view its documentation.
*/
/**************************************************************************/
void WiFiConnect::displayManualReset() {
  /*dummy*/
}
/**************************************************************************/
/*!
    @brief  This function gets overridden by WiFiConnectOLED,
            please view its documentation.
*/
/**************************************************************************/
void WiFiConnect::displayTurnOFF(int ms) {
  /*dummy*/
}
/**************************************************************************/
/*!
    @brief  This function gets overridden by WiFiConnectOLED,
            please view its documentation.
*/
/**************************************************************************/
void WiFiConnect::displayLoop() {
  /*dummy*/
}
/**************************************************************************/
/*!
    @brief  This function gets overridden by WiFiConnectOLED,
            please view its documentation.
*/
/**************************************************************************/
void WiFiConnect::displayON() {
  /*dummy*/
}
/**************************************************************************/
/*!
    @brief  Sets the timeout for when the Access Point is idle.
    @param  mins
            The number of minutes to before exiting an idle access point.
*/
/**************************************************************************/
void WiFiConnect::setAPModeTimeoutMins(int mins) {
  if (mins > 0) {
    _apTimeoutMins = mins;
  }
}
/**************************************************************************/
/*!
    @brief  Sets the name for the Access Point, setting a default based on
            the ESP chip id if needed.
    @param  apName
            The new name for the access point.
*/
/**************************************************************************/
void WiFiConnect::setAPName(const char *apName) {
  if(strlen(apName)>32){return;}
  if (strlen(apName)==0||(apName == NULL)) {
    String ssid = "ESP_" + String(ESP_getChipId());
    //_apName = ssid.c_str();
    strcpy(_apName,ssid.c_str());

  } else if (apName != NULL &&  strlen(apName)>0) {
   // _apName = apName;
    strcpy(_apName,apName);
  }
}
/**************************************************************************/
/*!
    @brief  Gets the Access Point name or the default based on the
            ESP chip id if not already set.
    @return  The name to be given to the access point.
*/
/**************************************************************************/
const char* WiFiConnect::getAPName() {
  if ((_apName == NULL ) || strlen(_apName)==0) {
    setAPName(NULL);
  }
  return _apName;
}
/**************************************************************************/
/*!
    @brief  Adds a custom parameter to be displayed in the accesspoint and
            parameters portals.
            Custom parameters can be used to save information that you do
            not want to hard code i.e. IP address, Port number that can be
            saved to the file system.
    @param  p
            The custom parameter to add.
*/
/**************************************************************************/
void WiFiConnect::addParameter(WiFiConnectParam *p) {
  _params[_paramsCount] = p;
  _paramsCount++;
  DEBUG_WC("Adding parameter");
  DEBUG_WC(p->getID());
}
/**************************************************************************/
/*!
    @brief  Starts the custom Parameters Portal using default
            access point name and password.
    @return Returns wether WiFi is connected to a network
*/
/**************************************************************************/
boolean WiFiConnect::startParamsPortal() {
  return startParamsPortal(AP_NONE, _apName, _apPassword);
}
/**************************************************************************/
/*!
    @brief  Starts the custom Parameters Portal using default
            access point name and password, but allows for specifying how
            to continue.
    @param  apcontinue
            What should the access point do if there is still no connection.
            AP_NONE - continue running code,
            AP_LOOP - stay in an infinate loop,
            AP_RESET - Restart the ESP
			AP_WAIT  - Keep AP alive waiting for input			
   @return  Returns wether WiFi is connected to a network if
            AP_NONE is passed as a parameter
*/
/**************************************************************************/
boolean WiFiConnect::startParamsPortal(AP_Continue apcontinue) {
  return startParamsPortal(apcontinue, _apName, _apPassword);
}
/**************************************************************************/
/*!
    @brief  Starts the custom Parameters Portal.
    @param  apcontinue
            What should the access point do if there is still no connection.
            AP_NONE - continue running code,
            AP_LOOP - stay in an infinate loop,
            AP_RESET - Restart the ESP
			AP_WAIT  - Keep AP alive waiting for input			
    @param  apName
            The Name to use for the access point
    @param  apPassword
            The optional Password to protect the access point
   @return  Returns wether WiFi is connected to a network if
            AP_NONE is passed as a parameter
*/
/**************************************************************************/
boolean WiFiConnect::startParamsPortal(AP_Continue apcontinue, const char  *apName, const char  *apPassword) {

	// Wrapper
	return startConfigurationPortal(apcontinue, apName, apPassword, true);  
}
/**************************************************************************/
/*!
    @brief  Starts the Access Portal for setting SSID and Password for
            connecting to a network and any custom user parameters.
            Uses the default access point name and password.
   @return  Returns wether WiFi is connected to a network if
            AP_NONE is passed as a parameter
*/
/**************************************************************************/
boolean WiFiConnect::startConfigurationPortal() {
  return startConfigurationPortal(AP_NONE, _apName, _apPassword);
}
/**************************************************************************/
/*!
    @brief  Starts the Access Portal for setting SSID and Password for
            connecting to a network and any custom user parameters.
            Uses the default access point name and password, but
            allows for specifying how to continue if network not connected.
    @param  apcontinue
            What should the access point do if there is still no connection.
            AP_NONE - continue running code,
            AP_LOOP - stay in an infinate loop,
            AP_RESET - Restart the ESP
			AP_WAIT  - Keep AP alive waiting for input
    @return Returns wether WiFi is connected to a network if
            AP_NONE is passed as a parameter
*/
/**************************************************************************/
boolean WiFiConnect::startConfigurationPortal(AP_Continue apcontinue) {
  return startConfigurationPortal(apcontinue, _apName, _apPassword);
}
/**************************************************************************/
/*!
    @brief  Starts the Access Portal for setting SSID and Password for
            connecting to a network and any custom user parameters.
    @param  apcontinue
            What should the access point do if there is still no connection.
            AP_NONE - continue running code,
            AP_LOOP - stay in an infinate loop,
            AP_RESET - Restart the ESP, handy for sensors if you network
                       goes down, do they keep trying to connect.
			AP_WAIT  - Keep AP alive waiting for input					   
    @param  apName
            The Name to use for the access point
    @param  apPassword
            The optional Password to protect the access point
    @return Returns wether WiFi is connected to a network if
            AP_NONE is passed as a parameter
*/
/**************************************************************************/
boolean WiFiConnect::startConfigurationPortal(AP_Continue apcontinue, const char  *apName, const char  *apPassword, bool paramsMode) {

  if (paramsMode) {
	    DEBUG_WC(F("WiFi AP STA - Parameters Portal"));
  } else {
		DEBUG_WC(F("WiFi AP STA - Configuration Portal"));
  }
  _lastAPPage = millis();

  delay(50);

  if (WiFi.status() != WL_CONNECTED) {
	  if (paramsMode) {
		   // Can't show the parameters portal if not connected to a WiFi network.
		   startConfigurationPortal(AP_WAIT, apName, apPassword, false);
	  } else {
      DEBUG_WC("startConfigurationPortal(): WiFi.mode(WIFI_AP)");
		  WiFi.mode(WIFI_AP);
	  }
  } else {
    WiFi.mode(WIFI_AP_STA); // start an access point on the same channel we're already connected to.
  }
  dnsServer.reset(new DNSServer());
#ifdef ESP8266
  server.reset(new ESP8266WebServer(80));
#else
  server.reset(new WebServer(80));
#endif
  setAPName(apName);
  DEBUG_WC(_apName);

  if (strlen(apPassword)>0){
    if (strlen(apPassword) < 8 || strlen(apPassword) > 63) {
      // fail passphrase to short or long!
      DEBUG_WC(F("Invalid AccessPoint password. Ignoring"));
      apPassword = NULL;
    }
    //_apPassword = apPassword;
    strcpy(_apPassword,apPassword);
    DEBUG_WC(_apPassword);
  }

  //optional soft ip config
  if (_ap_static_ip) {
    DEBUG_WC(F("Custom AP IP/GW/Subnet"));
    WiFi.softAPConfig(_ap_static_ip, _ap_static_gw, _ap_static_sn);
  }

  //if (_apPassword != NULL && _apPassword!="") {
  if(strlen(apPassword)>0){
    WiFi.softAP(_apName, _apPassword);//password option
  } else {
    WiFi.softAP(_apName);
    DEBUG_WC("startConfigurationPortal(): WiFi.softAP(_apName)");
  }

  delay(500); // Without delay I've seen the IP address blank
  DEBUG_WC(F("AP IP address: "));
  DEBUG_WC(WiFi.softAPIP());
  
  // Show on OLED depending on the mode
  if (!paramsMode) { displayAP(); } else {   displayParams(); }
  
  /* Setup the DNS server redirecting all the domains to the apIP */
  dnsServer->setErrorReplyCode(DNSReplyCode::NoError);
  dnsServer->start(DNS_PORT, "*", WiFi.softAPIP());

  if (paramsMode)
  {
	  
	  server->on("/", std::bind(&WiFiConnect::handleParamRoot, 			this));
	  server->on("/param", std::bind(&WiFiConnect::handleParams, 		this));
	  server->on("/params", std::bind(&WiFiConnect::handleParams, 		this));
	  server->on("/wifisave", std::bind(&WiFiConnect::handleWifiSave, 	this));
	  server->on("/i", std::bind(&WiFiConnect::handleInfo, 				this));
	  //server->on("/r", std::bind(&WiFiConnect::handleReset, this)); // doesn't work - causes boot loop
	  //server->on("/generate_204", std::bind(&WiFiConnect::handle204, this));  //Android/Chrome OS captive portal check.
	  server->on("/fwlink", std::bind(&WiFiConnect::handleParamRoot, 	this));  //Microsoft captive portal. Maybe not needed. Might be handled by notFound handler.
	  server->on("/hotspot_detect.html", std::bind(&WiFiConnect::handleParamRoot, this));
	  
  } else  { // Config portal mode
	  
	  /* Setup web pages: root, wifi config pages, SO captive portal detectors and not found. */
	  server->on("/", 			std::bind(&WiFiConnect::handleRoot, this));
	  server->on("/wifi", 		std::bind(&WiFiConnect::handleWifi, this, true));   // Auto Scan of APs
	  server->on("/0wifi", 		std::bind(&WiFiConnect::handleWifi, this, false)); // Manual entry form only
	  server->on("/wifisave", 	std::bind(&WiFiConnect::handleWifiSave, this));
	  server->on("/i", 			std::bind(&WiFiConnect::handleInfo, this)); // Not of interest - commented out in static HTML
	  server->on("/r", 			std::bind(&WiFiConnect::handleReset, this));
	  //server->on("/generate_204", std::bind(&WiFiConnect::handle204, this));  //Android/Chrome OS captive portal check.
	  server->on("/fwlink", 	std::bind(&WiFiConnect::handleRoot, this));  //Microsoft captive portal. Maybe not needed. Might be handled by notFound handler.
	  server->on("/hotspot_detect.html", std::bind(&WiFiConnect::handleRoot, this));
	  
  } // config portal  

  	server->onNotFound 	   (std::bind(&WiFiConnect::handleNotFound, this));
    server->begin(); // Web server start
    DEBUG_WC(F("HTTP server started"));

  //notify we entered AP mode
  if ( _apcallback != NULL) {
    _apcallback(this);
  }
  
  if (apcontinue == AP_WAIT)
	  DEBUG_WC(F("AP to stay alive indefinitely waiting for configuration."));
		

  _lastAPPage = millis();
  _readyToConnect = false;	  
  while (true) {
	
	if (apcontinue != AP_WAIT)	// Stay here if the AP_LOOP enabled	
		if (millis() - (unsigned long int)_lastAPPage >= ( (unsigned int)_apTimeoutMins * 60 * 1000)) {
		  break;
		}
		
    dnsServer->processNextRequest();
    server->handleClient();
    if (_readyToConnect) {
      _readyToConnect = false;

      DEBUG_WC(F("startConfigurationPortal(): Calling: autoConnect(_ssid.c_str(), _password.c_str(), WIFI_AP_STA)"));	      

      if (autoConnect(_ssid.c_str(), _password.c_str(), WIFI_AP_STA)) {
		  
        DEBUG_WC(F("Connection was a success, so changing to WIFI_STA mode only."));		
        WiFi.mode(WIFI_STA);
        delay(500);
        //WiFi.reconnect();
        //delay(500);
        if (_savecallback != NULL) {
          _savecallback();
        }
        break; // we connected!
      }
      else
      {
        DEBUG_WC(F("Connection was a failure. We keep waiting."));		  
      }
    } // ready to connect
	
    yield();
  }
  
  //teardown??
  DEBUG_WC(F("Exiting AP (or AP Params) Mode"));
  server->close();
  server.reset();

  dnsServer.reset();
  boolean con = (WiFi.status() == WL_CONNECTED);
  if (!con) {
    switch (apcontinue) {
      case AP_WAIT:
        break;
      case AP_NONE:
        DEBUG_WC(F("No AP continue action"));
        break;
      case AP_LOOP:
        DEBUG_WC(F("AP to turn off and stay in loop lockup not being accessible."));
        displayManualReset();
        displayTurnOFF(5 * 60 * 100); //5mins
        WiFi.mode(WIFI_OFF);
        while (true) {
          displayLoop();
          delay(1000);
          yield();
        }
	
        break;
      case AP_RESTART:
       case AP_RESET:
        displayManualReset();
        DEBUG_WC(F("AP restart chip"));
        delay(1000);
#if defined(ESP8266)
        ESP.reset();
#else
        ESP.restart();
#endif
        delay(2000);
    }
    WiFi.mode(WIFI_STA);
  }
  return con;
}
/**************************************************************************/
/*!
    @brief  Tries to connect to the last connected WiFi network.
    @return Returns wether WiFi is connected

    @section example_startConfigurationPortal Example

    The return value can be used to automatically start the access point
    so you may change networks if required.

    @code
    if(!wificonnect.autoConnect()){
        //Start the access point
        wificonnect.startConfigurationPortal();
    }
    @endcode
*/
/**************************************************************************/
boolean WiFiConnect::autoConnect() {
  return autoConnect(NULL, NULL, WIFI_STA);
}
/**************************************************************************/
/*!
    @brief  Tries to connect to the specified WiFi network.
    @param ssidName
            The SSID of the netqork you want to connect to.
    @param ssidPassword
           The password for a secure network.
    @param WiFiMode
           The WiFi mode
    @return Returns wether WiFi is connected
*/
/**************************************************************************/
boolean WiFiConnect::autoConnect(char const *ssidName, char const *ssidPassword, WiFiMode_t acWiFiMode) {
/*
 #ifdef ESP32
    WiFi.persistent(false); // disable persistent for esp32 after esp_wifi_start or else saves wont work (from WiFiManager)
 #endif  
*/

  DEBUG_WC(F("Auto Connect"));
  WiFi.mode(acWiFiMode);
  DEBUG_WC("autoConnect():WiFi.mode(acWiFiMode)");
  if (WiFi.status() == WL_CONNECTED) {
    DEBUG_WC(F("Already Connected"));
	  DEBUG_WC(WiFi.localIP());
    return true;
  }
  if (_sta_static_ip) {
    DEBUG_WC(F("Custom STA IP/GW/Subnet"));
    WiFi.config(_sta_static_ip, _sta_static_gw, _sta_static_sn);
    DEBUG_WC(WiFi.localIP());
  }
  
 //fix for auto connect racing issue
 /*
  if (WiFi.status() == WL_CONNECTED && (WiFi.SSID() == String(ssidName))) {
    DEBUG_WC(F("Already connected. Bailing out."));
	  DEBUG_WC(WiFi.localIP());
	  return true;
    //return WL_CONNECTED;
  }
*/

  int c = 0;
  while (c < _retryAttempts) {
    displayConnecting(c + 1, _retryAttempts);
    long ms = millis();
    if (ssidName == NULL || strlen(ssidName)==0) {
    
    #ifdef ESP8266
        if (WiFi.SSID() == "")
        {
          DEBUG_WC(F("No ESP8266 WiFi SSID configuration stored. Bailing."));
          return false;
        }    
        DEBUG_WC(F("Stored SSID:")); DEBUG_WC(WiFi.SSID());        
    #endif

    #ifdef ESP32
      // WiFi.SSID() Always returns null on ESP32.... Arduino framework bug?
      // So use lower-level IDF function to pull persistent configuration.
      wifi_config_t conf;
      esp_wifi_get_config(WIFI_IF_STA, &conf);

      String stored_ssid = String(reinterpret_cast<const char*>(conf.sta.ssid));
      if (stored_ssid == "")
      {
        DEBUG_WC(F("No ESP32 WiFi SSID configuration stored. Bailing."));
        return false;
      }       

        DEBUG_WC(F("Stored SSID:")); DEBUG_WC(stored_ssid);              
    #endif

        
      WiFi.begin(); // persistence is on by default, so if this WiFi connection should happen automatically
      DEBUG_WC("autoConnect(): WiFi.begin()");

/*          
    #ifdef ESP32
                            //https://forum.arduino.cc/index.php?topic=652513.0
      int  wifi_retry = 0;   // COUNTER SOLVES ESP32-BUG WITH CERTAIN ROUTERS: CONNECTION ONLY ESTABLISHED EVERY SECOND TIME
    
      while (WiFi.waitForConnectResult() != WL_CONNECTED && wifi_retry < 3) {
          DEBUG_WC("WLAN Not yet connected on ESP32...retrying");
          WiFi.begin();
          delay(250);
          wifi_retry++;
      }

    #endif
*/

    } else {
      DEBUG_WC(F("Connecting with SSID & Password"));
      DEBUG_WC(ssidName);
      DEBUG_WC(ssidPassword);
      WiFi.begin(ssidName, ssidPassword);
      DEBUG_WC("autoConnect(): WiFi.begin(ssidName, ssidPassword)");
    }


    while (millis() - (unsigned long int)ms < ( (unsigned int)_connectionTimeoutSecs * 1000)) {
      int ws = WiFi.status();
      if (ws == WL_CONNECTED) {
        DEBUG_WC(F("WIFI_CONNECTED"));
        delay(500);//wait for ip to refresh
        DEBUG_WC(WiFi.localIP());
        displayConnected();
        return true;
      } else if (ws == WL_CONNECT_FAILED) {
        DEBUG_WC(F("WIFI_CONNECT_FAILED"));
        delay(500);
        //return false;
      } else {
        delay(200);
        yield();
      }
    }
    DEBUG_WC(statusToString(WiFi.status()));
    c++;
  } // attemps
  DEBUG_WC(statusToString(WiFi.status()));
  return false;
}
/**************************************************************************/
/*!
    @brief  Sets how many times we should try to connect to the WiFi network.
    @param  attempts
            How may attampts.
*/
/**************************************************************************/
void WiFiConnect::setRetryAttempts(int attempts) {
  if (attempts >= 1) {
    _retryAttempts = attempts;
  }
}
/**************************************************************************/
/*!
    @brief  Sets how long to wait for the connection before failing.
    @param  timeout
            How may seconds to wait for the connection to be made.
*/
/**************************************************************************/
void WiFiConnect::setConnectionTimeoutSecs(int timeout) {
  if (timeout >= 1) {
    _connectionTimeoutSecs = timeout;
  }
}
/**************************************************************************/
/*!
    @brief  A helper to clear the settings for the last connected network.
            Remember to comment out or remove and re-upload your code
            otherwise chip will constantly restart.
*/
/**************************************************************************/
void WiFiConnect::resetSettings() {
  DEBUG_WC(F("Clearing Settings for:"));
  DEBUG_WC(WiFi.SSID());
#if defined(ESP8266)
  WiFi.disconnect(true);
  delay(1000);
  ESP.reset();
#else
  WiFi.disconnect(true);//doesnt seem to work
  esp_wifi_restore();//doesnt seem to work
  WiFi.begin("0", "0"); //HACK
  delay(1000);
  ESP.restart();
#endif
  delay(2000);
}
/**************************************************************************/
/*!
    @brief  A helper function to return the string version of a
            wifi network state.
    @param  state
            The state that we want the string representation of.
    @return WiFi state as a string.
*/
/**************************************************************************/
const char* WiFiConnect::statusToString(int state) {
  switch (state) {
    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";
    case WL_SCAN_COMPLETED:
      return "WL_SCAN_COMPLETED";
    case WL_NO_SSID_AVAIL:
      return "WL_NO_SSID_AVAIL";
    case WL_IDLE_STATUS:
      return "WL_IDLE_STATUS";
    case WL_NO_SHIELD:
      return "WL_NO_SHIELD";
    default:
      return "UNKNOWN CODE";
  }
}
/**************************************************************************/
/*!
    @brief  Outputs debug messages
    @param  text
            The text to be outputted
*/
/**************************************************************************/
template <typename Generic>
void WiFiConnect::DEBUG_WC(Generic text) {
  if (_debug) {
    Serial.print("*WC: ");
    Serial.println(text);
  }
}
/**************************************************************************/
/*!
    @brief  Handles the web request for root in the Parameters Portal and
            sends back the html menu
*/
/**************************************************************************/
void WiFiConnect::handleParamRoot() {
  if (captivePortal()) { // If captive portal redirect instead of displaying the page.
    return;
  }
  _lastAPPage = millis();
  DEBUG_WC("Sending Root");
  String page = FPSTR(AP_HTTP_HEAD);
  page.replace("{v}", "Options");
  page += FPSTR(AP_HTTP_SCRIPT);
  page += FPSTR(AP_HTTP_STYLE);
  page += FPSTR(AP_HTTP_HEAD_END);
  page += String(F("<h1>"));
  page += _apName;
  page += String(F("</h1>"));
//  page += String(F("<h3>WiFiConnect</h3>"));
  page += FPSTR(AP_HTTP_PORTAL_PARAM_OPTIONS);
  page += FPSTR(AP_HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);

}
/**************************************************************************/
/*!
    @brief  Handles the web request for displaying custom parameters
            in the Parameters Portal and sends back the html
*/
/**************************************************************************/
void WiFiConnect::handleParams() {
  _lastAPPage = millis();
  DEBUG_WC(F("Sending Params"));

  String page = FPSTR(AP_HTTP_HEAD);
  page.replace("{v}", "Config Params");
  page += FPSTR(AP_HTTP_SCRIPT);
  page += FPSTR(AP_HTTP_STYLE);
  page += FPSTR(AP_HTTP_HEAD_END);
  page += FPSTR(AP_HTTP_FORM_PARAM_START);
  char parLength[4];
  // add the extra parameters to the form
  for (int i = 0; i < _paramsCount; i++) {
    if (_params[i] == NULL) {
      break;
    }

    String pitem = FPSTR(AP_HTTP_FORM_PARAM);
    if (_params[i]->getID() != NULL) {
      pitem.replace("{i}", _params[i]->getID());
      pitem.replace("{n}", _params[i]->getID());
      pitem.replace("{p}", _params[i]->getPlaceholder());
      snprintf(parLength, 4, "%d", _params[i]->getValueLength());
      pitem.replace("{l}", parLength);
      pitem.replace("{v}", _params[i]->getValue());
      pitem.replace("{c}", _params[i]->getCustomHTML());
    } else {
      pitem = _params[i]->getCustomHTML();
    }

    page += pitem;
  }
  if (_params[0] != NULL) {
    page += "<br/>";
  }

  page += FPSTR(AP_HTTP_FORM_END);

  page += FPSTR(AP_HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);
}
/**************************************************************************/
/*!
    @brief  Handles the web request for displaying the menu in the
            Access Point portal.
*/
/**************************************************************************/
void WiFiConnect::handleRoot() {
  if (captivePortal()) { // If captive portal redirect instead of displaying the page.
    return;
  }
  _lastAPPage = millis();
  DEBUG_WC("Sending Root");
  String page = FPSTR(AP_HTTP_HEAD);
  page.replace("{v}", "Options");
  page += FPSTR(AP_HTTP_SCRIPT);
  page += FPSTR(AP_HTTP_STYLE);
  page += FPSTR(AP_HTTP_HEAD_END);
  page += String(F("<h1>"));
  page += _apName;
  page += String(F("</h1>"));
  //page += String(F("<h3>WiFiConnect</h3>"));
  page += FPSTR(AP_HTTP_PORTAL_OPTIONS);
  page += FPSTR(AP_HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);


}

/**************************************************************************/
/*!
    @brief  Handles the web request for entering the SSID and Password of
            the network you want to connect too.
            Optionally scans and shows available networks.
    @param  scan
            Whether to scan for networks or not.
*/
/**************************************************************************/
void WiFiConnect::handleWifi(boolean scan) {

  _lastAPPage = millis();
  DEBUG_WC(F("Sending WiFi"));

  String page = FPSTR(AP_HTTP_HEAD);
  page.replace("{v}", "Select WiFi");
  page += FPSTR(AP_HTTP_SCRIPT);
  page += FPSTR(AP_HTTP_STYLE);
  page += FPSTR(AP_HTTP_HEAD_END);

  if (scan) {
    int n = WiFi.scanNetworks();
    DEBUG_WC(F("Scan done"));
    if (n == 0) {
      DEBUG_WC(F("No networks found"));
      page += F("No networks found. Refresh to scan again.");
    } else {

      //sort networks
      int indices[n];
      for (int i = 0; i < n; i++) {
        indices[i] = i;
      }

      // RSSI SORT

      // old sort
      for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
          if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) {
            std::swap(indices[i], indices[j]);
          }
        }
      }

      /*std::sort(indices, indices + n, [](const int & a, const int & b) -> bool
        {
        return WiFi.RSSI(a) > WiFi.RSSI(b);
        });*/

      // remove duplicates ( must be RSSI sorted )
      if (_removeDuplicateAPs) {
        String cssid;
        for (int i = 0; i < n; i++) {
          if (indices[i] == -1) continue;
          cssid = WiFi.SSID(indices[i]);
          for (int j = i + 1; j < n; j++) {
            if (cssid == WiFi.SSID(indices[j])) {
              DEBUG_WC("DUP AP: " + WiFi.SSID(indices[j]));
              indices[j] = -1; // set dup aps to index -1
            }
          }
        }
      }

      //display networks in page
      for (int i = 0; i < n; i++) {
        if (indices[i] == -1) continue; // skip dups
        DEBUG_WC(WiFi.SSID(indices[i]));
        DEBUG_WC(WiFi.RSSI(indices[i]));
        int quality = getRSSIasQuality(WiFi.RSSI(indices[i]));

        if (_minimumQuality == -1 || _minimumQuality < quality) {
          String item = FPSTR(AP_HTTP_ITEM);
          String rssiQ;
          rssiQ += quality;
          item.replace("{v}", WiFi.SSID(indices[i]));
          item.replace("{r}", rssiQ);
#if defined(ESP8266)
          if (WiFi.encryptionType(indices[i]) != ENC_TYPE_NONE) {
#else
          if (WiFi.encryptionType(indices[i]) != WIFI_AUTH_OPEN) {
#endif
            item.replace("{i}", "l");
          } else {
            item.replace("{i}", "");
          }
          //DEBUG_WM(item);
          page += item;
          delay(0);
        } else {
          DEBUG_WC(F("Skipping due to quality"));
        }

      }
      page += "<br/>";
    }
  }

  page += FPSTR(AP_HTTP_FORM_START);
  char parLength[4];
  // add the extra parameters to the form
  for (int i = 0; i < _paramsCount; i++) {
    if (_params[i] == NULL) {
      break;
    }

    String pitem = FPSTR(AP_HTTP_FORM_PARAM);
    if (_params[i]->getID() != NULL) {
      pitem.replace("{i}", _params[i]->getID());
      pitem.replace("{n}", _params[i]->getID());
      pitem.replace("{p}", _params[i]->getPlaceholder());
      snprintf(parLength, 4, "%d", _params[i]->getValueLength());
      pitem.replace("{l}", parLength);
      pitem.replace("{v}", _params[i]->getValue());
      pitem.replace("{c}", _params[i]->getCustomHTML());
    } else {
      pitem = _params[i]->getCustomHTML();
    }

    page += pitem;
  }
  if (_params[0] != NULL) {
    page += "<br/>";
  }

  if (_sta_static_ip) {

    String item = FPSTR(AP_HTTP_FORM_PARAM);
    item.replace("{i}", "ip");
    item.replace("{n}", "ip");
    item.replace("{p}", "Static IP");
    item.replace("{l}", "15");
    item.replace("{v}", _sta_static_ip.toString());

    page += item;

    item = FPSTR(AP_HTTP_FORM_PARAM);
    item.replace("{i}", "gw");
    item.replace("{n}", "gw");
    item.replace("{p}", "Static Gateway");
    item.replace("{l}", "15");
    item.replace("{v}", _sta_static_gw.toString());

    page += item;

    item = FPSTR(AP_HTTP_FORM_PARAM);
    item.replace("{i}", "sn");
    item.replace("{n}", "sn");
    item.replace("{p}", "Subnet");
    item.replace("{l}", "15");
    item.replace("{v}", _sta_static_sn.toString());

    page += item;

    page += "<br/>";
  }

  page += FPSTR(AP_HTTP_FORM_END);
  page += FPSTR(AP_HTTP_SCAN_LINK);

  page += FPSTR(AP_HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);
}
void WiFiConnect::handleWifiSave() {
  _lastAPPage = millis();
  DEBUG_WC(F("Sending WiFi Save"));
  //SAVE/connect here
  if (_debug) {
    Serial.println(F("*WC Form Args"));
    for (int x = 0; x < server->args(); x++) {
      Serial.print(F("*WC "));
      Serial.print(server->argName(x));
      Serial.print(F("="));
      Serial.println(server->arg(x));
    }
  }
  _ssid = server->arg("s").c_str();
  _ssid.trim();
  DEBUG_WC(_ssid);
  _password = server->arg("p").c_str();
  _password.trim();
  DEBUG_WC(_password);
  //parameters
  for (int i = 0; i < _paramsCount; i++) {
    if (_params[i] == NULL) {
      break;
    }
    //read parameter
    String value = server->arg(_params[i]->getID()).c_str();
    //store it in array
    value.toCharArray(_params[i]->_value, _params[i]->_length);
    DEBUG_WC(F("Parameter"));
    DEBUG_WC(_params[i]->getID());
    DEBUG_WC(value);
  }

  if (server->arg("ip") != "") {
    DEBUG_WC(F("static ip"));
    DEBUG_WC(server->arg("ip"));
    //_sta_static_ip.fromString(server->arg("ip"));
    String ip = server->arg("ip");
    optionalIPFromString(&_sta_static_ip, ip.c_str());
  }
  if (server->arg("gw") != "") {
    DEBUG_WC(F("static gateway"));
    DEBUG_WC(server->arg("gw"));
    String gw = server->arg("gw");
    optionalIPFromString(&_sta_static_gw, gw.c_str());
  }
  if (server->arg("sn") != "") {
    DEBUG_WC(F("static netmask"));
    DEBUG_WC(server->arg("sn"));
    String sn = server->arg("sn");
    optionalIPFromString(&_sta_static_sn, sn.c_str());
  }

  String page = FPSTR(AP_HTTP_HEAD);
  page.replace("{v}", "Credentials Saved");
  page += FPSTR(AP_HTTP_SCRIPT);
  page += FPSTR(AP_HTTP_STYLE);
  page += FPSTR(AP_HTTP_HEAD_END);
  page += FPSTR(AP_HTTP_SAVED);
  page.replace("{ap}", _ssid);
  page += FPSTR(AP_HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);
  _readyToConnect = true;
}
/**************************************************************************/
/*!
    @brief  Handles the web request and shows basic information about the chip.
            TODO: ESP32 information still needs completing.
*/
/**************************************************************************/
void WiFiConnect::handleInfo() {
  _lastAPPage = millis();  DEBUG_WC(F("Sending Info"));

  String page = FPSTR(AP_HTTP_HEAD);
  page.replace("{v}", "Info");
  page += FPSTR(AP_HTTP_SCRIPT);
  page += FPSTR(AP_HTTP_STYLE);
  page += FPSTR(AP_HTTP_HEAD_END);
  page += F("<dl>");
  page += F("<dt>Chip ID</dt><dd>");
  page += ESP_getChipId();
  page += F("</dd>");
  page += F("<dt>Flash Chip ID</dt><dd>");
#if defined(ESP8266)
  page += ESP.getFlashChipId();
#else
  // TODO
  page += F("TODO");
#endif
  page += F("</dd>");
  page += F("<dt>IDE Flash Size</dt><dd>");
  page += ESP.getFlashChipSize();
  page += F(" bytes</dd>");
  page += F("<dt>Real Flash Size</dt><dd>");
#if defined(ESP8266)
  page += ESP.getFlashChipRealSize();
#else
  // TODO
  page += F("TODO");
#endif
  page += F(" bytes</dd>");
  page += F("<dt>Soft AP IP</dt><dd>");
  page += WiFi.softAPIP().toString();
  page += F("</dd>");
  page += F("<dt>Soft AP MAC</dt><dd>");
  page += WiFi.softAPmacAddress();
  page += F("</dd>");
  page += F("<dt>Station MAC</dt><dd>");
  page += WiFi.macAddress();
  page += F("</dd>");
  page += F("</dl>");
  page += FPSTR(AP_HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);

  DEBUG_WC(F("Sent info page"));
}
/**************************************************************************/
/*!
    @brief  Handles the reset web request and and restarts the chip.
            Does not clear settings.
*/
/**************************************************************************/
void WiFiConnect::handleReset() {
  _lastAPPage = millis();
  DEBUG_WC(F("Sending Reset"));

  String page = FPSTR(AP_HTTP_HEAD);
  page.replace("{v}", "Info");
  page += FPSTR(AP_HTTP_SCRIPT);
  page += FPSTR(AP_HTTP_STYLE);
  page += FPSTR(AP_HTTP_HEAD_END);
  page += F("Module will reset in a few seconds.");
  page += FPSTR(AP_HTTP_END);

  server->sendHeader("Content-Length", String(page.length()));
  server->send(200, "text/html", page);

  DEBUG_WC(F("Sent reset page"));
  delay(5000);
#if defined(ESP8266)
  ESP.reset(); // causes boot loop 
#else
  ESP.restart();
#endif
  delay(2000);

}
/**************************************************************************/
/*!
    @brief  Handles the 204 web request from Android devices
*/
/**************************************************************************/
void WiFiConnect::handle204() {
  _lastAPPage = millis();
  DEBUG_WC(F("Sending 204"));
  handleRoot();
}
/**************************************************************************/
/*!
    @brief  Handles any web requests that are not found.
*/
/**************************************************************************/
void WiFiConnect::handleNotFound() {
  if (captivePortal()) { // If captive portal redirect instead of displaying the error page.
    DEBUG_WC(F("Webserver called handleNotFound();"));  
    return;
  }
  _lastAPPage = millis();
  DEBUG_WC(F("Sending Not Found"));
  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->sendHeader("Cache-Control", "no-cache, no-store, must-revalidate");
  server->sendHeader("Pragma", "no-cache");
  server->sendHeader("Expires", "-1");
  server->sendHeader("Content-Length", String(message.length()));
  server->send ( 404, "text/plain", message );
}

/**************************************************************************/
/*!
    @brief  Redirect to captive portal if we get a request for another domain.
            Return true in that case so the page handler do not try to
            handle the request again.
    @return True if we have redirected to our portal, else false and contimue
            to handle request
*/
/**************************************************************************/
boolean WiFiConnect::captivePortal() {
	DEBUG_WC(server->hostHeader());
	
  if (!isIp(server->hostHeader()) ) {
    _lastAPPage = millis();
    DEBUG_WC(F("Request redirected to captive portal"));
    String msg = "redirect\n";
    server->sendHeader("Location", String("http://") + toStringIp(server->client().localIP()), true);
    server->sendHeader("Content-Length", String(msg.length()));
    server->send ( 302, "text/plain", msg); // Empty content inhibits Content-length header so we have to close the socket ourselves.
    //server->client().stop(); // Stop is needed because we sent no content length

    return true;
  }
  return false;
}

/**************************************************************************/
/*!
    @brief  Sets the function to be called when we enter Access Point
            for configuration.
            Gets passed a the WiFiConnect for additional usage within function.
    @param  func
            The function to be called.

*/
/**************************************************************************/
void WiFiConnect::setAPCallback( void (*func)(WiFiConnect* myWiFiConnect) ) {
  _apcallback = func;
}
/**************************************************************************/
/*!
    @brief  Sets the function to be called when we need to save the
            custom parameters/configuration.
    @param  func
            The function to be called.
            This function should be short running and ideally just set.
            a flag for processing else where.
*/
/**************************************************************************/
void WiFiConnect::setSaveConfigCallback( void (*func)() ) {
  _savecallback = func;
}
/**************************************************************************/
/*!
    @brief  Function to convert RSSI into a usable range 0-100 so we can
            use it to exclude poor networks.
    @param  RSSI
            The quality/signal strength of the WiFi network.
    @return Value in the range of 0-100 to network indicate quality.

*/
/**************************************************************************/
int WiFiConnect::getRSSIasQuality(int RSSI) {
  int quality = 0;

  if (RSSI <= -100) {
    quality = 0;
  } else if (RSSI >= -50) {
    quality = 100;
  } else {
    quality = 2 * (RSSI + 100);
  }
  return quality;
}
/**************************************************************************/
/*!
    @brief  Function to check if a given string is an IP Address.
    @param  str
            The string to process.
    @return True if inputted string is an IP Address.

*/
/**************************************************************************/
boolean WiFiConnect::isIp(String str) {
  for (unsigned int i = 0; i < str.length(); i++) {
    int c = str.charAt(i);
    if (c != '.' && (c < '0' || c > '9')) {
      return false;
    }
  }
  return true;
}
/**************************************************************************/
/*!
    @brief  Function to return a IPAddress as a string representation.
    @param  ip
            The IPAddress to process.
    @return String version of the IPAddress

*/
/**************************************************************************/
String WiFiConnect::toStringIp(IPAddress ip) {
  String res = "";
  for (int i = 0; i < 3; i++) {
    res += String((ip >> (8 * i)) & 0xFF) + ".";
  }
  res += String(((ip >> 8 * 3)) & 0xFF);
  return res;
}
/**************************************************************************/
/*!
    @brief  Used to turn on or off messages to the serial port/monitor.
    @param  isDebug
            Set to true to enable messages.
*/
/**************************************************************************/
void WiFiConnect::setDebug(boolean isDebug) {
  _debug = isDebug;
}

/**************************************************************************/
/*!
    @brief  Used to set a static ip/gateway and subnet mask for the access point.
            IP and DNS Gateway are normally the same for a captive portal.
    @param  ip
            The IP address that you want to access the portal through.
    @param  gw
            The gateway address for the DNS server, usually the same as IP for
            captive portal.
    @param  sn
            The subnet mask for the access point, usually 255.255.255.0
*/
/**************************************************************************/
void          WiFiConnect::setAPStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn) {
  _ap_static_ip = ip;
  _ap_static_gw = gw;
  _ap_static_sn = sn;
}
/**************************************************************************/
/*!
    @brief  Used to set a static ip/gateway and subnet mask for the local
            network.
    @param  ip
            The IP address that you want to use on the network.
    @param  gw
            The gateway address for the DNS server or network router.
    @param  sn
            The subnet mask for the access point, usually 255.255.255.0
            for local networks.
*/
/**************************************************************************/
void          WiFiConnect::setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn) {
  _sta_static_ip = ip;
  _sta_static_gw = gw;
  _sta_static_sn = sn;
}
