/*
	Copyright 2025 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 "types.h"
#include <asio.hpp>
#include "netservice.h"
#include "util/tsqueue.h"
#include "oslib/oslib.h"
#include "emulator.h"
#include "hw/bba/bba.h"
#include "cfg/option.h"
#include "stdclass.h"
#ifndef LIBRETRO
#include "cfg/cfg.h"
#endif

#include <thread>
#include <memory>
#include <array>
#include <chrono>
#ifndef __ANDROID__
//#define WIRESHARK_DUMP 1
#endif

namespace net::modbba
{

static TsQueue<u8> toModem;

class DCNetService : public Service
{
public:
	bool start() override;
	void stop() override;

	void writeModem(u8 b) override;
	int readModem() override;
	int modemAvailable() override;

	void receiveEthFrame(const u8 *frame, u32 size) override;
};

class PPPSocket
{
public:
	PPPSocket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint,
			const std::string& endpointName = "")
		: socket(io_context)
	{
		asio::error_code ec;
		socket.connect(endpoint, ec);
		if (ec)
			throw FlycastException(ec.message().c_str());
		os_notify("Connected to DCNet with modem", 5000, endpointName.c_str());
		receive();
	}

	virtual ~PPPSocket() {
		if (dumpfp != nullptr)
			fclose(dumpfp);
	}

	void send(u8 b)
	{
		if (sendBufSize == sendBuffer.size()) {
			WARN_LOG(NETWORK, "PPP output buffer overflow");
			return;
		}
		sendBuffer[sendBufSize++] = b;
		doSend();
	}

protected:
	virtual void receive()
	{
		socket.async_read_some(asio::buffer(recvBuffer),
			[this](const std::error_code& ec, size_t len)
			{
				if (ec || len == 0)
				{
					if (ec)
						ERROR_LOG(NETWORK, "Receive error: %s", ec.message().c_str());
					close();
					return;
				}
				pppdump(recvBuffer.data(), len, false);
				for (size_t i = 0; i < len; i++)
					toModem.push(recvBuffer[i]);
				receive();
			});
	}

	void doSend()
	{
		if (sending)
			return;
		pppdump(sendBuffer.data(), sendBufSize, true);
		sending = true;
		asio::async_write(socket, asio::buffer(sendBuffer, sendBufSize),
			[this](const std::error_code& ec, size_t len)
			{
				if (ec)
				{
					ERROR_LOG(NETWORK, "Send error: %s", ec.message().c_str());
					close();
					return;
				}
				sending = false;
				sendBufSize -= len;
				if (sendBufSize > 0) {
					memmove(&sendBuffer[0], &sendBuffer[len], sendBufSize);
					doSend();
				}
			});
	}

	void close() {
		std::error_code ignored;
		socket.close(ignored);
	}

	void pppdump(uint8_t *buf, int len, bool egress)
	{
#ifdef WIRESHARK_DUMP
		if (!len)
			return;
		if (dumpfp == nullptr)
		{
			dumpfp = fopen("ppp.dump", "a");
			if (dumpfp == nullptr)
	            return;
			time_t now;
			time(&now);
			u32 reset_time = ntohl((u32)now);
	        fputc(7, dumpfp);                    // Reset time
	        fwrite(&reset_time, sizeof(reset_time), 1, dumpfp);
	        dump_last_time_ms = getTimeMs();
		}

		u32 delta = getTimeMs() / 100 - dump_last_time_ms / 100;
		if (delta < 256) {
			fputc(6, dumpfp);					// Time step (short)
			fwrite(&delta, 1, 1, dumpfp);
		}
		else
		{
			delta = ntohl(delta);
			fputc(5, dumpfp);					// Time step (long)
			fwrite(&delta, sizeof(delta), 1, dumpfp);
		}
		dump_last_time_ms = getTimeMs();
		fputc(egress ? 1 : 2, dumpfp);			// Sent/received data
		uint16_t slen = htons(len);
		fwrite(&slen, 2, 1, dumpfp);
		fwrite(buf, 1, len, dumpfp);
#endif
	}

	asio::ip::tcp::socket socket;
	std::array<u8, 1542> recvBuffer;
	std::array<u8, 1542> sendBuffer;
	u32 sendBufSize = 0;
	bool sending = false;

	FILE *dumpfp = nullptr;
	u64 dump_last_time_ms;
};

class PowerSmashPPPSocket : public PPPSocket
{
public:
	PowerSmashPPPSocket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint,
			const std::string& endpointName = "")
		: PPPSocket(io_context, endpoint, endpointName) {}

private:
	void receive() override
	{
		socket.async_read_some(asio::buffer(&recvBuffer[recvBufSize], recvBuffer.size() - recvBufSize),
			[this](const std::error_code& ec, size_t len)
			{
				if (ec || len == 0)
				{
					if (ec)
						ERROR_LOG(NETWORK, "Receive error: %s", ec.message().c_str());
					close();
					return;
				}
				recvBufSize += len;
				while (recvBufSize != 0)
				{
					u32 frameSize = 0;
					for (u32 i = 1; i < recvBufSize; i++)
					{
						if (recvBuffer[i] == '~') {
							frameSize = i + 1;
							break;
						}
					}
					if (frameSize == 0)
						break;
					pppdump(recvBuffer.data(), frameSize, false);
					// Power Smash requires both start and end Flag Sequences
					if (recvBuffer[0] != '~')
						toModem.push('~');
					for (size_t i = 0; i < frameSize; i++)
						toModem.push(recvBuffer[i]);
					recvBufSize -= frameSize;
					if (recvBufSize != 0)
						memmove(&recvBuffer[0], &recvBuffer[frameSize], recvBufSize);
				}
				receive();
			});
	}

	u32 recvBufSize = 0;
};

class EthSocket
{
public:
	EthSocket(asio::io_context& io_context, const asio::ip::tcp::endpoint& endpoint,
			const std::string& endpointName = "")
		: socket(io_context)
	{
		asio::error_code ec;
		socket.connect(endpoint, ec);
		if (ec)
			throw FlycastException(ec.message().c_str());
		os_notify("Connected to DCNet with Ethernet", 5000, endpointName.c_str());
		receive();
		u8 prolog[] = { 'D', 'C', 'N', 'E', 'T', 1 };
		send(prolog, sizeof(prolog));
	}

	~EthSocket() {
		if (dumpfp != nullptr)
			fclose(dumpfp);
	}

	void send(const u8 *frame, u32 size)
	{
		if (sendBufferIdx + size >= sendBuffer.size()) {
			WARN_LOG(NETWORK, "Dropped out frame (buffer:%d + %d bytes). Increase send buffer size\n", sendBufferIdx, size);
			return;
		}
		if (size >= 32) // skip prolog
			ethdump(frame, size);
		*(u16 *)&sendBuffer[sendBufferIdx] = size;
		sendBufferIdx += 2;
		memcpy(&sendBuffer[sendBufferIdx], frame, size);
		sendBufferIdx += size;
		doSend();
	}

private:
	using iterator = asio::buffers_iterator<asio::const_buffers_1>;

	std::pair<iterator, bool>
	static packetMatcher(iterator begin, iterator end)
	{
		if (end - begin < 3)
			return std::make_pair(begin, false);
		iterator i = begin;
		uint16_t len = (uint8_t)*i++;
		len |= uint8_t(*i++) << 8;
		len += 2;
		if (end - begin < len)
			return std::make_pair(begin, false);
		return std::make_pair(begin + len, true);
	}

	void receive()
	{
		asio::async_read_until(socket, asio::dynamic_vector_buffer(recvBuffer), packetMatcher,
			[this](const std::error_code& ec, size_t len)
			{
				if (ec || len == 0)
				{
					if (ec)
						ERROR_LOG(NETWORK, "Receive error: %s", ec.message().c_str());
					std::error_code ignored;
					socket.close(ignored);
					return;
				}
				/*
				verify(len - 2 == *(u16 *)&recvBuffer[0]);
				printf("In frame: dest %02x:%02x:%02x:%02x:%02x:%02x "
					   "src %02x:%02x:%02x:%02x:%02x:%02x, ethertype %04x, size %d bytes\n",
					   recvBuffer[2], recvBuffer[3], recvBuffer[4], recvBuffer[5], recvBuffer[6], recvBuffer[7],
					   recvBuffer[8], recvBuffer[9], recvBuffer[10], recvBuffer[11], recvBuffer[12], recvBuffer[13],
					*(u16 *)&recvBuffer[14], (int)len - 2);
				*/
				ethdump(&recvBuffer[2], len - 2);
				bba_recv_frame(&recvBuffer[2], len - 2);
				if (len < recvBuffer.size())
					recvBuffer.erase(recvBuffer.begin(), recvBuffer.begin() + len);
				else
					recvBuffer.clear();
				receive();
			});
	}

	void doSend()
	{
		if (sending)
			return;
		sending = true;
		asio::async_write(socket, asio::buffer(sendBuffer, sendBufferIdx),
			[this](const std::error_code& ec, size_t len)
			{
				sending = false;
				if (ec)
				{
					ERROR_LOG(NETWORK, "Send error: %s", ec.message().c_str());
					std::error_code ignored;
					socket.close(ignored);
					return;
				}
				sendBufferIdx -= len;
				if (sendBufferIdx != 0) {
					memmove(sendBuffer.data(), sendBuffer.data() + len, sendBufferIdx);
					doSend();
				}
			});
	}

	void ethdump(const uint8_t *frame, int size)
	{
#ifdef WIRESHARK_DUMP
		if (dumpfp == nullptr)
		{
			dumpfp = fopen("bba.pcapng", "wb");
			if (dumpfp == nullptr)
			{
				const char *home = getenv("HOME");
				if (home != nullptr)
				{
					std::string path = home + std::string("/bba.pcapng");
					dumpfp = fopen(path.c_str(), "wb");
				}
				if (dumpfp == nullptr)
					return;
			}
			u32 blockType = 0x0A0D0D0A; // Section Header Block
			fwrite(&blockType, sizeof(blockType), 1, dumpfp);
			u32 blockLen = 28;
			fwrite(&blockLen, sizeof(blockLen), 1, dumpfp);
			u32 magic = 0x1A2B3C4D;
			fwrite(&magic, sizeof(magic), 1, dumpfp);
			u32 version = 1; // 1.0
			fwrite(&version, sizeof(version), 1, dumpfp);
			u64 sectionLength = ~0; // unspecified
			fwrite(&sectionLength, sizeof(sectionLength), 1, dumpfp);
			fwrite(&blockLen, sizeof(blockLen), 1, dumpfp);

			blockType = 1; // Interface Description Block
			fwrite(&blockType, sizeof(blockType), 1, dumpfp);
			blockLen = 20;
			fwrite(&blockLen, sizeof(blockLen), 1, dumpfp);
			const u32 linkType = 1; // Ethernet
			fwrite(&linkType, sizeof(linkType), 1, dumpfp);
			const u32 snapLen = 0; // no limit
			fwrite(&snapLen, sizeof(snapLen), 1, dumpfp);
			// TODO options? if name, ip/mac address
			fwrite(&blockLen, sizeof(blockLen), 1, dumpfp);
		}
		const u32 blockType = 6; // Extended Packet Block
		fwrite(&blockType, sizeof(blockType), 1, dumpfp);
		u32 roundedSize = ((size + 3) & ~3) + 32;
		fwrite(&roundedSize, sizeof(roundedSize), 1, dumpfp);
		u32 ifId = 0;
		fwrite(&ifId, sizeof(ifId), 1, dumpfp);
		u64 now = getTimeMs() * 1000;
		fwrite((u32 *)&now + 1, 4, 1, dumpfp);
		fwrite(&now, 4, 1, dumpfp);
		fwrite(&size, sizeof(size), 1, dumpfp);
		fwrite(&size, sizeof(size), 1, dumpfp);
		fwrite(frame, 1, size, dumpfp);
		fwrite(frame, 1, roundedSize - size - 32, dumpfp);
		fwrite(&roundedSize, sizeof(roundedSize), 1, dumpfp);
#endif
	}

	asio::ip::tcp::socket socket;
	std::vector<u8> recvBuffer;
	std::array<u8, 1600> sendBuffer;
	u32 sendBufferIdx = 0;
	bool sending = false;

	FILE *dumpfp = nullptr;
};

class AccessPointFinder
{
public:
	AccessPointFinder(asio::io_context& io_context)
		: io_context(io_context), socket(io_context, asio::ip::udp::endpoint()),
		  timer(io_context)
	{
	}

	template<typename Handler>
	void find(const Handler& handler)
	{
		this->handler = std::function<void(const std::error_code&, const asio::ip::address&, const std::string&)>(handler);
		try {
			asio::ip::udp::resolver resolver(io_context);
			auto it = resolver.resolve("dcnet.flyca.st", std::to_string(PORT));
			if (it.empty()) {
				finish();
				return;
			}
			mainEndpoint = *it.begin();

			std::array<uint8_t, 5> buf;
			memcpy(&buf[0], &MAGIC, sizeof(MAGIC));
			buf[4] = DISCOVER;	// discover access points
			socket.send_to(asio::buffer(buf), mainEndpoint);

			timer.expires_after(asio::chrono::milliseconds(500));
			timer.async_wait([this](const std::error_code& ec)
			{
				if (ec)
					return;
				// Re-ping access points that didn't answer after 500 ms
				for (const auto& ap : accessPoints) {
					if (ap.count == 0)
						sendPing(ap.endpoint);
				}
				timer.expires_after(asio::chrono::milliseconds(500));
				timer.async_wait([this](const std::error_code& ec)
				{
					// 1 sec final timeout
					if (ec)
						return;
					std::error_code err;
					socket.close(err);
					finish();
				});
			});
			receiveAccessPoints();
		} catch (const std::system_error& e) {
			finish(e.code());
		}
	}

private:
	void receiveAccessPoints()
	{
		socket.async_receive_from(asio::buffer(recvbuf), recvEndpoint, [this](const std::error_code& ec, size_t len)
			{
				if (recvEndpoint != mainEndpoint || len < 5
						|| memcmp(&recvbuf[0], &MAGIC, sizeof(MAGIC)) || recvbuf[4] != DISCOVER) {
					// Unexpected or invalid packet
					receiveAccessPoints();
					return;
				}
				const uint8_t *p = &recvbuf[5];
				while (p - &recvbuf[0] < (int)len)
				{
					accessPoints.emplace_back();
					uint32_t addr;
					memcpy(&addr, p, sizeof(uint32_t));
					accessPoints.back().endpoint = asio::ip::udp::endpoint(asio::ip::address_v4(htonl(addr)), PORT);
					p += 4;
					size_t l = *p++;
					accessPoints.back().name = std::string((const char *)p, (const char *)(p + l));
					p += l;
				}
				if (accessPoints.size() > 1)
				{
					// Need to ping
					for (const auto& ap : accessPoints)
						sendPing(ap.endpoint);
				}
				else {
					finish();
				}
			});
	}

	void sendPing(const asio::ip::udp::endpoint& endpoint)
	{
		std::array<uint8_t, 13> buf;
		memcpy(&buf[0], &MAGIC, sizeof(MAGIC));
		buf[4] = PING;
		u64 now = (u64)getTimeMs();
		memcpy(&buf[5], &now, sizeof(u64));
		socket.send_to(asio::buffer(buf), endpoint);
		receivePing();
	}

	void receivePing()
	{
		if (receiving)
			return;
		receiving = true;
		socket.async_receive_from(asio::buffer(recvbuf), recvEndpoint, [this](const std::error_code& ec, size_t len)
			{
				receiving = false;
				if (ec)
				{
					if (ec != asio::error::operation_aborted && ec != asio::error::bad_descriptor)
						INFO_LOG(NETWORK, "receivePing error: %s", ec.message().c_str());
					return;
				}
				if (len != 13 || recvbuf[4] != PONG) {
					receivePing();
					return;
				}
				u64 ts;
				memcpy(&ts, &recvbuf[5], sizeof(ts));
				int ping = getTimeMs() - (time_t)ts;
				for (auto& ap : accessPoints)
				{
					if (ap.endpoint == recvEndpoint)
					{
						ap.ping += ping;
						ap.count++;
						if (ap.count < 3)
							sendPing(ap.endpoint);
						else
							// we have 3 answers from one AP so let's stop here
							finish();
						return;
					}
				}
				receivePing();
			});
	}

	void finish(const std::error_code& ec = {})
	{
		std::error_code e;
		socket.close(e);
		timer.cancel(e);
		if (ec) {
			handler(ec, {}, {});
		}
		else if (accessPoints.empty()) {
			handler({}, mainEndpoint.address(), {});
		}
		else
		{
			int bestPing = 1000000;
			const AccessPoint *bestAP = nullptr;
			for (const AccessPoint& ap : accessPoints)
			{
				if (ap.count == 0) {
					INFO_LOG(NETWORK, "AP %s (%s): no answer", ap.name.c_str(), ap.endpoint.address().to_string().c_str());
					continue;
				}
				const int ping = ap.ping / ap.count;
				INFO_LOG(NETWORK, "AP %s (%s): ping %d ms", ap.name.c_str(), ap.endpoint.address().to_string().c_str(), ping);
				if (ping < bestPing) {
					bestPing = ping;
					bestAP = &ap;
				}
			}
			if (bestAP == nullptr)
				bestAP = &accessPoints[0];
			handler({}, bestAP->endpoint.address(), bestAP->name);
		}
	}

	struct AccessPoint
	{
		asio::ip::udp::endpoint endpoint;
		std::string name;
		int ping = 0;
		int count = 0;
	};

	asio::io_context& io_context;
	asio::ip::udp::socket socket;
	std::array<uint8_t, 512> recvbuf;
	asio::ip::udp::endpoint recvEndpoint;
	asio::ip::udp::endpoint mainEndpoint;
	std::vector<AccessPoint> accessPoints;
	bool receiving = false;
	asio::steady_timer timer;
	std::function<void(const std::error_code&, const asio::ip::address&, const std::string&)> handler;

	static constexpr uint16_t PORT = 7655;
	static constexpr uint32_t MAGIC = 0xDC15C001;
	static constexpr uint8_t PING = 1;
	static constexpr uint8_t PONG = 2;
	static constexpr uint8_t DISCOVER = 3;
};

class DCNetThread
{
public:
	void start()
	{
		if (thread.joinable())
			return;
		io_context = std::make_unique<asio::io_context>();
		thread = std::thread(&DCNetThread::run, this);
	}

	void stop()
	{
		if (!thread.joinable())
			return;
		io_context->stop();
		thread.join();
		pppSocket.reset();
		ethSocket.reset();
		io_context.reset();
		os_notify("DCNet disconnected", 3000);
	}

	void sendModem(u8 v)
	{
		if (io_context == nullptr || pppSocket == nullptr)
			return;
		io_context->post([this, v]() {
			pppSocket->send(v);
		});
	}
	void sendEthFrame(const u8 *frame, u32 len)
	{
		if (io_context != nullptr && ethSocket != nullptr)
		{
			std::vector<u8> vbuf(frame, frame + len);
			io_context->post([this, vbuf]() {
				ethSocket->send(vbuf.data(), vbuf.size());
			});
		}
		else {
			// restart the thread if previously stopped
			start();
		}
	}

private:
	void run();
	void connect(const asio::ip::address& address = {}, const std::string& apname = {});

	std::thread thread;
	std::unique_ptr<asio::io_context> io_context;
	std::unique_ptr<PPPSocket> pppSocket;
	std::unique_ptr<EthSocket> ethSocket;

	static constexpr uint16_t PPP_PORT = 7654;
	static constexpr uint16_t TAP_PORT = 7655;
	static constexpr uint16_t POWER_SMASH_PPP_PORT = 7656;
	friend DCNetService;
};
static DCNetThread thread;

bool DCNetService::start()
{
	emu.setNetworkState(true);
	thread.start();
	return true;
}

void DCNetService::stop() {
	thread.stop();
	emu.setNetworkState(false);
}

void DCNetService::writeModem(u8 b) {
	thread.sendModem(b);
}

int DCNetService::readModem()
{
	if (toModem.empty())
		return -1;
	else
		return toModem.pop();
}

int DCNetService::modemAvailable() {
	return toModem.size();
}

void DCNetService::receiveEthFrame(u8 const *frame, unsigned int len)
{
	/*
    printf("Out frame: dest %02x:%02x:%02x:%02x:%02x:%02x "
           "src %02x:%02x:%02x:%02x:%02x:%02x, ethertype %04x, size %d bytes %s\n",
        frame[0], frame[1], frame[2], frame[3], frame[4], frame[5],
        frame[6], frame[7], frame[8], frame[9], frame[10], frame[11],
        *(u16 *)&frame[12], len, EthSocket::Instance == nullptr ? "LOST" : "");
    */
    // Stop DCNet on DHCP Release
	if (len >= 0x11d
			&& *(u16 *)&frame[0xc] == 0x0008		// type: IPv4
			&& frame[0x17] == 0x11					// UDP
			&& ntohs(*(u16 *)&frame[0x22]) == 68	// src port: dhcpc
			&& ntohs(*(u16 *)&frame[0x24]) == 67)	// dest port: dhcps
	{
		const u8 *options = &frame[0x11a];
		while (options - frame < len && *options != 0xff)
		{
			if (*options == 53		// message type
				&& options[2] == 7)	// release
			{
				stop();
				return;
			}
			options += options[1] + 2;
		}
	}
	thread.sendEthFrame(frame, len);
}

void DCNetThread::connect(const asio::ip::address& address, const std::string& apname)
{
	const bool powerSmash = settings.content.gameId == "HDR-0113"	// Power Smash
			|| settings.content.gameId == "HDR-0091";				// Pro Yakyuu Team de Asobou Net!
	asio::ip::tcp::endpoint endpoint;
	if (address.is_unspecified())
	{
		std::string hostname = "dcnet.flyca.st";
#ifndef LIBRETRO
		hostname = cfgLoadStr("network", "DCNetServer", hostname);
#endif
		std::string port;
		if (config::EmulateBBA)
			port = std::to_string(TAP_PORT);
		else if (powerSmash)
			port = std::to_string(POWER_SMASH_PPP_PORT);
		else
			port = std::to_string(PPP_PORT);
		asio::ip::tcp::resolver resolver(*io_context);
		asio::error_code ec;
		auto it = resolver.resolve(hostname, port, ec);
		if (ec)
			throw FlycastException(ec.message());
		if (it.empty())
			throw FlycastException("Host not found");
		endpoint = *it.begin();
	}
	else
	{
		endpoint.address(address);
		if (config::EmulateBBA)
			endpoint.port(TAP_PORT);
		else if (powerSmash)
			endpoint.port(POWER_SMASH_PPP_PORT);
		else
			endpoint.port(PPP_PORT);
	}
	if (config::EmulateBBA)
		ethSocket = std::make_unique<EthSocket>(*io_context, endpoint, apname);
	else if (powerSmash)
		pppSocket = std::make_unique<PowerSmashPPPSocket>(*io_context, endpoint, apname);
	else
		pppSocket = std::make_unique<PPPSocket>(*io_context, endpoint, apname);
}

void DCNetThread::run()
{
	toModem.clear();
	try {
		std::string hostname;
#ifndef LIBRETRO
		hostname = cfgLoadStr("network", "DCNetServer", "");
		if (!hostname.empty())
			connect();
#endif
		AccessPointFinder finder(*io_context);
		if (hostname.empty())
			finder.find([this](const std::error_code& ec,
					const asio::ip::address& address, const std::string& apname)
				{
					if (ec)
						WARN_LOG(NETWORK, "AP discovery failed: %s", ec.message().c_str());
					this->connect(address, apname);
				});

		io_context->run();
	} catch (const FlycastException& e) {
		ERROR_LOG(NETWORK, "DCNet connection error: %s", e.what());
		os_notify("Can't connect to DCNet", 8000, e.what());
	} catch (const std::runtime_error& e) {
		ERROR_LOG(NETWORK, "DCNetThread::run error: %s", e.what());
	}
}

}
