/*
 * Copyright (c) 2019, 2020, 2021, Erich Styger
 *
 * SPDX-License-Identifier: BSD-3-Clause
 * ******************************************************************
 * Dear programmer:
 * When I wrote this code, only god and I knew how it worked.
 * Now, only god knows it!
 *
 * Therefore, if you are trying to optimize or change this code
 * and it fails (most surely), please increase the counter below
 * as a warning for the next person:
 *
 * total_hours_wasted_here = 257
 * *******************************************************************
 */

#include "platform.h"
#if PL_CONFIG_USE_WIFI
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "esp_wifi.h"
#include "esp_wpa2.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_system.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#if PL_CONFIG_USE_SNTP_TIME
  #include "sntp_time.h"
#endif
#if PL_CONFIG_USE_UDP_CLIENT
  #include "udp_client.h"
#endif
#if PL_CONFIG_USE_UDP_SERVER
  #include "udp_server.h"
#endif
#if PL_CONFIG_USE_BLINKY
  #include "led.h"
  #define LED_ON_TIME_MS_CONNECTED      1000
  #define LED_ON_TIME_MS_DISCONNECTED   5
#endif
#include "McuUtility.h"
#include "McuXFormat.h"
#include "esp32_mac.h"
#include "McuLog.h"

#define EAP_PEAP 1  /* WPA2 Enterprise with password and no certificate */
#define EAP_TTLS 2  /* TLS method */
#define TAG "WiFi" /* tag for logging with ESP_LOG */

#include "pwd.h"  /* local file with login information */

#ifndef CONFIG_ESP_MAXIMUM_RETRY
  #define CONFIG_ESP_MAXIMUM_RETRY (2) /*  number of retries to connect to the network */
#endif

static int s_retry_num = 0;

/* FreeRTOS event group to signal when we are connected & ready to make a request */
static EventGroupHandle_t s_wifi_event_group;
/* The event group allows multiple bits for each event, but we only care about two events:
 * - we are connected to the AP with an IP
 * - we failed to connect after the maximum amount of retries */
#define WIFI_EVENT_HANDLER_CONNECTED_BIT (1<<0)
#define WIFI_EVENT_HANDLER_FAIL_BIT      (1<<1)
#define WIFI_CONNECTED_BIT               (1<<2)
#define WIFI_WAS_CONNECTED_ONCE          (1<<3) // enables retry when the WiFi was lost

static esp_netif_t *APP_WiFi_NetIf;
static bool APP_WiFi_isOn = true;

void APP_WiFi_PrintIP(void) {
  tcpip_adapter_ip_info_t ip;

  memset(&ip, 0, sizeof(tcpip_adapter_ip_info_t));
  if (tcpip_adapter_get_ip_info(ESP_IF_WIFI_STA, &ip) == ESP_OK) {
    McuLog_info("IP:"IPSTR " MASK:"IPSTR " GW:"IPSTR, IP2STR(&ip.ip), IP2STR(&ip.netmask), IP2STR(&ip.gw));
  } else {
    McuLog_error("failed tcpip_adapter_get_ip_info()");
  }
}

static void event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) {
  if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
    McuLog_info("WIFI_EVENT_STA_START: start event");
    esp_wifi_connect();
  } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
    McuLog_info("WIFI_EVENT_STA_DISCONNECTED: disconnected");
    // set bit for WiFiTask => state is disconnected
    xEventGroupClearBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    // any retrys left?
    if (s_retry_num < CONFIG_ESP_MAXIMUM_RETRY) {
      esp_wifi_connect();
      McuLog_info("retrying to connect to the AP, retry %d", ++s_retry_num);
    }  // if wifi already connected => endless reconnect, no counting
    else if(xEventGroupGetBits(s_wifi_event_group)&WIFI_WAS_CONNECTED_ONCE){
      esp_wifi_connect();
      McuLog_info("retrying endlessly to connect to the AP");
    }
    else {
      // set bits for init
      xEventGroupSetBits(s_wifi_event_group, WIFI_EVENT_HANDLER_FAIL_BIT);
      xEventGroupClearBits(s_wifi_event_group, WIFI_EVENT_HANDLER_CONNECTED_BIT);
    }
    McuLog_info("connect to the AP fail");
  } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
    ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data;
    McuLog_info("IP_EVENT_STA_GOT_IP: got ip:" IPSTR, IP2STR(&event->ip_info.ip));
    s_retry_num = 0;
    // set bits for init
    xEventGroupSetBits(s_wifi_event_group, WIFI_EVENT_HANDLER_CONNECTED_BIT);
    xEventGroupClearBits(s_wifi_event_group, WIFI_EVENT_HANDLER_FAIL_BIT);
    // set bit for WiFiTask => state is connected
    xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
    // set wifi was connected once => enables endless reconnect from here
    xEventGroupSetBits(s_wifi_event_group, WIFI_WAS_CONNECTED_ONCE);
  }
}

typedef enum {
  WIFI_PASSWORD_METHOD_PSK,
  WIFI_PASSWORD_METHOD_WPA2,
} WiFi_PasswordMethod_e;

static void GetNetworkSSID(unsigned char *ssidBuf, size_t ssidBufSize) {
  wifi_ap_record_t ap;
  esp_err_t err;

  err = esp_wifi_sta_get_ap_info(&ap);
  if (err==ESP_OK) {
    McuUtility_strcpy(ssidBuf, ssidBufSize, ap.ssid);
  } else if (err==ESP_ERR_WIFI_CONN) {
    McuUtility_strcpy(ssidBuf, ssidBufSize, (unsigned char*)"not init");
  } else if (err==ESP_ERR_WIFI_NOT_CONNECT) {
    McuUtility_strcpy(ssidBuf, ssidBufSize, (unsigned char*)"not connected");
  } else {
    McuUtility_strcpy(ssidBuf, ssidBufSize, (unsigned char*)"error?");
  }
}

static void SetPasswordMode(WiFi_PasswordMethod_e mode) {
  wifi_config_t wifi_config;

  McuLog_info("SetPasswordMode(): %d", mode);
  ESP_ERROR_CHECK( esp_wifi_set_storage(WIFI_STORAGE_RAM) );
  memset(&wifi_config, 0, sizeof(wifi_config_t)); /* initialize all fields */
  if (mode==WIFI_PASSWORD_METHOD_WPA2) {
    strncpy((char*)wifi_config.sta.ssid, CONFIG_WIFI_EAP_SSID, sizeof(wifi_config.sta.ssid));
  } else if (mode==WIFI_PASSWORD_METHOD_PSK) {
    strncpy((char*)wifi_config.sta.ssid, CONFIG_WIFI_PSK_SSID, sizeof(wifi_config.sta.ssid));
    strncpy((char*)wifi_config.sta.password, CONFIG_WIFI_PSK_PASSWORD, sizeof(wifi_config.sta.password));
  } else {
    McuLog_error("Wrong connection mode: %d", mode);
  }
  ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
  ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config) );
  if (mode==WIFI_PASSWORD_METHOD_WPA2) {
    const ESP32_Device_t *device;

    device = ESP32_GetDeviceConfig();
    McuLog_info("EAP_ID: %s", device->eee_id);
    ESP_ERROR_CHECK( esp_wifi_sta_wpa2_ent_set_identity((uint8_t *)device->eee_id, strlen(device->eee_id)) );
    if (CONFIG_WIFI_EAP_METHOD == EAP_PEAP || CONFIG_WIFI_EAP_METHOD == EAP_TTLS) {
      McuLog_info("EAP_USERNAME: %s", device->eee_id);
      ESP_ERROR_CHECK( esp_wifi_sta_wpa2_ent_set_username((uint8_t *)device->eee_id, strlen(device->eee_id)) );
      ESP_ERROR_CHECK( esp_wifi_sta_wpa2_ent_set_password((uint8_t *)device->eee_pwd, strlen(device->eee_pwd)) );
    }
    ESP_ERROR_CHECK( esp_wifi_sta_wpa2_ent_enable() );
  }
}

static void initialise_wifi(void) {
  WiFi_PasswordMethod_e mode = CONFIG_WIFI_START_WITH; /* starting mode */
  const ESP32_Device_t *config;

  s_wifi_event_group = xEventGroupCreate();
  ESP_ERROR_CHECK(esp_netif_init());
  ESP_ERROR_CHECK(esp_event_loop_create_default());
  APP_WiFi_NetIf = esp_netif_create_default_wifi_sta();

  config = ESP32_GetDeviceConfig();
  McuLog_info("Setting hostname: %s", config->hostName);
  ESP_ERROR_CHECK(esp_netif_set_hostname(APP_WiFi_NetIf, config->hostName));
  wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
  ESP_ERROR_CHECK(esp_wifi_init(&cfg));

  ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL));
  ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL));

  SetPasswordMode(mode);

  McuLog_info("Starting WiFi");
  ESP_ERROR_CHECK(esp_wifi_start());

  /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
   * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
  EventBits_t bits;

  do {
    if (APP_WiFi_isOn) {
      bits = xEventGroupWaitBits(s_wifi_event_group,
              WIFI_EVENT_HANDLER_CONNECTED_BIT | WIFI_EVENT_HANDLER_FAIL_BIT, /* bits to wait for */
              pdTRUE, /* bits to clear on exit: we do not clear the WIFI_CONNECTED_BIT because used by WiFi task */
              pdFALSE, /* wait for all bits */
              pdMS_TO_TICKS(20000)); /* wait time */
      if (bits&WIFI_EVENT_HANDLER_CONNECTED_BIT) {
        break; /* leave loop */
      }
      if (bits&WIFI_EVENT_HANDLER_FAIL_BIT) {
        McuLog_info("FAILED connecting, restarting WiFi with different mode");
        ESP_ERROR_CHECK(esp_wifi_stop());
        /* toggle mode */
        if (mode==WIFI_PASSWORD_METHOD_PSK) {
          mode = WIFI_PASSWORD_METHOD_WPA2;
        } else if (mode==WIFI_PASSWORD_METHOD_WPA2) {
          ESP_ERROR_CHECK(esp_wifi_sta_wpa2_ent_disable());
          mode = WIFI_PASSWORD_METHOD_PSK;
        }
        SetPasswordMode(mode);
        ESP_ERROR_CHECK(esp_wifi_start());
      }
    } else {
      vTaskDelay(pdMS_TO_TICKS(500));
    }
  } while(true); /* breaks */

  /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually happened. */
  if (bits & WIFI_EVENT_HANDLER_CONNECTED_BIT) {
    if (mode == WIFI_PASSWORD_METHOD_WPA2) {
      McuLog_info("connected to AP SSID: %s ",  CONFIG_WIFI_EAP_SSID);
    } else if (mode == WIFI_PASSWORD_METHOD_PSK) {
      McuLog_info("connected to AP SSID: %s",  CONFIG_WIFI_PSK_SSID);
    }
  } else if (bits & WIFI_EVENT_HANDLER_FAIL_BIT) {
    if (mode == WIFI_PASSWORD_METHOD_WPA2) {
      McuLog_info("Failed to connect to SSID: %s", CONFIG_WIFI_EAP_SSID);
    } else if (mode == WIFI_PASSWORD_METHOD_PSK) {
      McuLog_info("Failed to connect to SSID: %s", CONFIG_WIFI_PSK_SSID);
    }
  } else {
    McuLog_error("UNEXPECTED EVENT");
  }
}

typedef enum {
  WIFI_STATE_INIT,
  WIFI_STATE_CONNECTED,
  WIFI_STATE_DISCONNECTED,
} WiFi_State_e;

static void WiFiTask(void *pv) {
  bool isConnected = false;
  WiFi_State_e state = WIFI_STATE_INIT;

  McuLog_info("Initialize WiFi");
  initialise_wifi();
#if PL_CONFIG_USE_BLINKY
  LED_SetOnTime(LED_ON_TIME_MS_DISCONNECTED);
#endif
  for(;;) {
    vTaskDelay(pdMS_TO_TICKS(5000));
    isConnected = xEventGroupGetBits(s_wifi_event_group)&WIFI_CONNECTED_BIT;
    #if PL_CONFIG_USE_BLINKY
      LED_SetOnTime(isConnected?LED_ON_TIME_MS_CONNECTED:LED_ON_TIME_MS_DISCONNECTED);
    #endif
    if (state == WIFI_STATE_INIT && isConnected) { /* first connection */
      state = WIFI_STATE_CONNECTED;
      ESP_LOGI(TAG, "WiFi is connected.");
      if (isConnected) {
    	  APP_WiFi_PrintIP();
      }
    #if PL_CONFIG_USE_UDP_SERVER
      McuLog_info("starting UDP server.");
      UDP_Server_Start();
    #endif
    #if PL_CONFIG_USE_UDP_CLIENT
      McuLog_info("starting UDP client.");
      UDP_Client_Start();
    #endif
    #if PL_CONFIG_USE_SNTP_TIME
      if (SNTP_ObtainTime()!=0) {
        McuLog_error("failed getting SNTP time.");
      }
    #endif
#if 1
    } else if (isConnected) {
      ESP_LOGI(TAG, "still connected.");
      APP_WiFi_PrintIP();

      const char *hostname;
      ESP_ERROR_CHECK(esp_netif_get_hostname(APP_WiFi_NetIf, &hostname));
      ESP_LOGI(TAG,"hostname: %s", hostname);
    } else {
      ESP_LOGI(TAG, "not connected yet.");
#endif
    }
  } /* for */
}

bool WiFi_isConnected(void) {
  bool isConnected;

  isConnected = xEventGroupGetBits(s_wifi_event_group)&WIFI_CONNECTED_BIT;
  return isConnected;
}

#if PL_CONFIG_USE_SHELL
static uint8_t PrintStatus(const McuShell_StdIOType *io) {
  uint8_t buf[32];
  uint8_t mac[6];

  McuShell_SendStatusStr((unsigned char*)"wifi", (unsigned char*)"ESP32 WiFi status\r\n", io->stdOut);
  McuUtility_strcpy(buf, sizeof(buf), (unsigned char*)IDF_VER);
  McuUtility_strcat(buf, sizeof(buf), (unsigned char*)"\r\n");
  McuShell_SendStatusStr((unsigned char*)"  IDF", buf, io->stdOut);
  McuShell_SendStatusStr((unsigned char*)"  connected", WiFi_isConnected()?(unsigned char*)"yes\r\n":(unsigned char*)"no\r\n", io->stdOut);

  ESP32_MacRead(mac);
  ESP32_MacToString(mac, buf, sizeof(buf));
  McuUtility_strcat(buf, sizeof(buf), (unsigned char*)"\r\n");
  McuShell_SendStatusStr((unsigned char*)"  MAC", buf, io->stdOut);
  McuShell_SendStatusStr((unsigned char*)"  WiFi", APP_WiFi_isOn?(unsigned char*)"on\r\n":(unsigned char*)"off\r\n", io->stdOut);

  if (WiFi_isConnected()) {
    unsigned char buf[64];
    tcpip_adapter_ip_info_t ip;
    const char *hostname;

    if (esp_netif_get_hostname(APP_WiFi_NetIf, &hostname)==ERR_OK) {
      McuUtility_strcpy(buf, sizeof(buf), (unsigned char*)hostname);
      McuUtility_strcat(buf, sizeof(buf), (unsigned char*)"\r\n");
    } else {
      McuUtility_strcpy(buf, sizeof(buf), (unsigned char*)"failed\r\n");
    }
    McuShell_SendStatusStr((unsigned char*)"  hostname", buf, io->stdOut);

    memset(&ip, 0, sizeof(tcpip_adapter_ip_info_t));
    if (tcpip_adapter_get_ip_info(ESP_IF_WIFI_STA, &ip) == ESP_OK) {
      McuXFormat_xsnprintf((char*)buf, sizeof(buf), "IP:"IPSTR " MASK:"IPSTR " GW:"IPSTR "\r\n", IP2STR(&ip.ip), IP2STR(&ip.netmask), IP2STR(&ip.gw));
    } else {
      McuUtility_strcpy(buf, sizeof(buf), (unsigned char*)"failed\r\n");
    }
    McuShell_SendStatusStr((unsigned char*)"  IP", buf, io->stdOut);

    GetNetworkSSID(buf, sizeof(buf));
    McuUtility_strcat(buf, sizeof(buf), (unsigned char*)"\r\n");
    McuShell_SendStatusStr((unsigned char*)"  SSID", buf, io->stdOut);
  }
  return ERR_OK;
}

static uint8_t PrintHelp(const McuShell_StdIOType *io) {
  McuShell_SendHelpStr((unsigned char*)"wifi", (unsigned char*)"Group of ESP32 WiFi commands\r\n", io->stdOut);
  McuShell_SendHelpStr((unsigned char*)"  help|status", (unsigned char*)"Shows WiFi help or status\r\n", io->stdOut);
  McuShell_SendHelpStr((unsigned char*)"  on|off", (unsigned char*)"Turn WiFi on or off\r\n", io->stdOut);
  return ERR_OK;
}

uint8_t WiFi_ParseCommand(const unsigned char* cmd, bool *handled, const McuShell_StdIOType *io) {
  if (McuUtility_strcmp((char*)cmd, (char*)McuShell_CMD_HELP)==0 || McuUtility_strcmp((char*)cmd, (char*)"wifi help")==0) {
    *handled = TRUE;
    return PrintHelp(io);
  } else if (McuUtility_strcmp((char*)cmd, (char*)McuShell_CMD_STATUS)==0 || McuUtility_strcmp((char*)cmd, (char*)"wifi status")==0) {
    *handled = TRUE;
    return PrintStatus(io);
  } else if (McuUtility_strcmp((char*)cmd, (char*)"wifi on")==0) {
    *handled = TRUE;
    APP_WiFi_isOn = true;
    return ERR_OK;
  } else if (McuUtility_strcmp((char*)cmd, (char*)"wifi off")==0) {
    *handled = TRUE;
    APP_WiFi_isOn = false;
    return ERR_OK;
  }
  return ERR_OK;
}
#endif /* PL_CONFIG_USE_SHELL */

void WiFi_Init(void) {
  BaseType_t res;

  res = xTaskCreate(WiFiTask, "WiFi", 16*1024/sizeof(StackType_t), NULL, tskIDLE_PRIORITY, NULL);
  if (res==pdPASS) {
    McuLog_info("created WiFi task");
  } else {
    McuLog_error("failed creating WiFi task!");
  }
  /* set mac address, otherwise will get "system_api: Base MAC address is not set, read default base MAC address from BLK0 of EFUSE" whenever asking for it */
  uint8_t mac[6];
  res = esp_read_mac(&mac[0], ESP_MAC_WIFI_STA);
  if (res==ESP_OK) {
    ESP_ERROR_CHECK(esp_base_mac_addr_set(&mac[0]));
  }
}
#endif /* PL_CONFIG_USE_WIFI */
