/*
	Copyright 2022 flyinghead

	This file is part of Flycast.

    Flycast is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 2 of the License, or
    (at your option) any later version.

    Flycast is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Flycast.  If not, see <https://www.gnu.org/licenses/>.
 */
#include "card_reader.h"
#include "oslib/oslib.h"
#include "hw/sh4/modules/modules.h"
#include "hw/maple/maple_cfg.h"
#include "hw/maple/maple_devs.h"
#include <deque>
#include <memory>
#include <cerrno>

namespace card_reader {

class CardReaderWriter
{
public:
	virtual ~CardReaderWriter() = default;

	void insertCard()
	{
		cardInserted = loadCard();
		if (cardInserted)
			INFO_LOG(NAOMI, "Card inserted");
	}

protected:
	virtual bool loadCard() = 0;

	bool loadCard(u8 *cardData, u32 len)
	{
		std::string path = hostfs::getArcadeFlashPath() + ".card";
		FILE *fp = nowide::fopen(path.c_str(), "rb");
		if (fp == nullptr)
			return false;

		INFO_LOG(NAOMI, "Loading card file from %s", path.c_str());
		if (fread(cardData, 1, len, fp) != len)
			WARN_LOG(NAOMI, "Truncated or empty card file: %s" ,path.c_str());
		fclose(fp);

		return true;
	}

	void saveCard(const u8 *cardData, u32 len)
	{
		std::string path = hostfs::getArcadeFlashPath() + ".card";
		FILE *fp = nowide::fopen(path.c_str(), "wb");
		if (fp == nullptr)
		{
			WARN_LOG(NAOMI, "Can't create card file %s: errno %d", path.c_str(), errno);
			return;
		}
		INFO_LOG(NAOMI, "Saving card file to %s", path.c_str());
		if (fwrite(cardData, 1, len, fp) != len)
			WARN_LOG(NAOMI, "Truncated write to file: %s", path.c_str());
		fclose(fp);
	}

	template<typename T>
	static u8 calcCrc(T begin, T end)
	{
		u32 crc = 0;
		for (auto it = begin; it != end; it++)
			crc ^= *it;
		return crc;
	}

	bool cardInserted = false;
	std::deque<u8> outBuffer;
	std::vector<u8> inBuffer;

	static constexpr u8 STX = 2;
	static constexpr u8 ETX = 3;
	static constexpr u8 ENQ = 5;
	static constexpr u8 ACK = 6;
};

/*
	Sanwa CRP-1231BR-10 card reader/writer protocol (from my good friend Metallic)
	used in InitialD and Derby Owners Club

>>>	SEND PKT: [START 02][LEN][CMD][0][0][0]{optional data}[STOP 03][CRC]
<<<	RECV ACK: [OK 06]
>>>	SEND REQ: [RQ 05]
<<<	RECV PKT: [START 02][LEN][CMD][RESULT1][RESULT2][RESULT3]{optional data}[STOP 03][CRC]
<<< RECV ERR: [15]
	RESULT1 (SENSORS): binary value SSTCCCCC
		S - Shutter state:
			0 - "ERROR"
			1 - "CLOSED"
			2 - "OPEN"
			3 - "NOW" (both open and close sensors)
		T - Stocker (card dispenser) state:
			0 - "NO" (empty)
			1 - "OK" (full)
		C - Card sensors:
			0     - "NO CARD"
			1     - "CARD EXIST (ENTER)" (card inserted in front of shutter)
			18    - "CARD EXIST" (card loaded inside of reader)
			other - "CARD EXIST (OTHER)"
	RESULT2: char 01235QRSTUV`
		Error ENUM "OK", "READ ERR", "WRITE ERR", "STOP ERR", "PRINT ERR", "READERR T1", "READERR T2", "READERR T3",
		 "READERR T12", "READERR T13", "READERR T23", "SHUT ERR"
	RESULT3 (CMD_STATE): char 02345
		State ENUM "OK", "DISABLE", "BUSY", "CARD WAIT", "NO CARD IN BOX"

	Protocol dumps and more:
	https://www.arcade-projects.com/threads/naomi-2-chihiro-triforce-card-reader-emulator-initial-d3-wmmt-mario-kart-f-zero-ax.814/

	Bits from YACardEmu: https://github.com/GXTX/YACardEmu/
	Copyright (C) 2020-2023 wutno (https://github.com/GXTX)
    Copyright (C) 2022-2023 tugpoat (https://github.com/tugpoat)
*/

class SanwaCRP1231BR : public CardReaderWriter, public SerialPort::Pipe
{
public:
	void write(u8 b) override
	{
		if (inBuffer.empty() && b == ENQ)
		{
			DEBUG_LOG(NAOMI, "Received RQ(5)");
			handleCommand();
			return;
		}
		inBuffer.push_back(b);
		if (inBuffer.size() >= 3)
		{
			if (inBuffer[0] != STX)
			{
				INFO_LOG(NAOMI, "Unexpected cmd start byte %x", inBuffer[0]);
				inBuffer.clear();
				return;
			}
			u32 len = inBuffer[1];
			if (inBuffer.size() < len + 2)
			{
				if (inBuffer.size() == 256)
				{
					WARN_LOG(NAOMI, "Card reader buffer overflow");
					inBuffer.clear();
				}
				return;
			}
			u32 crc = calcCrc(inBuffer.begin() + 1, inBuffer.end() - 1);
			if (crc != inBuffer.back())
			{
				INFO_LOG(NAOMI, "Wrong crc: expected %x got %x", crc, inBuffer.back());
				inBuffer.clear();
				return;
			}
			DEBUG_LOG(NAOMI, "Received cmd %x len %d", inBuffer[2], inBuffer[1]);
			outBuffer.push_back(ACK);
			rxCommandLen = std::min(inBuffer.size() - 3, sizeof(rxCommand));
			memcpy(rxCommand, &inBuffer[2], rxCommandLen);
			inBuffer.clear();
		}
	}

	u8 read() override
	{
		if (outBuffer.empty())
			return 0;
		u8 b = outBuffer.front();
		outBuffer.pop_front();
		DEBUG_LOG(NAOMI, "Sending %x", b);
		return b;
	}

	int available() override {
		return outBuffer.size();
	}

protected:
	enum Commands
	{
		CARD_INIT			= 0x10,
		CARD_GET_CARD_STATE	= 0x20,
		CARD_CANCEL			= 0x40,
		CARD_LOAD_CARD		= 0xB0,
		CARD_CLEAN_CARD		= 0xA0,
		CARD_READ			= 0x33,
		CARD_WRITE			= 0x53,
		CARD_PRINT			= 0x7C,
		CARD_PRINT_SETTINGS	= 0x78,
		CARD_REGISTER_FONT	= 0x7A,
		CARD_ERASE_PRINT	= 0x7D,
		CARD_DOOR			= 0xD0,
		CARD_EJECT			= 0x80,
		CARD_NEW            = 0xB0,
	};

	bool loadCard() override {
		return CardReaderWriter::loadCard(cardData, sizeof(cardData));
	}

	virtual u8 getStatus1()
	{
		return ((doorOpen ? 2 : 1) << 6) | 0x20 | (cardInserted ? 0x18 : 0);
	}

	void handleCommand()
	{
		if (rxCommandLen == 0)
			return;
		outBuffer.push_back(STX);
		u32 crcIdx = outBuffer.size();
		u8 status1 = getStatus1();
		u8 status2 = '0';
		u8 status3 = '0';
		switch (rxCommand[0])
		{
		case CARD_DOOR:
			doorOpen = rxCommand[4] == '1';
			INFO_LOG(NAOMI, "Door %s", doorOpen ? "open" : "closed");
			status1 = getStatus1();
			break;

		case CARD_NEW:
			INFO_LOG(NAOMI, "New card");
			cardInserted = true;
			doorOpen = false;
			status1 = getStatus1();
			break;

		case CARD_WRITE:
			// 4: mode ('0': read 0x45 bytes, '1': variable length write 1-47 bytes)
			// 5: parity ('0': 7-bit parity, '1': 8-bit no parity)
			// 6: track (see below)
			INFO_LOG(NAOMI, "Card write mode %c parity %c track %c", rxCommand[4], rxCommand[5], rxCommand[6]);
			switch (rxCommand[6])
			{
			case '0': // track 1
				memcpy(cardData, &rxCommand[7], TRACK_SIZE);
				break;
			case '1': // track 2
				memcpy(cardData + TRACK_SIZE, &rxCommand[7], TRACK_SIZE);
				break;
			case '2': // track 3
				memcpy(cardData + TRACK_SIZE * 2, &rxCommand[7], TRACK_SIZE);
				break;
			case '3': // track 1 & 2
				memcpy(cardData, &rxCommand[7], TRACK_SIZE * 2);
				break;
			case '4': // track 1 & 3
				memcpy(cardData, &rxCommand[7], TRACK_SIZE);
				memcpy(cardData + TRACK_SIZE * 2, &rxCommand[7 + TRACK_SIZE], TRACK_SIZE);
				break;
			case '5': // track 2 & 3
				memcpy(cardData + TRACK_SIZE, &rxCommand[7], TRACK_SIZE * 2);
				break;
			case '6': // track 1 2 & 3
				memcpy(cardData, &rxCommand[7], TRACK_SIZE * 3);
				break;
			default:
				WARN_LOG(NAOMI, "Unknown track# %02x", rxCommand[6]);
				break;
			}
			saveCard(cardData, sizeof(cardData));
			break;

		case CARD_READ:
			// 4: mode ('0': read 0x45 bytes, '1': variable length read 1-47 bytes, '2': card capture, pull in card?)
			// 5: parity ('0': 7-bit parity, '1': 8-bit no parity)
			// 6: track (see below)
			INFO_LOG(NAOMI, "Card read mode %c parity %c track %c", rxCommand[4], rxCommand[5], rxCommand[6]);
			if (!cardInserted || doorOpen)
				status3 = cardInserted ? '0' : '4';
			break;

		case CARD_EJECT:
			NOTICE_LOG(NAOMI, "Card ejected");
			if (cardInserted)
				os_notify("Card ejected", 2000);
			cardInserted = false;
			status1 = getStatus1();
			break;

		case CARD_CANCEL:
		case CARD_GET_CARD_STATE:
		case CARD_INIT:
		case CARD_REGISTER_FONT:
		case CARD_PRINT_SETTINGS:
		case CARD_PRINT:
		case CARD_CLEAN_CARD:
			break;

		default:
			WARN_LOG(NAOMI, "Unknown command %x", rxCommand[0]);
			break;
		}
		outBuffer.push_back(ACK);
		outBuffer.push_back(rxCommand[0]);
		outBuffer.push_back(status1);
		outBuffer.push_back(status2);
		outBuffer.push_back(status3);
		if (rxCommand[0] == CARD_READ && cardInserted && !doorOpen && rxCommand[4] != '2')
		{
			u32 idx = 0;
			u32 size = TRACK_SIZE;
			switch (rxCommand[6])
			{
			case '0': // track 1
				break;
			case '1': // track 2
				idx = TRACK_SIZE;
				break;
			case '2': // track 3
				idx = TRACK_SIZE * 2;
				break;
			case '3': // track 1 & 2
				size = TRACK_SIZE * 2;
				break;
			case '4': // track 1 & 3
				for (u32 i = 0; i < TRACK_SIZE; i++)
					outBuffer.push_back(cardData[i]);
				outBuffer[crcIdx] += TRACK_SIZE;
				idx = TRACK_SIZE * 2;
				break;
			case '5': // track 2 & 3
				idx = TRACK_SIZE;
				size = TRACK_SIZE * 2;
				break;
			case '6': // track 1 2 & 3
				size = TRACK_SIZE * 3;
				break;
			default:
				WARN_LOG(NAOMI, "Unknown track# %02x", rxCommand[6]);
				size = 0;
				break;
			}
			for (u32 i = 0; i < size; i++)
				outBuffer.push_back(cardData[idx + i]);
			outBuffer[crcIdx] += size;
		}
		outBuffer.push_back(ETX);
		outBuffer.push_back(calcCrc(outBuffer.begin() + crcIdx, outBuffer.end()));
	}

	u8 rxCommand[256];
	u32 rxCommandLen = 0;

	static constexpr u32 TRACK_SIZE = 0x45;
	u8 cardData[TRACK_SIZE * 3];
	bool doorOpen = false;
};

class SanwaCRP1231LR : public SanwaCRP1231BR
{
	u8 getStatus1() override
	{
		// '0'	no card
		// '1'	pos magnetic read/write
		// '2'	pos thermal printer
		// '3'	pos thermal dispenser
		// '4'	ejected not removed
		return cardInserted ? '1' : '0';
	}
};

// Hooked to the SH4 SCIF serial port
class InitialDCardReader final : public SanwaCRP1231BR
{
public:
	InitialDCardReader() {
		SCIFSerialPort::Instance().setPipe(this);
	}

	~InitialDCardReader() override {
		SCIFSerialPort::Instance().setPipe(nullptr);
	}
};

// Hooked to the MIE via a 838-13661 RS232/RS422 converter board
class DerbyBRCardReader final : public SanwaCRP1231BR
{
public:
	DerbyBRCardReader() {
		getMieDevice()->setPipe(this);
	}

	~DerbyBRCardReader() override {
		getMieDevice()->setPipe(nullptr);
	}
};

class DerbyLRCardReader final : public SanwaCRP1231LR
{
public:
	DerbyLRCardReader() {
		getMieDevice()->setPipe(this);
	}

	~DerbyLRCardReader() override {
		getMieDevice()->setPipe(nullptr);
	}
};

/*
	Club Kart - Sanwa CR-1231R

>>>	SEND CMD: [START 02][CMD char1][CMD char2]{parameter char}{data}[STOP 03][CRC]
<<<	RECV ACK: [OK 06] or [ERR 15]
<<<	RECV STX: [START 02]{REPLY}{data}[STOP 03][CRC]
	note: it seems reply packet sent only after command fully completed or error happened

	REPLY: 2chars
		OK - RESULT_OK
		O1 - RESULT_CANCEL_INSERT
		N0 - ERROR_CONNECT / Unknown Error
		N1 - ERROR_COMMAND / Connection Error
		N2 - ERROR_MOTOR / Mechanic Error 1
		N3 - ERROR_HEAD_UPDOWN / Mechanic Error 2
		N4 - ERROR_CARD_STUCK / Card Stuffed
		N5 - ERROR_VERIFY / OK ????
		N6 - ERROR_HEAD_TEMP / Mechanic Error 3
		N7 - ERROR_CARD_EMPTY / Card Empty
		N8 - ERROR_CARD_LOAD / Draw Card Error
		N9 - ERROR_NO_HOPPER / Card Empty
		NA - ERROR_CARD_PRESENT
		NB - ERROR_CARD_EJECT
		NC - ERROR_CANT_CANCEL
		ND - ERROR_NOT_INSERT
		NE - ERROR_NOT_WAIT
		NF - ERROR_BAD_CARD

	CMD SS REPLY: 6 ASCII characters
		5chars '0'/'1' - Card Sensors Status, MSB first
			0
			10 18
			1C C E 8 7 3
			other
		1char '0'/'1'  - Dispenser Status
			'0' - Empty
			'1' - Full

Commands:
IN   - init
CA   - cancel command
OT0  - eject card
HI   - get new card from dispenser
CL   - cleaning
RT5  - unknown
RL   - read data/load card into reader
WL   - write data (followed by 69 bytes of data)
SS   - get status

reply for commands is simple
06
02 'O' 'K' 03 crc
except
- RL command, which have 69 bytes of card data after OK (or no any reply if no card insterted)
- SS command, which is
06
02 '0/1' '0/1' '0/1' '0/1' '0/1' '0/1' 03 crc
there 0/1 - encoded in char binary value described earlier
 */
class ClubKartCardReader : public CardReaderWriter, SerialPort::Pipe
{
public:
	ClubKartCardReader() {
		SCIFSerialPort::Instance().setPipe(this);
	}
	~ClubKartCardReader() override {
		SCIFSerialPort::Instance().setPipe(nullptr);
	}

	void write(u8 data) override
	{
		inBuffer.push_back(data);
		if (inBuffer.size() == 5)
		{
			if ((inBuffer[1] != 'W' || inBuffer[2] != 'L') && inBuffer[2] != 'T')
			{
				handleCommand();
				inBuffer.clear();
			}
		}
		else if (inBuffer.size() == 6 && inBuffer[2] == 'T') // OT0, RT5
		{
			handleCommand();
			inBuffer.clear();
		}
		else if (inBuffer.size() == TRACK_SIZE + 5) // WL
		{
			handleCommand();
			inBuffer.clear();
		}
	}

	int available() override {
		return outBuffer.size();
	}

	u8 read() override
	{
		if (outBuffer.empty())
			return 0;
		u8 b = outBuffer.front();
		outBuffer.pop_front();
		return b;
	}

private:
	enum Commands {
		CARD_INIT,
		CARD_CANCEL_CMD,
		CARD_EJECT,
		CARD_NEW,
		CARD_CLEAN,
		CARD_RT5,
		CARD_READ,
		CARD_WRITE,
		CARD_STATUS,

		CARD_MAX
	};
	static const u8 CommandBytes[][2];

	bool loadCard() override
	{
		bool rc = CardReaderWriter::loadCard(cardData, sizeof(cardData));
		if (rc && readPending)
		{
			sendReply(CARD_READ);
			readPending = false;
		}
		return rc;
	}

	void handleCommand()
	{
		readPending = false;
		int cmd;
		for (cmd = 0; cmd < CARD_MAX; cmd++)
			if (inBuffer[1] == CommandBytes[cmd][0] && inBuffer[2] == CommandBytes[cmd][1])
				break;
		if (cmd == CARD_MAX)
		{
			WARN_LOG(NAOMI, "Unhandled command '%c%c'", inBuffer[1], inBuffer[2]);
			return;
		}
		u32 crc = calcCrc(inBuffer.begin() + 1, inBuffer.end() - 1);
		if (crc != inBuffer.back())
		{
			WARN_LOG(NAOMI, "Wrong crc: expected %x got %x", crc, inBuffer.back());
			return;
		}
		outBuffer.push_back(ACK);
		switch (cmd)
		{
		case CARD_WRITE:
			INFO_LOG(NAOMI, "Card write");
			for (u32 i = 0; i < sizeof(cardData); i++)
				cardData[i] = inBuffer[i + 3];
			saveCard(cardData, sizeof(cardData));
			break;
		case CARD_READ:
			INFO_LOG(NAOMI, "Card read");
			if (!cardInserted) {
				readPending = true;
				return;
			}
			break;
		case CARD_EJECT:
			NOTICE_LOG(NAOMI, "Card ejected");
			if (cardInserted)
				os_notify("Card ejected", 2000);
			cardInserted = false;
			break;
		case CARD_NEW:
			INFO_LOG(NAOMI, "New card");
			cardInserted = true;
			break;
		case CARD_INIT:
			DEBUG_LOG(NAOMI, "Card init");
			break;
		case CARD_CANCEL_CMD:
			DEBUG_LOG(NAOMI, "Cancel cmd");
			break;
		case CARD_CLEAN:
			DEBUG_LOG(NAOMI, "Card clean");
			break;
		case CARD_RT5:
			DEBUG_LOG(NAOMI, "Card RT5");
			break;
		case CARD_STATUS:
			DEBUG_LOG(NAOMI, "Card status (cardInserted %d)", cardInserted);
			break;
		}
		sendReply(cmd);
	}

	void sendReply(int cmd)
	{
		outBuffer.push_back(STX);
		u32 crcIndex = outBuffer.size();
		if (cmd == CARD_STATUS)
		{
			outBuffer.push_back('0');
			outBuffer.push_back('0');
			outBuffer.push_back('0');
			outBuffer.push_back(cardInserted ? '1' : '0');
			outBuffer.push_back(cardInserted ? '1' : '0');
			outBuffer.push_back('1'); // dispenser full
		}
		else
		{
			outBuffer.push_back('O');
			outBuffer.push_back('K');
			if (cmd == CARD_READ) {
				for (u32 i = 0; i < sizeof(cardData); i++)
					outBuffer.push_back(cardData[i]);
			}
		}
		outBuffer.push_back(ETX);
		outBuffer.push_back(calcCrc(outBuffer.begin() + crcIndex, outBuffer.end()));
	}

	static constexpr u32 TRACK_SIZE = 0x45;
	u8 cardData[TRACK_SIZE];

	bool readPending = false;
};

const u8 ClubKartCardReader::CommandBytes[][2]
{
	{ 'I', 'N' },	// init
	{ 'C', 'A' },	// cancel command
	{ 'O', 'T' },	// ...0  - eject card
	{ 'H', 'I' },	// get new card from dispenser
	{ 'C', 'L' },	// cleaning
	{ 'R', 'T' },	// ...5  - unknown
	{ 'R', 'L' },	// read data/load card into reader
	{ 'W', 'L' },	// write data (followed by 69 bytes of data)
	{ 'S', 'S' },	// get status
};

static std::unique_ptr<CardReaderWriter> cardReader;

void initdInit() {
	term();
	cardReader = std::make_unique<InitialDCardReader>();
}

void derbyInit()
{
	term();
	if (settings.content.gameId == " DERBY OWNERS CLUB WE ---------")
		cardReader = std::make_unique<DerbyBRCardReader>();
	else
		cardReader = std::make_unique<DerbyLRCardReader>();
}

void clubkInit() {
	term();
	cardReader = std::make_unique<ClubKartCardReader>();
}

void term() {
	cardReader.reset();
}

class BarcodeReader final : public SerialPort::Pipe
{
public:
	BarcodeReader(char eotChar) : eotChar(eotChar) {
		SCIFSerialPort::Instance().setPipe(this);
	}

	~BarcodeReader() override {
		SCIFSerialPort::Instance().setPipe(nullptr);
	}

	int available() override {
		return toSend.size();
	}

	u8 read() override
	{
		u8 data = toSend.front();
		toSend.pop_front();
		return data;
	}

	void insertCard()
	{
		if (toSend.size() >= 32)
			return;
		INFO_LOG(NAOMI, "Card read: %s", card.c_str());
		std::string data = card + eotChar;
		toSend.insert(toSend.end(), (const u8 *)&data[0], (const u8 *)(&data.back() + 1));
		SCIFSerialPort::Instance().updateStatus();
	}

	std::string getCard() const	{
		return card;
	}

	void setCard(const std::string& card) {
		this->card = card;
	}

private:
	const char eotChar;
	std::deque<u8> toSend;
	std::string card;
};

static std::unique_ptr<BarcodeReader> barcodeReader;

void barcodeInit()
{
	if (settings.content.gameId == "BTR 2K9 VER 1.004")	// System SP Battle Racer
		barcodeReader = std::make_unique<BarcodeReader>('\r');
	else
		barcodeReader = std::make_unique<BarcodeReader>('*');
}

void barcodeTerm() {
	barcodeReader.reset();
}

bool barcodeAvailable() {
	return barcodeReader != nullptr;
}

std::string barcodeGetCard()
{
	if (barcodeReader != nullptr)
		return barcodeReader->getCard();
	else
		return "";
}

void barcodeSetCard(const std::string& card)
{
	if (barcodeReader != nullptr)
		barcodeReader->setCard(card);
}

void insertCard(int playerNum)
{
	if (cardReader != nullptr)
		cardReader->insertCard();
	else if (barcodeReader != nullptr)
		barcodeReader->insertCard();
	else
		insertRfidCard(playerNum);
}

bool readerAvailable()
{
	return cardReader != nullptr
			|| barcodeAvailable()
			|| getRfidCardData(0) != nullptr;
}

}
