
#include "net.h"

#ifdef ENA_WIFI

#if defined(ENV_RP2040)
#elif defined(ENV_ESP32)
#error why ESP32 in net.h
#else
#error illegal ENV for net.h
#endif

#include "time.h"

#include "loopScanner.h"
#include "timeutils.h"
#include "textBuffer.h"

typedef struct {
  const char* ssid;
  const char* password;
} WiFiCredentials;

#define NUMOFWLANS    3

const WiFiCredentials wlans[NUMOFWLANS] = {
  { ssid:   "PUCONWLAN01",
    password: "Hadersdorf" },
  { ssid:   "PUCONWLAN02",
    password: "Hadersdorf" },
  { ssid:   "PUCONWLAN03",
    password: "Hadersdorf" } /* ,
  { ssid:   "PUCONWLANM2",
    password: "Hadersdorf"  } */
};

//
// ============== network module local variables ============================
//

const char* netntpServer = "pool.ntp.org";
const char* netntpServer2 = "time.nist.gov";
const long  netgmtOffset_sec = 3600;
const int   netdaylightOffset_sec = 3600;

int8_t            nwn;        // number of networks scanned
int               nwi;        // current network index
int               knwi;       // current known network index
int8_t            nwst;       // network state (NST_xxxxx)
uint32_t          nwstlc;     // number of loops (of 20 ms) already in this state
int8_t            nwrc;       // network operation retry count
int               nwgc1;      // network generic counter 1
int               nwi1;       // network generic variable 1
int               nwi2;       // network generic variable 2

//
// ============== network procedures ========================================
//

int findInKnownNetworks(const char* nssid) {
  for (int nn = 0; nn<NUMOFWLANS; nn++) {
    if (String(wlans[nn].ssid).equals(String(nssid))) {
      return nn;
    }
  }
  return -1;
}

#if defined(ENV_RP2040)
String getEncName(uint8_t encod) {
  return String("UNKNOWN_WIFI_ENC_") + encod;
  /*
  switch (encod) {
    case 8:   return String("AUTO");      break;
    case 7:   return String("NONE");      break;
    case 5:   return String("WEP");       break;
    case 4:   return String("WPA-CCMP");  break;
    case 2:   return String("WPA-TKIP");  break;
    default:  SERIAL_PRINTF("UNKNOWN_WIFI_ENC_%03d\n",encod);
              return String("UNKNOWN_WIFI_ENC_") + encod;
  }
  */
}
#elif defined(ENV_ESP32)
String getEncName(int encod) {
  switch (encod) {
    case 8:   return String("AUTO");      break;
    case 7:   return String("NONE");      break;
    case 5:   return String("WEP");       break;
    case 4:   return String("WPA-CCMP");  break;
    case 2:   return String("WPA-TKIP");  break;
    default:  return String("UNKNOWN_WIFI_ENC");
  }
}
#else
#error unrecognized ENV for getEncName
#endif

void connectToWifi() {
  int retries;
  int nwi;
  int fnwi;
  int fkni;
  // Operate in WiFi Station mode
#if defined(ENV_RP2040)
  WiFi.mode(WIFI_STA);
#elif defined(ENV_ESP32)
  WiFi.enableSTA(true);
#else
#error unrecognized ENV for WiFi.STA
#endif
  delay(WLANSETUPDELAY);
  #if defined(ENV_RP2040)
  int8_t itms = WiFi.scanNetworks();
  #elif defined(ENV_ESP32)
  int8_t itms = WiFi.scanNetworks(false,true);
  #else
  #error unrecognized ENV for WiFi.scanNetworks
  #endif
  SERIAL_PRINT(itms);
  SERIAL_PRINTLN(" networks found:");
  for (nwi=0; nwi<itms; nwi++) {
    SERIAL_PRINTLN(WiFi.SSID(nwi));
  }
  for (fnwi = 0; fnwi<itms; fnwi++) {
    for (nwi=0, fkni=-1; nwi<NUMOFWLANS; nwi++) {
      if (String(wlans[nwi].ssid)==WiFi.SSID(fnwi)) {
        fkni = nwi;
      }
    }
    if (fkni<0) continue;
    SERIAL_PRINT("connectToWiFi: Trying to connect to ");
    SERIAL_PRINTLN(wlans[fkni].ssid);
    WiFi.begin(wlans[fkni].ssid, wlans[fkni].password);
    for (retries=MAXWLANRETRY; retries>0; retries--) {
      SERIAL_PRINT("Status of ");
      SERIAL_PRINT(wlans[fkni].ssid);
      SERIAL_PRINT(" is ");
      SERIAL_PRINTLN(WiFi.status());
      if (WiFi.status()==WL_CONNECTED) break;
      delay(WLANRETRYDELAY);
      SERIAL_PRINT(".");
    }
    if (WiFi.status()==WL_CONNECTED) break;
  }
  if (retries>0) {
    SERIAL_PRINTLN("");
    SERIAL_PRINT("WiFi connected to ");
    SERIAL_PRINTLN(wlans[fkni].ssid);
    SERIAL_PRINT("IP address: ");
    SERIAL_PRINTLN(netLocalAddress());
    SERIAL_PRINT("local hostname: ");
    SERIAL_PRINTLN(WiFi.getHostname());
    // DNSClient dns;
    /*
    dns.begin("192.168.178.1"); <= TODO: doesn't work, need IPAddress
    SERIAL_PRINT("DNS hostname: ");
    SERIAL_PRINTLN(dns.getHostByAddress(WiFi.localIP()));
    */
  } else {
    SERIAL_PRINTLN("");
    SERIAL_PRINTLN("************** WiFi not connected *****************");
  }
}

void restartNetworkState() {
  nwstlc = 0;
}

void enterNetworkState(int8_t newstate) {
  SERIAL_PRINTF("entering network state %d[%s], previous is %d[%s]\n",newstate,nstNames[newstate].c_str(),nwst,nstNames[nwst].c_str());
  if (newstate!=nwst) {
    if (newstate==NST_CONNECTED) {
      putEventQueueEntry(EVT_NET_UP,0);
    } else if (nwst==NST_CONNECTED) {
      putEventQueueEntry(EVT_NET_DOWN,0);
    }
  }
  nwst = newstate;
  restartNetworkState();
}

void networkScanned() {
  int emillis = millis();
  SERIAL_PRINTF("enterNetworkScanning done, took %d milliseconds, found %d networks:\n",emillis-nwi1,nwn);
  if (nwn==0) {
    enterNetworkState(NST_NOTCONN);
    return;
  }
  for (int ni=0; ni<nwn; ni++) {
#if defined(ENV_RP2040)
    const char* ssid = WiFi.SSID(ni);
    uint8_t enc  = WiFi.encryptionType(ni);
    const char* encname = getEncName(enc).c_str();
    uint8_t bssid[6]; WiFi.BSSID(bssid);
    int rssi = WiFi.RSSI(ni);
    SERIAL_PRINTF("%03d - SSID: %s - encoding: %s - BSSID: %02X-%02X-%02X-%02X-%02X-%02X - RSSI: %d\n",ni+1,ssid,encname,bssid[0],bssid[1],bssid[2],bssid[3],bssid[4],bssid[5],rssi);
#elif defined(ENV_ESP32)
    SERIAL_PRINTF("%03d - SSID: %s - encoding: %s - BSSID: %03d - RSSI: %d\n",ni+1,WiFi.SSID(ni).c_str(),getEncName(WiFi.encryptionType(ni)).c_str(),WiFi.BSSID(ni),WiFi.RSSI(ni));
#else
#error unrecognized ENV for string formatting SSIDs in networkScanned()
#endif
  }
  // now we know we have found at least one network, try to connect, start with first one, indexed by <nwi>
  nwi = 0;
  initNetworkConnect();
}

// network enter scanning state
void enterNetworkScanning() {
  SERIAL_PRINTLN("enterNetworkScanning.BEGIN");
  setTimeValid(false);
  nwi1 = millis();
  SERIAL_PRINTF("enterNetworkScanning startmillis = %d\n",nwi1);
#if defined(ENV_RP2040)
  nwn = WiFi.scanNetworks();
  SERIAL_PRINTF("sync network scan complete, result = %d networks\n",nwn);
  networkScanned();
#elif defined(ENV_ESP32)
  nwn = WiFi.scanNetworks(false,true);
  SERIAL_PRINTF("async scan started, result= %d\n",nwn);
  enterNetworkState(NST_SCANNING);
#else
#error unrecognized ENV for WiFi.scanNetworks
#endif
}

void checkNetworkScanComplete() {
  // SERIAL_PRINTF("checkNetworkScanComplete, nwlc = %d\n",nwstlc);
  nwn = WiFi.scanComplete();
  SERIAL_PRINTF("async scan complete check, result = %d\n",nwn);
  int emillis = millis();
  if (nwn<0) {
    // SERIAL_PRINTF("network scan not yet complete = %d\n",nwn);
    if (nwstlc<NWLC_MAXLCSCAN) return;
    enterNetworkState(NST_NOTCONN);
    return;
  }    
  networkScanned();
}

// try to connect to next network (if available)
void tryConnectNextNetwork() {
  if (nwi>=nwn) {
    enterNetworkState(NST_NOTCONN);
    return;
  } else {
    nwi++;
    initNetworkConnect();
    return;
  }  
}

// initiate connection to network <nwi>
void initNetworkConnect() {
    const char* nssid;
    nssid = WiFi.SSID(nwi);
    SERIAL_PRINTF("initNetworkConnect: check if we know found network number %d of %d with SSID=\"%s\"\n",nwi,nwn,nssid);
    knwi = findInKnownNetworks(nssid);
    if (knwi<0) {          // the current network is not known
      SERIAL_PRINTF("initNetworkConnect: found network number %d, SSID=\"%s\", is not known, will not try to connect\n",nwi,nssid);
      tryConnectNextNetwork();  // try next one
      return;
    }
    SERIAL_PRINTF("initNetworkConnect: Trying to connect to found network number %d of %d with SSID \"%s\", known, named \"%s\" with password \"%s\"\n",nwi,nwn,nssid,wlans[knwi].ssid,wlans[knwi].password);
    nwrc = 0;                         // initialize retry counter
    WiFi.begin(wlans[knwi].ssid, wlans[knwi].password);
    enterNetworkState(NST_CONNECTING);
}

// check if connection successful
void checkNetworkConnect() {
  if (nwstlc<NWLC_CHECKCONNECT) return;
  if (WiFi.status()==WL_CONNECTED) {
    uint8_t mac[6];
    WiFi.macAddress(mac);
    IPAddress ip = WiFi.localIP();
    SERIAL_PRINTF("Successfully connected to SSID=\"%s\", known WLAN number %d, named \"%s\", MAC-address = %02X:%02X:%02X:%02X:%02X:%02X, \n",WiFi.SSID(nwi),nwi,wlans[nwi].ssid);
    SERIAL_PRINT("Local IP address: ");
    SERIAL_PRINTLN(ip);
    setTimeValid(true);
    nwgc1 = 0;
    enterNetworkState(NST_CONNECTED);
#if defined(ENV_RP2040)
    configTime(netgmtOffset_sec, netdaylightOffset_sec, netntpServer, netntpServer2 );
    setenv("TZ",NET_TIMEZONE,1);
    tzset();
#elif defined(ENV_ESP32)
    configTime(netgmtOffset_sec, netdaylightOffset_sec, netntpServer);
#else
#error unrecognized ENV for configTime
#endif
    return;
  }
  nwrc++;
  if (nwrc<NW_MAXCONNECTRETRYCOUNT) {
    restartNetworkState();
    return;
  }
  tryConnectNextNetwork();
}

// monitor the network, if enough time has passed: 
// - MONCONNECT*20ms to check WiFi connection
// - MONITOR*20 ms   to perform monitoring network operation 
void monitorNetwork() {
  if (nwstlc<NWLC_MONCONNECT) return;
  if (WiFi.status()!=WL_CONNECTED) {
    enterNetworkState(NST_WAITRECONNECT);
    return;
  }
  if (nwstlc<NWLC_MONITOR) return;
  // SERIAL_PRINTLN("Monitor (currently doing nothing) ...");
  // SERIAL_PRINTF("network still connected, millis: %022d\n",millis());
  restartNetworkState();
}

void initStaSetup() {
#if defined(ENV_RP2040)
  WiFi.mode(WIFI_STA);
#elif defined(ENV_ESP32)
  WiFi.enableSTA(true);
#else
#error unrecognized ENV for WiFi STA
#endif
  SERIAL_PRINTLN("network STA enabled");
  enterNetworkState(NST_WAITSTASETUP);  
}

void waitStaSetup() {
  // SERIAL_PRINTF("net.staWaitSetup.millis=%s.nwstlc=%06d\n",formatLongGrouped(millis()).c_str(),nwstlc);
  if (nwstlc<NWLC_WAITSTASETUP) return;
  enterNetworkScanning();
}

void waitReconnect() {
  if (nwstlc<NWLC_WAITRECONNECT) return;
  enterNetworkScanning();
}

/*
 * ==================== network loop ===================================
 * 
 * this is called every 20 milliseconds, it is a loopScanner (running in foreground!)
 */
void netLoop(int para) {
    // SERIAL_PRINTF("netLoop.millis=%s.state=%03d.loopcount=%07d\n",formatLongGrouped(millis()).c_str(),nwst,nwstlc);
    nwstlc++;
    switch (nwst) {
      case NST_UNDEF:         initStaSetup();             break;
      case NST_NOTCONN:       enterNetworkScanning();     break;
      case NST_SCANNING:      checkNetworkScanComplete(); break;
      case NST_SCANNED:       networkScanned();           break;
      case NST_WAITSTASETUP:  waitStaSetup();             break;
      case NST_WAITRECONNECT: waitReconnect();            break;
      case NST_CONNECTING:    checkNetworkConnect();      break;
      case NST_CONNECTED:     monitorNetwork();           break;
    }
}

String netLocalAddress() {
  if (nwst==NST_CONNECTED) {
    return WiFi.localIP().toString(); 
  } else {
    return String("<not-connected>");
  }
}

String netLocalMACAddress() {
  return WiFi.macAddress();
}

/*
 * ==================== setup network at restart =======================
 */
extern void initNet() {
  nwst = NST_UNDEF;
  addLoopScanner(20,netLoop,0,"netLoop");
  setTimeValid(false);
  SERIAL_PRINTLN("net initiaized");
 }

#else

#error net.cpp error: ENA_WIFI not defined

#endif
