#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
#include <SPIFFS.h>
#include <WiFiSettings.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <SPI.h>
#include <MFRC522.h>
#include <ESPDateTime.h>
#include <DateTimeTZ.h>
#include <rdm6300.h>

#include "wifiFix.h"
#include "pitches.h"
#include "utils.h"
#include "oled.h"
#include "cardReader.h"

#pragma clang diagnostic push
#pragma ide diagnostic ignored "OCUnusedMacroInspection"

//MFRC522 pinout (front) 3V3 RST GND IRQ MISO MOSI SCK SDA
//MFRC522 pinout (rear) SDA SCK MOSI MISO IRQ GND RST 3V3

#define HSPI_RST  4
#define HSPI_MISO 12
#define HSPI_MOSI 13
#define HSPI_SCLK 14
#define HSPI_SS   15
#define PIN_HWSERIAL_RX 16
#define PIN_HWSERIAL_TX 17
#define PIN_BUZZER 19
#define PIN_OLED_SDA 21
#define PIN_OLED_SCL 22
#define PIN_INTERRUPT_TRANSACT 26
#define PIN_INTERRUPT_BALANCE 25
#define PIN_INTERRUPT_LINK 33
#define PIN_INTERRUPT_CANCEL 32

U8G2_SH1106_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, U8X8_PIN_NONE);
MFRC522 mfrc522(HSPI_SS, HSPI_RST);
WiFiClient *wifi = new WiFiClientFixed();
HTTPClient *http = new HTTPClient();
Rdm6300 rdm6300;

auto mfrc522reader = MFRC522CardReader(&mfrc522);
auto rdm6300reader = RDM6300CardReader(&rdm6300);
CardReader* readers[2] = {&mfrc522reader, &rdm6300reader};

e_state state = STATE_IDLE;
String apiUrl = "";
const int gTimeout = 5000;
const int scanTimeout = 15000;

String scannedCardId = "";
String lastStatusText = "";

bool interruptFired = false;
unsigned long timer = 0;

void IRAM_ATTR TransactInterruptHandler() {
	if (interruptFired || state != STATE_IDLE) {
		return;
	}
	interruptFired = true;
	state = STATE_TRANSACT_CARDSCAN;
}

void IRAM_ATTR LinkInterruptHandler() {
	if (interruptFired || state != STATE_IDLE) {
		return;
	}
	interruptFired = true;
	state = STATE_LINK_CARDSCAN;
}

void IRAM_ATTR BalanceInterruptHandler() {
	if (interruptFired || state != STATE_IDLE) {
		return;
	}
	interruptFired = true;
	state = STATE_BALANCE_CARDSCAN;
}

bool cooldownCheck(long timeout) {
	if (millis() - timer > timeout) {
		state = STATE_IDLE;
		updateOLED(u8g2, state);
		return true;
	}

	return false;
}

void setup() {
	Serial.begin(115200);
	Serial2.begin(115200, SERIAL_8N1, PIN_HWSERIAL_RX, PIN_HWSERIAL_TX);
	u8g2.begin();
	drawLogo(u8g2, LOGO_MATECARD);
	u8g2.sendBuffer();
	SPI.begin(HSPI_SCLK, HSPI_MISO, HSPI_MOSI);
	SPIFFS.begin(true);
	WiFiSettings.hostname = "afrapay-";
	apiUrl = WiFiSettings.string("AfRApay.Web API", "http://192.168.50.170:5296");
	WiFiSettings.connect();
	rdm6300.begin(PIN_HWSERIAL_RX);
	mfrc522.PCD_Init();
	mfrc522.PCD_DumpVersionToSerial();
	DateTime.setTimeZone(TZ_Europe_Berlin);
	DateTime.begin();
	updateOLED(u8g2, state);

	ledcSetup(0, 5000, 12);
	pinMode(PIN_INTERRUPT_TRANSACT, INPUT_PULLUP);
	pinMode(PIN_INTERRUPT_BALANCE, INPUT_PULLUP);
	pinMode(PIN_INTERRUPT_CANCEL, INPUT_PULLUP);
	pinMode(PIN_INTERRUPT_LINK, INPUT_PULLUP);
	attachInterrupt(PIN_INTERRUPT_TRANSACT, TransactInterruptHandler, FALLING);
	attachInterrupt(PIN_INTERRUPT_BALANCE, BalanceInterruptHandler, FALLING);
	attachInterrupt(PIN_INTERRUPT_LINK, LinkInterruptHandler, FALLING);
}

void loop() {
	if (WiFi.status() != WL_CONNECTED) {
		updateOLED(u8g2, state, "WiFi disconnected :(");
		state = STATE_IDLE;
		WiFi.reconnect();
		delay(50);
		return;
	}

	if (interruptFired) {
		timer = millis();
		interruptFired = false;
	}

	switch (state) {
	case STATE_IDLE:
		updateOLED(u8g2, state);
		break;
	case STATE_TRANSACT_CARDSCAN:
		for (CardReader *reader : readers) {
			if (reader->isNewCardPresent()) {
				scannedCardId = reader->getCardUid();
				lastStatusText = "Card #" + scannedCardId;
				state = STATE_TRANSACT_VERIFY;
				return;
			}
		}
		if (!cooldownCheck(scanTimeout)) {
			updateOLED(u8g2, state, String("1.50€"), String(cooldownSecondsRemaining(scanTimeout, timer)));
		}
		break;
	case STATE_TRANSACT_VERIFY:
		updateOLED(u8g2, state, lastStatusText);
		tone(PIN_BUZZER, NOTE_A5, 25);
		tone(PIN_BUZZER, NOTE_NONE, 150);
		lastStatusText = cardTransaction(wifi, http, apiUrl, scannedCardId, "1.50");
		if (lastStatusText.startsWith("S:")) {
			tone(PIN_BUZZER, NOTE_C7, 650);
			lastStatusText += "€";
			state = STATE_RESULT_SUCCESS;
		} else {
			tone(PIN_BUZZER, NOTE_CS5, 100);
			tone(PIN_BUZZER, NOTE_NONE, 25);
			tone(PIN_BUZZER, NOTE_CS5, 400);
			state = STATE_RESULT_FAILURE;
		}
		lastStatusText = lastStatusText.substring(2);
		timer = millis();
		return;
	case STATE_LINK_CARDSCAN:
		for (CardReader *reader : readers) {
			if (reader->isNewCardPresent()) {
				scannedCardId = reader->getCardUid();
				lastStatusText = "Card #" + scannedCardId;
				state = STATE_LINK_VERIFY;
				return;
			}
		}
		if (!cooldownCheck(scanTimeout)) {
			updateOLED(u8g2, state, "Link", String(cooldownSecondsRemaining(scanTimeout, timer)));
		}
		break;
	case STATE_LINK_VERIFY:
		updateOLED(u8g2, state, lastStatusText);
		tone(PIN_BUZZER, NOTE_A5, 25);
		tone(PIN_BUZZER, NOTE_NONE, 150);
		lastStatusText = cardLink(wifi, http, apiUrl, scannedCardId);
		if (lastStatusText.startsWith("S:")) {
			tone(PIN_BUZZER, NOTE_C6, 100);
			tone(PIN_BUZZER, NOTE_NONE, 10);
			tone(PIN_BUZZER, NOTE_C6, 100);
			state = STATE_RESULT_SUCCESS;
		} else {
			tone(PIN_BUZZER, NOTE_CS5, 100);
			tone(PIN_BUZZER, NOTE_NONE, 25);
			tone(PIN_BUZZER, NOTE_CS5, 400);
			state = STATE_RESULT_FAILURE;
		}
		lastStatusText = lastStatusText.substring(2);
		timer = millis();
		return;
	case STATE_BALANCE_CARDSCAN:
		for (CardReader *reader : readers) {
			if (reader->isNewCardPresent()) {
				scannedCardId = reader->getCardUid();
				lastStatusText = "Card #" + scannedCardId;
				state = STATE_BALANCE_VERIFY;
				return;
			}
		}
		if (!cooldownCheck(scanTimeout)) {
			updateOLED(u8g2, state, "Balance", String(cooldownSecondsRemaining(scanTimeout, timer)));
		}
		break;
	case STATE_BALANCE_VERIFY:
		updateOLED(u8g2, state, lastStatusText);
		tone(PIN_BUZZER, NOTE_A5, 25);
		tone(PIN_BUZZER, NOTE_NONE, 150);
		lastStatusText = cardBalance(wifi, http, apiUrl, scannedCardId);
		if (lastStatusText.startsWith("S:")) {
			lastStatusText = lastStatusText + ":Card #" + scannedCardId;
			state = STATE_RESULT_DISPLAY;
		} else {
			tone(PIN_BUZZER, NOTE_CS5, 100);
			tone(PIN_BUZZER, NOTE_NONE, 25);
			tone(PIN_BUZZER, NOTE_CS5, 400);
			state = STATE_RESULT_FAILURE;
		}
		lastStatusText = lastStatusText.substring(2);
		timer = millis();
	case STATE_RESULT_SUCCESS:
	case STATE_RESULT_FAILURE:
	case STATE_RESULT_DISPLAY:
		if (!cooldownCheck(gTimeout)) {
			updateOLED(u8g2, state, lastStatusText, String(cooldownSecondsRemaining(gTimeout, timer)));
		}
		break;
	}

	delay(50);
}
#pragma clang diagnostic pop
