/**
 * WiFiManager.h
 * 
 * WiFiManager, a library for the ESP8266/Arduino platform
 * for configuration of WiFi credentials using a Captive Portal
 * 
 * @author Creator tzapu
 * @author tablatronix
 * @version 0.0.0
 * @license MIT
 */


#ifndef WiFiManager_h
#define WiFiManager_h

#if defined(ESP8266) || defined(ESP32)

#ifdef ESP8266
#include <core_version.h>
#endif

#include <vector>

// #define WM_MDNS            // also set MDNS with sethostname
// #define WM_FIXERASECONFIG  // use erase flash fix
// #define WM_ERASE_NVS       // esp32 erase(true) will erase NVS 
// #define WM_RTC             // esp32 info page will include reset reasons

#ifdef ARDUINO_ESP8266_RELEASE_2_3_0
#warning "ARDUINO_ESP8266_RELEASE_2_3_0, some WM features disabled" 
#define WM_NOASYNC         // esp8266 no async scan wifi
#endif

// #include "soc/efuse_reg.h" // include to add efuse chip rev to info, getChipRevision() is almost always the same though, so not sure why it matters.

// #define esp32autoreconnect    // implement esp32 autoreconnect event listener kludge, @DEPRECATED
// autoreconnect is WORKING https://github.com/espressif/arduino-esp32/issues/653#issuecomment-405604766

#define WM_WEBSERVERSHIM      // use webserver shim lib

#ifdef ESP8266

    extern "C" {
      #include "user_interface.h"
    }
    #include <ESP8266WiFi.h>
    #include <ESP8266WebServer.h>

    #ifdef WM_MDNS
        #include <ESP8266mDNS.h>
    #endif

    #define WIFI_getChipId() ESP.getChipId()
    #define WM_WIFIOPEN   ENC_TYPE_NONE

#elif defined(ESP32)

    #include <WiFi.h>
    #include <esp_wifi.h>  
    
    #define WIFI_getChipId() (uint32_t)ESP.getEfuseMac()
    #define WM_WIFIOPEN   WIFI_AUTH_OPEN

    #ifndef WEBSERVER_H
        #ifdef WM_WEBSERVERSHIM
            #include <WebServer.h>
        #else
            #include <ESP8266WebServer.h>
            // Forthcoming official ?
            // https://github.com/esp8266/ESPWebServer
        #endif
    #endif

    #ifdef WM_ERASE_NVS
       #include <nvs.h>
       #include <nvs_flash.h>
    #endif

    #ifdef WM_MDNS
        #include <ESPmDNS.h>
    #endif

    #ifdef WM_RTC
        #include <rom/rtc.h>
    #endif


#else
#endif

#include <DNSServer.h>
#include <memory>
#include "strings_en.h"

#ifndef WIFI_MANAGER_MAX_PARAMS
    #define WIFI_MANAGER_MAX_PARAMS 5 // params will autoincrement and realloc by this amount when max is reached
#endif

#define WFM_LABEL_BEFORE 1
#define WFM_LABEL_AFTER 2
#define WFM_NO_LABEL 0

class WiFiManagerParameter {
  public:
    /** 
        Create custom parameters that can be added to the WiFiManager setup web page
        @id is used for HTTP queries and must not contain spaces nor other special characters
    */
    WiFiManagerParameter();
    WiFiManagerParameter(const char *custom);
    WiFiManagerParameter(const char *id, const char *label);
    WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length);
    WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom);
    WiFiManagerParameter(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement);
    ~WiFiManagerParameter();

    const char *getID();
    const char *getValue();
    const char *getLabel();
    const char *getPlaceholder(); // @deprecated, use getLabel
    int         getValueLength();
    int         getLabelPlacement();
    const char *getCustomHTML();
    void        setValue(const char *defaultValue, int length);

  protected:
    void init(const char *id, const char *label, const char *defaultValue, int length, const char *custom, int labelPlacement);

  private:
    const char *_id;
    const char *_label;
    char       *_value;
    int         _length;
    int         _labelPlacement;
    const char *_customHTML;

    friend class WiFiManager;
};


class WiFiManager
{
  public:
    WiFiManager(Stream& consolePort);
    WiFiManager();
    ~WiFiManager();
    void WiFiManagerInit();

    // auto connect to saved wifi, or custom, and start config portal on failures
    boolean       autoConnect();
    boolean       autoConnect(char const *apName, char const *apPassword = NULL);

    //manually start the config portal, autoconnect does this automatically on connect failure
    boolean       startConfigPortal(); // auto generates apname
    boolean       startConfigPortal(char const *apName, char const *apPassword = NULL);

    //manually stop the config portal if started manually, stop immediatly if non blocking, flag abort if blocking
    bool          stopConfigPortal();
    
    //manually start the web portal, autoconnect does this automatically on connect failure    
    void          startWebPortal();
    //manually stop the web portal if started manually
    void          stopWebPortal();
    // Run webserver processing, if setConfigPortalBlocking(false)
    boolean       process();

    // get the AP name of the config portal, so it can be used in the callback
    String        getConfigPortalSSID();
    int           getRSSIasQuality(int RSSI);

    // erase wifi credentials
    void          resetSettings();
    // reboot esp
    void          reboot();
    // disconnect wifi, without persistent saving or erasing
    bool          disconnect();
    // erase esp
    bool          erase();
    bool          erase(bool opt);

    //adds a custom parameter, returns false on failure
    bool          addParameter(WiFiManagerParameter *p);
    //returns the list of Parameters
    WiFiManagerParameter** getParameters();
    // returns the Parameters Count
    int           getParametersCount();


    // SET CALLBACKS

    //called after AP mode and config portal has started
    void          setAPCallback( std::function<void(WiFiManager*)> func );
    //called after webserver has started
    void          setWebServerCallback( std::function<void()> func );
    //called when settings reset have been triggered
    void          setConfigResetCallback( std::function<void()> func );
    //called when wifi settings have been changed and connection was successful ( or setBreakAfterConfig(true) )
    void          setSaveConfigCallback( std::function<void()> func );
    //called when settings have been changed and connection was successful
    void          setSaveParamsCallback( std::function<void()> func );
    //called when settings before have been changed and connection was successful
    void          setPreSaveConfigCallback( std::function<void()> func );


    //sets timeout before AP,webserver loop ends and exits even if there has been no setup.
    //useful for devices that failed to connect at some point and got stuck in a webserver loop
    //in seconds setConfigPortalTimeout is a new name for setTimeout, ! not used if setConfigPortalBlocking
    void          setConfigPortalTimeout(unsigned long seconds);
    void          setTimeout(unsigned long seconds); // @deprecated, alias

    //sets timeout for which to attempt connecting, useful if you get a lot of failed connects
    void          setConnectTimeout(unsigned long seconds);
    //sets timeout for which to attempt connecting on saves, useful if there are bugs in esp waitforconnectloop
    void          setSaveConnectTimeout(unsigned long seconds);
    // toggle debug output
    void          setDebugOutput(boolean debug);
    //set min quality percentage to include in scan, defaults to 8% if not specified
    void          setMinimumSignalQuality(int quality = 8);
    //sets a custom ip /gateway /subnet configuration
    void          setAPStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn);
    //sets config for a static IP
    void          setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn);
    //sets config for a static IP with DNS
    void          setSTAStaticIPConfig(IPAddress ip, IPAddress gw, IPAddress sn, IPAddress dns);
    //if this is set, it will exit after config, even if connection is unsuccessful.
    void          setBreakAfterConfig(boolean shouldBreak);
    // if this is set, portal will be blocking and wait until save or exit, 
    // is false user must manually `process()` to handle config portal,
    // setConfigPortalTimeout is ignored in this mode, user is responsible for closing configportal
    void          setConfigPortalBlocking(boolean shouldBlock);
    //if this is set, customise style
    void          setCustomHeadElement(const char* element);
    //if this is true, remove duplicated Access Points - defaut true
    void          setRemoveDuplicateAPs(boolean removeDuplicates);
    //setter for ESP wifi.persistent so we can remember it and restore user preference, as WIFi._persistent is protected
    void          setRestorePersistent(boolean persistent);
    //if true, always show static net inputs, IP, subnet, gateway, else only show if set via setSTAStaticIPConfig
    void          setShowStaticFields(boolean alwaysShow);
    //if true, always show static dns, esle only show if set via setSTAStaticIPConfig
    void          setShowDnsFields(boolean alwaysShow);
    // toggle showing the saved wifi password in wifi form, could be a security issue.
    void          setShowPassword(boolean show);
    //if false, disable captive portal redirection
    void          setCaptivePortalEnable(boolean enabled);
    //if false, timeout captive portal even if a STA client connected to softAP (false), suggest disabling if captiveportal is open
    void          setAPClientCheck(boolean enabled);
    //if true, reset timeout when webclient connects (true), suggest disabling if captiveportal is open    
    void          setWebPortalClientCheck(boolean enabled);
    // if true, enable autoreconnecting
    void          setWiFiAutoReconnect(boolean enabled);
    // if true, wifiscan will show percentage instead of quality icons, until we have better templating
    void          setScanDispPerc(boolean enabled);
    // if true (default) then start the config portal from autoConnect if connection failed
    void          setEnableConfigPortal(boolean enable);
    // set a custom hostname, sets sta and ap dhcp client id for esp32, and sta for esp8266
    bool          setHostname(const char * hostname);
    // show erase wifi onfig button on info page, true
    void          setShowInfoErase(boolean enabled);
    // set ap channel
    void          setWiFiAPChannel(int32_t channel);
    // set ap hidden
    void          setWiFiAPHidden(bool hidden); // default false
    // clean connect, always disconnect before connecting
    void          setCleanConnect(bool enable); // default false

    // set custom menu
    // set custom menu items and order
    void          setMenu(std::vector<const char*>& menu);
    void          setMenu(const char* menu[], uint8_t size);
    
    // add params to its own menu page and remove from wifi, NOT TO BE COMBINED WITH setMenu!
    void          setParamsPage(bool enable);

    // get last connection result, includes autoconnect and wifisave
    uint8_t       getLastConxResult();
    // get a status as string
    String        getWLStatusString(uint8_t status);    
    String        getModeString(uint8_t mode);
    // check if the module has a saved ap to connect to
    bool          getWiFiIsSaved();

    // helper to get saved ssid, if persistent get stored, else get current if connected
    String        getWiFiPass(bool persistent = false);
    // helper to get saved password, if persistent get stored, else get current if connected
    String        getWiFiSSID(bool persistent = false);

    // debug output the softap config
    void          debugSoftAPConfig();
    // debug output platform info and versioning
    void          debugPlatformInfo();
    String        htmlEntities(String str);
    
    // set the country code for wifi settings
    void          setCountry(String cc);
    // set body class (invert)
    void          setClass(String str);
    String        getDefaultAPName();
    
    std::unique_ptr<DNSServer>        dnsServer;

    #if defined(ESP32) && defined(WM_WEBSERVERSHIM)
        using WM_WebServer = WebServer;
    #else
        using WM_WebServer = ESP8266WebServer;
    #endif
    
    std::unique_ptr<WM_WebServer> server;

  private:
    std::vector<uint8_t> _menuIds;
    std::vector<const char *> _menuIdsParams  = {"wifi","param","info","exit"};
    std::vector<const char *> _menuIdsDefault = {"wifi","info","exit"};

    // ip configs @todo struct ?
    IPAddress     _ap_static_ip;
    IPAddress     _ap_static_gw;
    IPAddress     _ap_static_sn;
    IPAddress     _sta_static_ip;
    IPAddress     _sta_static_gw;
    IPAddress     _sta_static_sn;
    IPAddress     _sta_static_dns;

    // defaults
    const byte    DNS_PORT                = 53;
    const byte    HTTP_PORT               = 80;
    String        _apName                 = "no-net";
    String        _apPassword             = "";
    String        _ssid                   = "";
    String        _pass                   = "";
    
    // options flags
    unsigned long _configPortalTimeout    = 0; // ms close config portal loop if set (depending on  _cp/webClientCheck options)
    unsigned long _connectTimeout         = 0; // ms stop trying to connect to ap if set
    unsigned long _saveTimeout            = 0; // ms stop trying to connect to ap on saves, in case bugs in esp waitforconnectresult
    unsigned long _configPortalStart      = 0; // ms config portal start time (updated for timeouts)
    unsigned long _webPortalAccessed      = 0; // ms last web access time
    WiFiMode_t    _usermode               = WIFI_OFF;
    String        _wifissidprefix         = FPSTR(S_ssidpre); // auto apname prefix prefix+chipid
    uint8_t       _lastconxresult         = WL_IDLE_STATUS;
    int           _numNetworks            = 0;
    unsigned long _lastscan               = 0; // ms for timing wifi scans
    unsigned long _startscan              = 0; // ms for timing wifi scans
    int           _cpclosedelay           = 2000; // delay before wifisave, prevents captive portal from closing to fast.
    bool          _cleanConnect           = false; // disconnect before connect in connectwifi, increases stability on connects
   
    bool          _disableSTA             = false; // disable sta when starting ap, always
    bool          _disableSTAConn         = true;  // disable sta when starting ap, if sta is not connected ( stability )
    bool          _channelSync            = false; // use same wifi sta channel when starting ap
    int32_t       _apChannel              = 0; // channel to use for ap
    bool          _apHidden               = false; // store softap hidden value

    #ifdef ESP32
    static uint8_t _lastconxresulttmp; // tmp var for esp32 callback
    #endif

    #ifndef WL_STATION_WRONG_PASSWORD
    uint8_t WL_STATION_WRONG_PASSWORD     = 7; // @kludge define a WL status for wrong password
    #endif

    // parameter options
    int           _minimumQuality         = -1;    // filter wifiscan ap by this rssi
    int            _staShowStaticFields   = 0;     // ternary 1=always show static ip fields, 0=only if set, -1=never(cannot change ips via web!)
    int            _staShowDns            = 0;     // ternary 1=always show dns, 0=only if set, -1=never(cannot change dns via web!)
    boolean       _removeDuplicateAPs     = true;  // remove dup aps from wifiscan
    boolean       _showPassword           = false; // show or hide saved password on wifi form, might be a security issue!
    boolean       _shouldBreakAfterConfig = false; // stop configportal on save failure
    boolean       _configPortalIsBlocking = true;  // configportal enters blocking loop 
    boolean       _enableCaptivePortal    = true;  // enable captive portal redirection
    boolean       _userpersistent         = true;  // users preffered persistence to restore
    boolean       _wifiAutoReconnect      = true;  // there is no platform getter for this, we must assume its true and make it so
    boolean       _apClientCheck          = false; // keep cp alive if ap have station
    boolean       _webClientCheck         = true;  // keep cp alive if web have client
    boolean       _scanDispOptions        = false; // show percentage in scans not icons
    boolean       _paramsInWifi           = true;  // show custom parameters on wifi page
    boolean       _showInfoErase          = true;  // info page erase button
    boolean       _enableConfigPortal     = true;  // use config portal if autoconnect failed
    const char *  _hostname               = "";

    const char*   _customHeadElement      = ""; // store custom head element html from user
    String        _bodyClass              = ""; // class to add to body

    // internal options
    boolean       _preloadwifiscan        = true;  // preload wifiscan if true
    boolean       _asyncScan              = false;
    unsigned int  _scancachetime          = 30000; // ms cache time for background scans
    boolean       _disableIpFields        = false; // modify function of setShow_X_Fields(false), forces ip fields off instead of default show if set, eg. _staShowStaticFields=-1

    String        _wificountry            = "";  // country code, @todo define in strings lang

    // wrapper functions for handling setting and unsetting persistent for now.
    bool          esp32persistent         = false;
    bool          _hasBegun               = false;
    void          _begin();
    void          _end();

    void          setupConfigPortal();
    bool          shutdownConfigPortal();

#ifdef NO_EXTRA_4K_HEAP
    boolean       _tryWPS                 = false; // try WPS on save failure, unsupported
    void          startWPS();
#endif

    bool          startAP();

    uint8_t       connectWifi(String ssid, String pass);
    bool          setSTAConfig();
    bool          wifiConnectDefault();
    bool          wifiConnectNew(String ssid, String pass);

    uint8_t       waitForConnectResult();
    uint8_t       waitForConnectResult(uint16_t timeout);
    void          updateConxResult(uint8_t status);

    // webserver handlers
    void          handleRoot();
    void          handleWifi(boolean scan);
    void          handleWifiSave();
    void          handleInfo();
    void          handleReset();
    void          handleNotFound();
    void          handleExit();
    void          handleClose();
    // void          handleErase();
    void          handleErase(boolean opt);
    void          handleParam();
    void          handleWiFiStatus();
    void          handleRequest();
    void          handleParamSave();
    void          doParamSave();

    boolean       captivePortal();
    boolean       configPortalHasTimeout();
    uint8_t       processConfigPortal();
    void          stopCaptivePortal();

    // wifi platform abstractions
    bool          WiFi_Mode(WiFiMode_t m);
    bool          WiFi_Mode(WiFiMode_t m,bool persistent);
    bool          WiFi_Disconnect();
    bool          WiFi_enableSTA(bool enable);
    bool          WiFi_enableSTA(bool enable,bool persistent);
    bool          WiFi_eraseConfig();
    uint8_t       WiFi_softap_num_stations();
    bool          WiFi_hasAutoConnect();
    void          WiFi_autoReconnect();
    String        WiFi_SSID(bool persistent = false) const;
    String        WiFi_psk(bool persistent = false) const;
    bool          WiFi_scanNetworks();
    bool          WiFi_scanNetworks(bool force,bool async);
    bool          WiFi_scanNetworks(unsigned int cachetime,bool async);
    bool          WiFi_scanNetworks(unsigned int cachetime);
    void          WiFi_scanComplete(int networksFound);
    bool          WiFiSetCountry();

    #ifdef ESP32
    void   WiFiEvent(WiFiEvent_t event, system_event_info_t info);
    #endif

    // output helpers
    String        getParamOut();
    String        getIpForm(String id, String title, String value);
    String        getScanItemOut();
    String        getStaticOut();
    String        getHTTPHead(String title);
    String        getMenuOut();
    //helpers
    boolean       isIp(String str);
    String        toStringIp(IPAddress ip);
    boolean       validApPassword();
    String        encryptionTypeStr(uint8_t authmode);
    void          reportStatus(String &page);
    String        getInfoData(String id);

    // flags
    boolean       connect;
    boolean       abort;
    boolean       reset               = false;
    boolean       configPortalActive  = false;
    boolean       webPortalActive     = false;
    boolean       portalTimeoutResult = false;
    boolean       portalAbortResult   = false;
    boolean       storeSTAmode        = true; // option store persistent STA mode in connectwifi 
    int           timer               = 0;
    
    // WiFiManagerParameter
    int         _paramsCount          = 0;
    int         _max_params;
    WiFiManagerParameter** _params    = NULL;

    // debugging
    typedef enum {
        DEBUG_ERROR     = 0,
        DEBUG_NOTIFY    = 1, // default stable
        DEBUG_VERBOSE   = 2,
        DEBUG_DEV       = 3, // default dev
        DEBUG_MAX       = 4
    } wm_debuglevel_t;

    boolean _debug  = true;
    
    // build debuglevel support
    // @todo use DEBUG_ESP_x?
    #ifdef WM_DEBUG_LEVEL
    uint8_t _debugLevel = (uint8_t)WM_DEBUG_LEVEL;
    #else 
    uint8_t _debugLevel = DEBUG_DEV; // default debug level
    #endif

    // @todo use DEBUG_ESP_PORT ?
    #ifdef WM_DEBUG_PORT
    Stream& _debugPort = WM_DEBUG_PORT;
    #else
    Stream& _debugPort = Serial; // debug output stream ref
    #endif

    template <typename Generic>
    void        DEBUG_WM(Generic text);

    template <typename Generic>
    void        DEBUG_WM(wm_debuglevel_t level,Generic text);
    template <typename Generic, typename Genericb>
    void        DEBUG_WM(Generic text,Genericb textb);
    template <typename Generic, typename Genericb>
    void        DEBUG_WM(wm_debuglevel_t level, Generic text,Genericb textb);

    // callbacks
    // @todo use cb list (vector) maybe event ids, allow no return value
    std::function<void(WiFiManager*)> _apcallback;
    std::function<void()> _webservercallback;
    std::function<void()> _savewificallback;
    std::function<void()> _presavecallback;
    std::function<void()> _saveparamscallback;
    std::function<void()> _resetcallback;

    template <class T>
    auto optionalIPFromString(T *obj, const char *s) -> decltype(  obj->fromString(s)  ) {
      return  obj->fromString(s);
    }
    auto optionalIPFromString(...) -> bool {
      // DEBUG_WM("NO fromString METHOD ON IPAddress, you need ESP8266 core 2.1.0 or newer for Custom IP configuration to work.");
      return false;
    }
};

#endif

#endif