/**
 *
 * @file WiFiMulti.cpp
 * @date 16.05.2015
 * @author Markus Sattler
 *
 * Copyright (c) 2015 Markus Sattler. All rights reserved.
 * This file is part of the esp8266 core for Arduino environment.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#include "WiFiMulti.h"
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED
#include <limits.h>
#include <string.h>
#include <esp32-hal.h>

WiFiMulti::WiFiMulti() {
  ipv6_support = false;
}

void WiFiMulti::APlistClean(void) {
  for (auto entry : APlist) {
    if (entry.ssid) {
      free(entry.ssid);
    }
    if (entry.passphrase) {
      free(entry.passphrase);
    }
#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT
    if (entry.username) {
      free(entry.username);
    }
    if (entry.identity) {
      free(entry.identity);
    }
#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */
  }
  APlist.clear();
}

WiFiMulti::~WiFiMulti() {
  APlistClean();
}

#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT
bool WiFiMulti::addAP(const char *ssid, const char *passphrase, const char *username, const char *identity) {
#else
bool WiFiMulti::addAP(const char *ssid, const char *passphrase) {
#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */
  WifiAPlist_t newAP;

  if (!ssid || *ssid == '\0' || strlen(ssid) > 31) {
    // fail SSID too long or missing!
    log_e("[WIFI][APlistAdd] no ssid or ssid too long");
    return false;
  }

  if (passphrase && strlen(passphrase) > 63) {
    // fail passphrase too long!
    log_e("[WIFI][APlistAdd] passphrase too long");
    return false;
  }

#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT
  if (username && strlen(username) > 63) {
    // fail username too long!
    log_e("[WIFI][APlistAdd] username too long");
    return false;
  }

  if (identity && strlen(identity) > 63) {
    // fail identity too long!
    log_e("[WIFI][APlistAdd] identity too long");
    return false;
  }
#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */

  newAP.ssid = strdup(ssid);

  if (!newAP.ssid) {
    log_e("[WIFI][APlistAdd] fail newAP.ssid == 0");
    return false;
  }

  if (passphrase && *passphrase != '\0') {
    newAP.passphrase = strdup(passphrase);
    if (!newAP.passphrase) {
      log_e("[WIFI][APlistAdd] fail newAP.passphrase == 0");
      free(newAP.ssid);
      return false;
    }
  } else {
    newAP.passphrase = NULL;
  }

#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT
  if (username && *username != '\0') {
    newAP.username = strdup(username);
    if (!newAP.username) {
      log_e("[WIFI][APlistAdd] fail newAP.username == 0");
      free(newAP.ssid);
      if (newAP.passphrase) {
        free(newAP.passphrase);
      }
      return false;
    }
  } else {
    newAP.username = NULL;
  }

  if (identity) {  // Allow empty identity as not all WPA2-Enterprise networks require it
    newAP.identity = strdup(identity);
    if (!newAP.identity) {
      log_e("[WIFI][APlistAdd] fail newAP.identity == 0");
      free(newAP.ssid);
      if (newAP.passphrase) {
        free(newAP.passphrase);
      }
      if (newAP.username) {
        free(newAP.username);
      }
      return false;
    }
  } else {
    newAP.identity = NULL;
  }
#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */

  newAP.hasFailed = false;
  APlist.push_back(newAP);
  log_i("[WIFI][APlistAdd] add SSID: %s", newAP.ssid);
  return true;
}

uint8_t WiFiMulti::run(uint32_t connectTimeout, bool scanHidden) {
  int8_t scanResult;
  unsigned long startTime;
  uint8_t status = WiFi.status();
  if (status == WL_CONNECTED) {
    if (!_bWFMInit && _connectionTestCBFunc != NULL) {
      if (_connectionTestCBFunc() == true) {
        _bWFMInit = true;
        return status;
      }
    } else {
      if (!_bStrict) {
        return status;
      } else {
        for (auto ap : APlist) {
          if (WiFi.SSID() == ap.ssid) {
            return status;
          }
        }
      }
    }
    WiFi.disconnect(false, false);
    delay(10);
    status = WiFi.status();
  }

  scanResult = WiFi.scanNetworks(false, scanHidden);
  if (scanResult == WIFI_SCAN_RUNNING) {
    // scan is running
    return WL_NO_SSID_AVAIL;
  } else if (scanResult >= 0) {
    // scan done analyze
    int32_t bestIndex = -1;
#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT
    WifiAPlist_t bestNetwork{NULL, NULL, NULL, NULL, false};
#else
    WifiAPlist_t bestNetwork{NULL, NULL, false};
#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */
    int bestNetworkDb = INT_MIN;
    int bestNetworkSec = WIFI_AUTH_MAX;
    uint8_t bestBSSID[6];
    int32_t bestChannel = 0;

    log_i("[WIFI] scan done");

    if (scanResult == 0) {
      log_e("[WIFI] no networks found");
    } else {
      log_i("[WIFI] %d networks found", scanResult);

      int8_t failCount = 0;
      int8_t foundCount = 0;
      for (int8_t i = 0; i < scanResult; ++i) {

        String ssid_scan;
        int32_t rssi_scan;
        uint8_t sec_scan;
        uint8_t *BSSID_scan;
        int32_t chan_scan;
        bool hidden_scan;

        WiFi.getNetworkInfo(i, ssid_scan, sec_scan, rssi_scan, BSSID_scan, chan_scan);
        hidden_scan = (ssid_scan.length() == 0) && scanHidden;
        // add any Open WiFi AP to the list, if allowed with setAllowOpenAP(true)
        if (_bAllowOpenAP && sec_scan == WIFI_AUTH_OPEN) {
          bool found = false;
          for (auto check : APlist) {
            if (ssid_scan == check.ssid) {
              found = true;
              break;
            }
          }
          // If we didn't find it, add this Open WiFi AP to the list
          if (!found) {
            log_i("[WIFI][APlistAdd] adding Open WiFi SSID: %s", ssid_scan.c_str());
            addAP(ssid_scan.c_str());
          }
        }

        if (hidden_scan) {
          log_v("hidden ssid on channel %" PRIi32 " found, trying to connect with known credentials...", chan_scan);
        }

        bool known = false;
        for (uint32_t x = 0; x < APlist.size(); x++) {
          WifiAPlist_t entry = APlist[x];

          if (ssid_scan == entry.ssid || hidden_scan) {  // SSID match or hidden network found
            if (!hidden_scan) {
              log_v("known ssid: %s, has failed: %s", entry.ssid, entry.hasFailed ? "yes" : "no");
              foundCount++;
            }
            if (!entry.hasFailed) {
              if (hidden_scan) {
                WiFi.begin(entry.ssid, entry.passphrase, chan_scan, BSSID_scan);

                // If the ssid returned from the scan is empty, it is a hidden SSID
                // it appears that the WiFi.begin() function is asynchronous and takes
                // additional time to connect to a hidden SSID. Therefore a delay of 1000ms
                // is added for hidden SSIDs before calling WiFi.status()
                delay(1000);

                status = WiFi.status();
                startTime = millis();

                while (status != WL_CONNECTED && (millis() - startTime) <= connectTimeout) {
                  delay(10);
                  status = WiFi.status();
                }

                WiFi.disconnect();
                delay(10);

                if (status == WL_CONNECTED) {
                  log_v("hidden ssid %s found", entry.ssid);
                  ssid_scan = entry.ssid;
                  foundCount++;
                } else {
                  continue;
                }
              }
              known = true;
              log_v("rssi_scan: %" PRIi32 ", bestNetworkDb: %d", rssi_scan, bestNetworkDb);
              if (rssi_scan > bestNetworkDb) {  // best network
#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT
                if (sec_scan == WIFI_AUTH_OPEN || ((sec_scan != WIFI_AUTH_WPA2_ENTERPRISE) && entry.passphrase)
                    || ((sec_scan == WIFI_AUTH_WPA2_ENTERPRISE) && entry.passphrase && entry.username && entry.identity)) {
#else
                if (sec_scan == WIFI_AUTH_OPEN || entry.passphrase) {  // check for passphrase if not open wlan
#endif
                  log_v("best network is now: %s", ssid_scan);
                  bestIndex = x;
                  bestNetworkSec = sec_scan;
                  bestNetworkDb = rssi_scan;
                  bestChannel = chan_scan;
                  memcpy((void *)&bestNetwork, (void *)&entry, sizeof(bestNetwork));
                  memcpy((void *)&bestBSSID, (void *)BSSID_scan, sizeof(bestBSSID));
                } else {
                  log_v("insufficient credentials for %s", ssid_scan);
                }
              }
              break;
            } else {
              failCount++;
            }
          }
        }

        if (known) {
          log_d(
            " --->   %d: [%" PRIi32 "][%02X:%02X:%02X:%02X:%02X:%02X] %s (%" PRIi32 ") (%c) (%s)", i, chan_scan, BSSID_scan[0], BSSID_scan[1], BSSID_scan[2],
            BSSID_scan[3], BSSID_scan[4], BSSID_scan[5], ssid_scan.c_str(), rssi_scan, (sec_scan == WIFI_AUTH_OPEN) ? ' ' : '*',
            (hidden_scan) ? "hidden" : "visible"
          );
        } else {
          log_d(
            "        %d: [%" PRIi32 "][%02X:%02X:%02X:%02X:%02X:%02X] %s (%" PRIi32 ") (%c) (%s)", i, chan_scan, BSSID_scan[0], BSSID_scan[1], BSSID_scan[2],
            BSSID_scan[3], BSSID_scan[4], BSSID_scan[5], ssid_scan.c_str(), rssi_scan, (sec_scan == WIFI_AUTH_OPEN) ? ' ' : '*',
            (hidden_scan) ? "hidden" : "visible"
          );
        }
      }
      log_v("foundCount = %d, failCount = %d", foundCount, failCount);
      // if all the APs in the list have failed, reset the failure flags
      if (foundCount == failCount) {
        resetFails();  // keeps trying the APs in the list
      }
    }
    // clean up ram
    WiFi.scanDelete();

    if (bestIndex >= 0) {
      log_i(
        "[WIFI] Connecting BSSID: %02X:%02X:%02X:%02X:%02X:%02X SSID: %s Channel: %" PRIi32 " (%d)", bestBSSID[0], bestBSSID[1], bestBSSID[2], bestBSSID[3],
        bestBSSID[4], bestBSSID[5], bestNetwork.ssid, bestChannel, bestNetworkDb
      );

#if CONFIG_LWIP_IPV6
      if (ipv6_support == true) {
        WiFi.enableIPv6();
      }
#endif
      WiFi.disconnect();
      delay(10);
#if CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT
      if (bestNetworkSec != WIFI_AUTH_WPA2_ENTERPRISE) {
        WiFi.begin(bestNetwork.ssid, (bestNetworkSec == WIFI_AUTH_OPEN) ? NULL : bestNetwork.passphrase, bestChannel, bestBSSID);
      } else {
        WiFi.begin(
          bestNetwork.ssid, WPA2_AUTH_PEAP, bestNetwork.identity, bestNetwork.username, bestNetwork.passphrase, NULL, NULL, NULL, -1, bestChannel, bestBSSID
        );
      }
#else
      WiFi.begin(bestNetwork.ssid, (bestNetworkSec == WIFI_AUTH_OPEN) ? NULL : bestNetwork.passphrase, bestChannel, bestBSSID);
#endif /* CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT */
      status = WiFi.status();
      _bWFMInit = true;

      startTime = millis();
      // wait for connection, fail, or timeout
      while (status != WL_CONNECTED && (millis() - startTime) <= connectTimeout) {  // && status != WL_NO_SSID_AVAIL && status != WL_CONNECT_FAILED
        delay(10);
        status = WiFi.status();
      }

      switch (status) {
        case WL_CONNECTED:
          log_i("[WIFI] Connecting done.");
          log_d("[WIFI] SSID: %s", WiFi.SSID().c_str());
          log_d("[WIFI] IP: %s", WiFi.localIP().toString().c_str());
          log_d("[WIFI] MAC: %s", WiFi.BSSIDstr().c_str());
          log_d("[WIFI] Channel: %" PRIi32, WiFi.channel());

          if (_connectionTestCBFunc != NULL) {
            // We connected to an AP but if it's a captive portal we're not going anywhere.  Test it.
            if (_connectionTestCBFunc()) {
              resetFails();
            } else {
              markAsFailed(bestIndex);
              WiFi.disconnect();
              delay(10);
              status = WiFi.status();
            }
          } else {
            resetFails();
          }
          break;
        case WL_NO_SSID_AVAIL:
          log_e("[WIFI] Connecting Failed AP not found.");
          markAsFailed(bestIndex);
          break;
        case WL_CONNECT_FAILED:
          log_e("[WIFI] Connecting Failed.");
          markAsFailed(bestIndex);
          break;
        default:
          log_e("[WIFI] Connecting Failed (%d).", status);
          markAsFailed(bestIndex);
          break;
      }
    } else {
      log_e("[WIFI] no matching wifi found!");
    }
  } else {
    // start scan
    log_d("[WIFI] delete old wifi config...");
    WiFi.disconnect();

    log_d("[WIFI] start scan");
    // scan wifi async mode
    WiFi.scanNetworks(true);
  }

  return status;
}

#if CONFIG_LWIP_IPV6
void WiFiMulti::enableIPv6(bool state) {
  ipv6_support = state;
}
#endif

void WiFiMulti::markAsFailed(int32_t i) {
  APlist[i].hasFailed = true;
  log_d("[WIFI] Marked SSID %s as failed", APlist[i].ssid);
}

void WiFiMulti::resetFails() {
  for (uint32_t i = 0; i < APlist.size(); i++) {
    APlist[i].hasFailed = false;
  }
  log_d("[WIFI] Resetting failure flags");
}

void WiFiMulti::setStrictMode(bool bStrict) {
  _bStrict = bStrict;
}

void WiFiMulti::setAllowOpenAP(bool bAllowOpenAP) {
  _bAllowOpenAP = bAllowOpenAP;
}

void WiFiMulti::setConnectionTestCallbackFunc(ConnectionTestCB_t cbFunc) {
  _connectionTestCBFunc = cbFunc;
}

#endif /* SOC_WIFI_SUPPORTED */
