#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 <ESPDateTime.h>
#include <DateTimeTZ.h>

#include "wifiFix.h"
#include "pitches.h"
#include "utils.h"
#include "oled.h"
#include "readers.h"
#include "vars.h"

OLED oled(U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, U8X8_PIN_NONE));
WiFiClient* wifi = new WiFiClientFixed();
HTTPClient* http = new HTTPClient();

Reader* readers[] = {
	//new MFRC522CardReader(PIN_HSPI_SS, PIN_HSPI_RST),
	new PN532Reader(PIN_HSPI_CS0),
	new RDM6300Reader(PIN_HWSERIAL_RX),
};

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

Reader* scannedCardReader;
String scannedCardId = "";
String lastStatusText = "";

bool deposit = false;
int transactionAmountIndex = 0;
int transactionAmountsDebit[] = {-150, -200};
int transactionAmountsCredit[] = {+100, +200, +500, +1000};

volatile bool toggleTransactionAmountDebit = false;
volatile bool toggleTransactionAmountCredit = false;
volatile bool interruptFired = false;
unsigned long timer = 0;

void IRAM_ATTR PN532_IRQ() {
	PN532Reader::irq++;
}

void IRAM_ATTR FeliCaInterruptHandler() {
	if (PN532Reader::toggleMode || scanstate != SCANSTATE_ACTIVE || digitalRead(PIN_INTERRUPT_FELICA)) {
		return;
	}
	PN532Reader::toggleMode = true;
}

void IRAM_ATTR TransactInterruptHandler() {
	if (interruptFired || digitalRead(PIN_INTERRUPT_TRANSACT)) {
		return;
	}
	if (state == STATE_IDLE) {
		interruptFired = true;
		state = STATE_TRANSACT_CARDSCAN;
	}
	else if (state == STATE_TRANSACT_CARDSCAN) {
		PN532Reader::toggleMode = true;
	}
	else {
		return;
	}
}

void IRAM_ATTR LinkInterruptHandler() {
	if (interruptFired || digitalRead(PIN_INTERRUPT_LINK)) {
		return;
	}
	if (state == STATE_IDLE) {
		interruptFired = true;
		state = STATE_LINK_CARD_SCAN;
	}
	else if (state == STATE_LINK_CARD_SCAN) {
		PN532Reader::toggleMode = true;
	}
	else if (state == STATE_TRANSACT_CARDSCAN) {
		toggleTransactionAmountCredit = true;
	}
	else {
		return;
	}
}

void IRAM_ATTR BalanceInterruptHandler() {
	if (interruptFired || digitalRead(PIN_INTERRUPT_BALANCE)) {
		return;
	}
	if (state == STATE_IDLE) {
		interruptFired = true;
		state = STATE_BALANCE_CARDSCAN;
	}
	else if (state == STATE_BALANCE_CARDSCAN) {
		PN532Reader::toggleMode = true;
	}
	else if (state == STATE_TRANSACT_CARDSCAN) {
		toggleTransactionAmountDebit = true;
	}
	else {
		return;
	}
}

void IRAM_ATTR CancelInterruptHandler() {
	if (digitalRead(PIN_INTERRUPT_CANCEL))
		return;
	state = STATE_IDLE;
}

int GetTransactionAmount() {
	if (deposit)
		return transactionAmountsCredit[transactionAmountIndex];
	else
		return transactionAmountsDebit[transactionAmountIndex];
}

String GetDisplayTransactionAmount() {
	String result = "";
	int total = abs(GetTransactionAmount());
	int cents = total % 100;

	if (PN532Reader::mode == PN532_MIFARE_ISO14443A) {
		if (deposit)
			result += "Deposit: ";

		result += total / 100;
		result += ".";
		result += cents;

		if (cents < 10)
			result += "0";

		result += "€";
	}
	else { // TOKYO MODE
		if (deposit)
			result += "Deposit: ";

		result += "¥";
		result += total;
	}

	return result;
}

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

	return false;
}

void updateCardscan() {
	switch (state) {
	case STATE_TRANSACT_CARDSCAN:
	case STATE_LINK_CARD_SCAN:
	case STATE_LINK_CARD_RESCAN:
	case STATE_BALANCE_CARDSCAN:
		if (scanstate == SCANSTATE_ACTIVE)
			return;

		for (Reader* reader : readers) {
			reader->begin();
		}
		scanstate = SCANSTATE_ACTIVE;
		break;
	case STATE_IDLE:
	case STATE_TRANSACT_VERIFY:
	case STATE_LINK_VERIFY:
	case STATE_BALANCE_VERIFY:
	case STATE_RESULT_SUCCESS:
	case STATE_RESULT_FAILURE:
	case STATE_RESULT_DISPLAY:
		if (scanstate == SCANSTATE_IDLE)
			return;

		for (Reader* reader : readers) {
			reader->end();
		}
		scanstate = SCANSTATE_IDLE;
		break;
	}
}

void setup() {
	Serial.begin(115200);
	oled.begin();
	Serial2.begin(115200, SERIAL_8N1, PIN_HWSERIAL_RX, PIN_HWSERIAL_TX);
	SPI.begin(PIN_HSPI_SCLK, PIN_HSPI_MISO, PIN_HSPI_MOSI);

	ledcSetup(0, 5000, 12);
	pinMode(PIN_INTERRUPT_IRQ_PN532, INPUT_PULLUP);
	pinMode(PIN_INTERRUPT_TRANSACT, INPUT_PULLUP);
	pinMode(PIN_INTERRUPT_BALANCE, INPUT_PULLUP);
	pinMode(PIN_INTERRUPT_FELICA, INPUT_PULLUP);
	pinMode(PIN_INTERRUPT_CANCEL, INPUT_PULLUP);
	pinMode(PIN_INTERRUPT_LINK, INPUT_PULLUP);

	SPIFFS.begin(true);
	WiFiSettings.hostname = "afrapay-";
	apiUrl = WiFiSettings.string("AfRApay.Web API", "http://192.168.50.170:5296");
	if (!digitalRead(PIN_INTERRUPT_CANCEL))
		WiFiSettings.portal();
	else
		WiFiSettings.connect();

	for (Reader* reader : readers) {
		reader->init();
	}

	DateTime.setTimeZone(TZ_Europe_Berlin);
	DateTime.begin();
	oled.updateOLED(state);

	attachInterrupt(PIN_INTERRUPT_IRQ_PN532, PN532_IRQ, FALLING);
	attachInterrupt(PIN_INTERRUPT_TRANSACT, TransactInterruptHandler, FALLING);
	attachInterrupt(PIN_INTERRUPT_BALANCE, BalanceInterruptHandler, FALLING);
	attachInterrupt(PIN_INTERRUPT_FELICA, FeliCaInterruptHandler, FALLING);
	attachInterrupt(PIN_INTERRUPT_CANCEL, CancelInterruptHandler, FALLING);
	attachInterrupt(PIN_INTERRUPT_LINK, LinkInterruptHandler, FALLING);
}

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

	updateCardscan();

	if (PN532Reader::toggleMode)
		timer = millis();

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

	switch (state) {
	case STATE_IDLE:
		if (PN532Reader::mode != PN532_MIFARE_ISO14443A)
			PN532Reader::mode = PN532_MIFARE_ISO14443A;

		if (deposit == true)
			deposit = false;

		if (transactionAmountIndex != 0)
			transactionAmountIndex = 0;

		oled.updateOLED(state);
		break;
	case STATE_TRANSACT_CARDSCAN:
		if (toggleTransactionAmountDebit) {
			if (millis() - timer > 250) {
				if (deposit) {
					deposit = false;
					transactionAmountIndex = -1;
				}
				transactionAmountIndex = ++transactionAmountIndex % 2;
				timer = millis();
			}
			toggleTransactionAmountDebit = false;
		}

		if (toggleTransactionAmountCredit) {
			if (millis() - timer > 250) {
				if (!deposit) {
					deposit = true;
					transactionAmountIndex = -1;
				}
				transactionAmountIndex = ++transactionAmountIndex % 4;
				timer = millis();
			}
			toggleTransactionAmountCredit = false;
		}

		for (Reader* reader : readers) {
			if (reader->isNewCardPresent()) {
				scannedCardId = reader->getCardUid();
				lastStatusText = cardIdDisplay(scannedCardId);
				scannedCardReader = reader;
				state = STATE_TRANSACT_VERIFY;
				return;
			}
		}

		if (!cooldownCheck(scanTimeout))
			oled.updateOLED(state, GetDisplayTransactionAmount(), String(cooldownSecondsRemaining(scanTimeout, timer)));

		break;
	case STATE_TRANSACT_VERIFY:
		oled.updateOLED(state, lastStatusText);

		#ifndef DISABLE_BUZZER
		if (PN532Reader::mode == PN532_MIFARE_ISO14443A) {
			tone(PIN_BUZZER, NOTE_A5, 25);
			tone(PIN_BUZZER, NOTE_NONE, 150);
		}
		else {
			tone(PIN_BUZZER, NOTE_FS7, 100);
			tone(PIN_BUZZER, NOTE_NONE, 150);
		}
		#endif

		lastStatusText = cardTransaction(wifi, http, apiUrl, scannedCardId, String(GetTransactionAmount()), scannedCardReader->getReaderName());
		if (lastStatusText.startsWith("S:")) {
			#ifndef DISABLE_BUZZER
			tone(PIN_BUZZER, NOTE_C7, 650);
			#endif
			state = STATE_RESULT_SUCCESS;
		} else {
			#ifndef DISABLE_BUZZER
			tone(PIN_BUZZER, NOTE_CS5, 100);
			tone(PIN_BUZZER, NOTE_NONE, 25);
			tone(PIN_BUZZER, NOTE_CS5, 400);
			#endif
			state = STATE_RESULT_FAILURE;
		}
		lastStatusText = lastStatusText.substring(2);
		timer = millis();
		return;
	case STATE_LINK_CARD_SCAN:
		for (Reader* reader : readers) {
			if (reader->isNewCardPresent()) {
				scannedCardReader = reader;
				scannedCardId = reader->getCardUid();
				if (reader->canHaveUnstableIdentifier()) {
					reader->reset();
					timer = millis();
					state = STATE_LINK_CARD_RESCAN;
					return;
				}
				else {
					lastStatusText = cardIdDisplay(scannedCardId);
					state = STATE_LINK_VERIFY;
					return;
				}
			}
		}
		if (!cooldownCheck(scanTimeout)) {
			if (PN532Reader::mode == PN532_MIFARE_ISO14443A)
				oled.updateOLED(state, "Link", String(cooldownSecondsRemaining(scanTimeout, timer)));
			else
				oled.updateOLED(state, "リンク", String(cooldownSecondsRemaining(scanTimeout, timer)), true);
		}
		break;
	case STATE_LINK_CARD_RESCAN:
		if (scannedCardReader->isNewCardPresent()) {
			if (scannedCardId == scannedCardReader->getCardUid()) {
				lastStatusText = cardIdDisplay(scannedCardId);
				state = STATE_LINK_VERIFY;
				return;
			}
			else {
				lastStatusText = "Unstable identifier";
				state = STATE_RESULT_FAILURE;
				timer = millis();
				return;
			}
		}
		if (!cooldownCheck(scanTimeout)) {
			if (PN532Reader::mode == PN532_MIFARE_ISO14443A)
				oled.updateOLED(state, "Link - rescan ", String(cooldownSecondsRemaining(scanTimeout, timer)));
			else
				oled.updateOLED(state, "リンク - rescan ", String(cooldownSecondsRemaining(scanTimeout, timer)), true);
		}
		break;
	case STATE_LINK_VERIFY:
		oled.updateOLED(state, lastStatusText);

		#ifndef DISABLE_BUZZER
		if (PN532Reader::mode == PN532_MIFARE_ISO14443A) {
			tone(PIN_BUZZER, NOTE_A5, 25);
			tone(PIN_BUZZER, NOTE_NONE, 150);
		}
		else {
			tone(PIN_BUZZER, NOTE_FS7, 100);
			tone(PIN_BUZZER, NOTE_NONE, 150);
		}
		#endif

		lastStatusText = cardLink(wifi, http, apiUrl, scannedCardId, scannedCardReader->getReaderName());
		if (lastStatusText.startsWith("S:")) {
			#ifndef DISABLE_BUZZER
			tone(PIN_BUZZER, NOTE_C6, 100);
			tone(PIN_BUZZER, NOTE_NONE, 10);
			tone(PIN_BUZZER, NOTE_C6, 100);
			#endif
			state = STATE_RESULT_SUCCESS;
		} else {
			#ifndef DISABLE_BUZZER
			tone(PIN_BUZZER, NOTE_CS5, 100);
			tone(PIN_BUZZER, NOTE_NONE, 25);
			tone(PIN_BUZZER, NOTE_CS5, 400);
			#endif
			state = STATE_RESULT_FAILURE;
		}
		lastStatusText = lastStatusText.substring(2);
		timer = millis();
		return;
	case STATE_BALANCE_CARDSCAN:
		for (Reader* reader : readers) {
			if (reader->isNewCardPresent()) {
				scannedCardId = reader->getCardUid();
				scannedCardReader = reader;
				lastStatusText = cardIdDisplay(scannedCardId);
				state = STATE_BALANCE_VERIFY;
				return;
			}
		}
		if (!cooldownCheck(scanTimeout)) {
			if (PN532Reader::mode == PN532_MIFARE_ISO14443A)
				oled.updateOLED(state, "Balance", String(cooldownSecondsRemaining(scanTimeout, timer)));
			else
				oled.updateOLED(state, "残高", String(cooldownSecondsRemaining(scanTimeout, timer)), true);
		}
		break;
	case STATE_BALANCE_VERIFY:
		oled.updateOLED(state, lastStatusText);

		#ifndef DISABLE_BUZZER
		if (PN532Reader::mode == PN532_MIFARE_ISO14443A) {
			tone(PIN_BUZZER, NOTE_A5, 25);
			tone(PIN_BUZZER, NOTE_NONE, 150);
		}
		else {
			tone(PIN_BUZZER, NOTE_FS7, 100);
			tone(PIN_BUZZER, NOTE_NONE, 150);
		}
		#endif

		lastStatusText = cardBalance(wifi, http, apiUrl, scannedCardId, scannedCardReader->getReaderName());
		if (lastStatusText.startsWith("S:")) {
			lastStatusText = lastStatusText + ":" + cardIdDisplay(scannedCardId);
			state = STATE_RESULT_DISPLAY;
		} else {
			#ifndef DISABLE_BUZZER
			tone(PIN_BUZZER, NOTE_CS5, 100);
			tone(PIN_BUZZER, NOTE_NONE, 25);
			tone(PIN_BUZZER, NOTE_CS5, 400);
			#endif
			state = STATE_RESULT_FAILURE;
		}
		lastStatusText = lastStatusText.substring(2);
		timer = millis();
	case STATE_RESULT_SUCCESS:
		if (cooldownSecondsRemaining(gTimeout, timer) <= 4) {
			lastStatusText = lastStatusText + ":" + cardIdDisplay(scannedCardId);
			state = STATE_RESULT_DISPLAY;
		}
	case STATE_RESULT_FAILURE:
	case STATE_RESULT_DISPLAY:
		if (!cooldownCheck(gTimeout)) {
			if (PN532Reader::mode == PN532_MIFARE_ISO14443A)
				oled.updateOLED(state, lastStatusText, String(cooldownSecondsRemaining(gTimeout, timer)));
			else
				oled.updateOLED(state, lastStatusText, String(cooldownSecondsRemaining(gTimeout, timer)), true);
		}
		break;
	}

	delay(50);
}
#pragma clang diagnostic pop
