#include <Arduino.h>
#include "IDECompat.h"

#include <WiFi.h>
#include <WiFiMulti.h>
#include <FS.h>
#include <SPIFFS.h>
#include <ESPAsyncWebServer.h>

#include "ADC.h"

#include "wifi-credentials.h"

AsyncWebServer server(80);

ADC currentSensor(36);
ADC batterySensor(39);
const int8_t speedSensorPin = 13;
const int8_t debugLedPin = 12;

const float wheelDiameterInches = 20;
const int numImpulsesPerTurn = 2;
const float wheelCircumferenceMeters = wheelDiameterInches * 0.0254f * 3.1415f / (float)numImpulsesPerTurn;

uint16_t batteryVoltage = 0; // in mV
uint16_t batteryCurrent = 0; // in mV

WiFiMulti wifiMulti;
wl_status_t wifi_STA_status = WL_NO_SHIELD;
unsigned long wifiConnexionBegin = 0;
const unsigned long retryWifiConnexionDelay = 60000; // in milliseconds

volatile bool debugLedState = true;

volatile bool speedSensorState = false;
volatile unsigned long speedSensorRiseTime = 0;
volatile unsigned long speedSensorLastImpulseTime = 0;
volatile unsigned long speedSensorLastImpulseInterval = (unsigned long)-1; // in milliseconds
void IRAM_ATTR onSpeedSensorChange(bool newState)
{
	if(speedSensorState == newState) return;
	unsigned long now = millis();
	speedSensorState = newState;

	bool magnetDetected = !speedSensorState; // the magnet closes the contact which pulls the pin low

	if(magnetDetected)
	{
		speedSensorRiseTime = now;
	}
	else
	{
		unsigned long impulseDuration = now > speedSensorRiseTime ? now - speedSensorRiseTime : (4294967295 - speedSensorRiseTime) + now; // if now is lower than speedSensorRiseTime, it means millis() has overflowed (happens every 50 days)
		if(impulseDuration > 1000) return; // impulse was too long, ignore it (maybe magnet stopped near the sensor)

		debugLedState = !debugLedState;
		digitalWrite(debugLedPin, debugLedState ? HIGH : LOW);

		unsigned long timeSinceLastImpulse = now > speedSensorLastImpulseTime ? now - speedSensorLastImpulseTime : (4294967295 - speedSensorLastImpulseTime) + now;
		speedSensorLastImpulseTime = now;
		if(timeSinceLastImpulse > 30 && timeSinceLastImpulse < 4000)
		{
			speedSensorLastImpulseInterval = timeSinceLastImpulse;
		}
		else
		{
			speedSensorLastImpulseInterval = (unsigned long)-1;
		}
	}
}

void IRAM_ATTR onSpeedSensorChange() { onSpeedSensorChange(digitalRead(speedSensorPin) == HIGH); }

float getSpeed()
{
	unsigned long now = millis();

	unsigned long lastImpulseInterval = speedSensorLastImpulseInterval;
	unsigned long lastImpulseTime = speedSensorLastImpulseTime;
	unsigned long timeSinceLastImpulse = now > lastImpulseTime ? now - lastImpulseTime : (4294967295 - lastImpulseTime) + now;

	unsigned long interval = timeSinceLastImpulse > lastImpulseInterval * 10 / 9 ? timeSinceLastImpulse : lastImpulseInterval;

	float speed = wheelCircumferenceMeters / (float)interval * 1000.0f; // in meters per second

	if(speed < 0.25f) return 0.0f; // if speed is very low (less than 1km/h) it probably means we've stopped
	return speed;
}

void connectWifi()
{
	wifiMulti = WiFiMulti();

	const int numSSIDs = sizeof(wifi_STA_credentials)/sizeof(wifi_STA_credentials[0]);
	if(numSSIDs > 0)
	{
		Serial.println("Connecting to wifi...");

		for(int idx = 0; idx < numSSIDs; ++idx)
		{
			wifiMulti.addAP(wifi_STA_credentials[idx].SSID, wifi_STA_credentials[idx].password);
		}

		wifiConnexionBegin = millis();
		wifiMulti.run();
	}
}

void setup()
{
	pinMode(speedSensorPin, INPUT_PULLUP);
	attachInterrupt(speedSensorPin, &onSpeedSensorChange, CHANGE);

	pinMode(debugLedPin, OUTPUT);
	digitalWrite(debugLedPin, debugLedState ? HIGH : LOW);

	Serial.begin(115200);

	if(!SPIFFS.begin(false)){
		Serial.println("SPIFFS Mount Failed");
		return;
	}

	// Set WiFi mode to both AccessPoint and Station
	WiFi.mode(WIFI_AP_STA);

	// Create the WiFi Access Point
	if(wifi_AP_ssid != nullptr)
	{
		Serial.println("Creating wifi access point...");
		WiFi.softAP(wifi_AP_ssid, wifi_AP_password);

		Serial.print("Wifi access point created, SSID=");
		Serial.print(wifi_AP_ssid);
		Serial.print(", IP=");
		Serial.println(WiFi.softAPIP());
	}

	// Also connect as a station (if the configured remote access point is in range)
	connectWifi();

	server.on("/api/status", HTTP_GET, [](AsyncWebServerRequest *request){
		int v = batteryVoltage;
		int c = batteryCurrent;
		int s = (int)(getSpeed() * 1000.0f + 0.5f);

		char json[128];
		sprintf(json, "{\"v\":%d,\"c\":%d,\"s\":%d}", v, c, s);
		request->send(200, "text/json", json);
	});

	// Special case to send index.html without caching
	server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ request->send(SPIFFS, "/www/index.html", "text/html"); });
	server.serveStatic("/index.html", SPIFFS, "/www/index.html");

	// Other static files are cached (index.html knows whether to ignore caching or not for each file)
	server.serveStatic("/", SPIFFS, "/www/").setCacheControl("max-age=5184000");

	server.begin();
	Serial.println("HTTP server started");
}

void loop()
{
	wl_status_t newWifiStatus = WiFi.status();
	if(newWifiStatus != wifi_STA_status)
	{
		if(newWifiStatus == WL_CONNECTED)
		{
			Serial.print("Connected to wifi (");
			Serial.print(WiFi.SSID().c_str());
			Serial.print("), ip=");
			Serial.println(WiFi.localIP());
		}
		else if(newWifiStatus == WL_DISCONNECTED)
		{
			char codeStr[16];
			sprintf(codeStr, "%d", (int)newWifiStatus);
			Serial.print("Lost wifi connexion (");
			Serial.print(codeStr);
			Serial.println(")");

			connectWifi();
		}
		else
		{
			char codeStr[16];
			sprintf(codeStr, "%d", (int)newWifiStatus);
			Serial.print("Wifi state: ");
			Serial.println(codeStr);
		}

		wifi_STA_status = newWifiStatus;
	}

	if(wifi_STA_status != WL_CONNECTED)
	{
		unsigned long now = millis();
		unsigned long elapsed = now > wifiConnexionBegin ? now - wifiConnexionBegin : (4294967295 - wifiConnexionBegin) + now;
		if(elapsed > retryWifiConnexionDelay)
			connectWifi();
	}

	const int numSamples = 100;

	float averageV = 0.0f;
	float averageC = 0.0f;
	for(int sample = 0; sample < numSamples; ++sample)
	{
		delay(1);
		float v = batterySensor.read();
		float c = currentSensor.read();

		averageV += v;
		averageC += c;
	}

	averageV /= (float)numSamples;
	averageC /= (float)numSamples;

	if(averageV < 0.2f) averageV = 0.0f;

	averageV *= 27.000f; // account for voltage divider to retrieve the input voltage
	averageC = max(0.0f, averageC - 2.5f) / 0.0238f; // convert voltage to current, according to the sensor linear relation

	// TODO: mutex ?
	batteryVoltage = (uint16_t)(averageV * 1000.0f + 0.5f);
	batteryCurrent = (uint16_t)(averageC * 1000.0f + 0.5f);

	delay(10);
}
