/// © MiroZ 2024
#include "Wifi.h"
#include "errors.h"
#include "Settings.h"

#include <WiFi.h>
#include <functional>
#include "utilities.h"
#include "app_config.h"

static const char *TAG  = "Wifi";
#define IGNORE_SSID_MINS	20

using namespace std::placeholders;


Wifi::Wifi()
{
	esp_log_level_set("wifi", ESP_LOG_WARN);
	esp_log_level_set("wifi_init", ESP_LOG_INFO);

	WiFi.onEvent(std::bind(&Wifi::wifi_event, this, _1, _2));
#ifdef USE_TIMER
	esp_timer_create_args_t timer;
	timer.arg = this;
	timer.callback = &timerCallback;
	timer.dispatch_method = ESP_TIMER_TASK;
	timer.skip_unhandled_events = true;
	timer.name = "wifi countdown timer";

	ESP_ERROR_CHECK(esp_timer_create(&timer, &m_timer));
	ESP_ERROR_CHECK(esp_timer_start_periodic(m_timer, 60000000));	// 1 min
#endif
}

#ifdef USE_TIMER
void Wifi::timerCallback(void* arg)
{
	for(int n = 0; n < SETTINGS_NUM_WIFI_ENTRIES; n++)
	{
		if(((Wifi*)arg)->m_ignored[n] > 0)
			((Wifi*)arg)->m_ignored[n]--;
	}
}
#endif

void Wifi::wifi_event(arduino_event_id_t event, arduino_event_info_t info)
{
	ESP_LOGI(TAG, "event: %s (%d)", WiFi.eventName(event), (int)event);
	if(event == ARDUINO_EVENT_WIFI_STA_DISCONNECTED)
	{
		wifi_err_reason_t reason = (wifi_err_reason_t)info.wifi_sta_disconnected.reason;
		ESP_LOGI(TAG, "reason: %s", WiFi.disconnectReasonName(reason));
	}
}

void Wifi::task()
{
	while(true)
	{
		if(!WiFi.isConnected())
		{
			m_wifi_status = WIFI_STATUS::PENDING;
			m_wifi_status = startConnecting();
		}
		delay(15000);
	}
}

/// @brief Connect to provisioned wifi
/// @param index index of wifi in settings
/// @return WIFI_STATUS::CONNECTED/NOT_CONENCTED connected
Wifi::WIFI_STATUS Wifi::connectTo(int index)
{
	IPAddress local_IP(0, 0, 0, 0);
	IPAddress gateway(0, 0, 0, 0);
	IPAddress subnet(SETTINGS.wifi.subnet_mask);
	IPAddress primaryDNS(SETTINGS.wifi.dns_primary);
	IPAddress secondaryDNS(SETTINGS.wifi.dns_secondary);

	ESP_LOGI(TAG, "Connecting to %s...", SETTINGS.wifi.entry[index].ssid);

	WiFi.mode(WIFI_STA);
  	WiFi.config(local_IP, gateway, subnet, primaryDNS, secondaryDNS);
  	WiFi.hostname("wellhub");

	WiFi.begin(SETTINGS.wifi.entry[index].ssid, SETTINGS.wifi.entry[index].pwd);
	int status = WiFi.waitForConnectResult(10000);

	if(status == WL_CONNECTED)
	{
		ESP_LOGI(TAG, "Connected");
		return WIFI_STATUS::CONNECTED;
	}
	WiFi.disconnect();
	ESP_LOGW(TAG, "Failed to connect");
	return WIFI_STATUS::NOT_CONNECTED;
}

int Wifi::start()
{
	if(m_task == nullptr)
	{
		ESP_LOGW(TAG, "Starting wifi...");
		m_task = TaskMgr::getInstance().createTask(std::bind(&Wifi::task, this), WIFI_TASK_NAME, WIFI_TASK_STACK_SIZE, WIFI_TASK_PRIORITY, WIFI_TASK_CORE);
	}
	return 0;
}

Wifi::WIFI_STATUS Wifi::waitForConnection()
{
	Wifi::WIFI_STATUS st;

	while((st = status()) == Wifi::WIFI_STATUS::PENDING)
		delay(500);

	return st;
}

/// @brief Connects to wifi. Returns immediately if already connected.
/// @return WH_OK | WH_ERR_WIFI_NOT_PROVISIONED
Wifi::WIFI_STATUS Wifi::startConnecting()
{
	if(SETTINGS.wifi.num == 0)
	{
		ESP_LOGE(TAG, "Not provisioned!");
		return WIFI_STATUS::NOT_PROVISIONED;
	}

	if(WiFi.isConnected())
		return WIFI_STATUS::CONNECTED;

	// if only one network is provisioned
	if(SETTINGS.wifi.num == 1)
	{
		SETTINGS.wifi.selected = 0;
		for(int n = 0; n < 3; n++)
		{
			if(connectTo(SETTINGS.wifi.selected) == WIFI_STATUS::CONNECTED)
				return WIFI_STATUS::CONNECTED;

			WiFi.disconnect();
		}

		ESP_LOGE(TAG, "Not connected");
		return WIFI_STATUS::NOT_CONNECTED;
	}
	
	// multiple networks are provisioned. Use either specific network or scan for highest rssi

	memset(m_ignored, 0, SETTINGS_NUM_WIFI_ENTRIES);
	int ssid_ix = SETTINGS.wifi.selected;

	if(SETTINGS.wifi.always_scan_before_connect || SETTINGS.wifi.selected >= SETTINGS.wifi.num)
		ssid_ix = scan();
	
	if(ssid_ix < 0)
		return WIFI_STATUS::NOT_PROVISIONED;

	if(connectTo(ssid_ix) == WIFI_STATUS::CONNECTED)
	{
		SETTINGS.wifi.selected = ssid_ix;
		return WIFI_STATUS::CONNECTED;
	}

	// didn't work. go through them all giving priority to one with highest rssi

	m_ignored[ssid_ix] = IGNORE_SSID_MINS;

	do
	{
		ssid_ix = scan();
		if(ssid_ix >= 0)
		{
			if(connectTo(ssid_ix) == WIFI_STATUS::CONNECTED)
				return WIFI_STATUS::CONNECTED;
			else
				m_ignored[ssid_ix] = IGNORE_SSID_MINS;
		}
		else
			break;

	} while(true);

	// that failed too, try them all again round-robin

	for(int m = 0; m < 3; m++)
	{
		for(int n = 0; n < SETTINGS.wifi.num; n++)
		{
			if(connectTo(n) == WIFI_STATUS::CONNECTED)
				return WIFI_STATUS::CONNECTED;
		}
	}

	ESP_LOGE(TAG, "Cannot connect, need provisioning");
	return WIFI_STATUS::NOT_PROVISIONED;
}

/// @brief Scans all visible ssid's and picks known ssid with highest rssi that is not ignored.
/// @return wifi index in Settings
int Wifi::scan()
{
	ESP_LOGI(TAG, "Scanning wifi networks...");

	int num_networks = WiFi.scanNetworks(false, false, false, 500);
	ESP_LOGI(TAG, "found %d networks", num_networks);

	int rssi = -500;
	int selected_index = -1;

	// crossref scanned and saved networks
	for(int n = 0; n < num_networks; n++)
	{
		for(int p = 0; p < SETTINGS.wifi.num; p++)
		{
			if(strcmp(SETTINGS.wifi.entry[p].ssid, WiFi.SSID(n).c_str()) == 0)
			{
				// found saved network
				ESP_LOGI(TAG, "Found provisioned network '%s' with RSSI %d dBm", SETTINGS.wifi.entry[p].ssid, WiFi.RSSI(n));
				if(WiFi.RSSI(n) > rssi && m_ignored[p] == 0)
				{
					rssi = WiFi.RSSI(n);
					selected_index = p;
				}
			}
		}
	}

	if(selected_index >= 0)
		ESP_LOGI(TAG, "Chosen network '%s'", SETTINGS.wifi.entry[selected_index].ssid);
	else
		ESP_LOGI(TAG, "No suitable networks found at this time");

	return selected_index;
}