// Copyright 2009 Dolphin Emulator Project
// Licensed under GPLv2+
// Refer to the license.txt file included.

#include <memory>
#include <mutex>
#include <queue>
#include <thread>

#include "Common/CommonTypes.h"
#include "Common/Flag.h"
#include "Common/Thread.h"
#include "Common/Logging/Log.h"
#include "Core/CoreTiming.h"
#include "Core/HW/SI_Device.h"
#include "Core/HW/SI_DeviceGBA.h"
#include "Core/HW/SystemTimers.h"

#include "SFML/Network.hpp"

static std::thread connectionThread;
static std::queue<std::unique_ptr<sf::TcpSocket>> waiting_socks;
static std::queue<std::unique_ptr<sf::TcpSocket>> waiting_clocks;
static std::mutex cs_gba;
static std::mutex cs_gba_clk;
static u8 num_connected;

namespace { Common::Flag server_running; }

enum EJoybusCmds
{
	CMD_RESET  = 0xff,
	CMD_STATUS = 0x00,
	CMD_READ   = 0x14,
	CMD_WRITE  = 0x15
};

const u64 BITS_PER_SECOND = 115200;
const u64 BYTES_PER_SECOND = BITS_PER_SECOND / 8;

u8 GetNumConnected()
{
	int num_ports_connected = num_connected;
	if (num_ports_connected == 0)
		num_ports_connected = 1;

	return num_ports_connected;
}

// --- GameBoy Advance "Link Cable" ---

int GetTransferTime(u8 cmd)
{
	u64 bytes_transferred = 0;

	switch (cmd)
	{
	case CMD_RESET:
	case CMD_STATUS:
	{
		bytes_transferred = 4;
		break;
	}
	case CMD_READ:
	{
		bytes_transferred = 6;
		break;
	}
	case CMD_WRITE:
	{
		bytes_transferred = 1;
		break;
	}
	default:
	{
		bytes_transferred = 1;
		break;
	}
	}
	return (int)(bytes_transferred * SystemTimers::GetTicksPerSecond() / (GetNumConnected() * BYTES_PER_SECOND));
}

static void GBAConnectionWaiter()
{
	server_running.Set();

	Common::SetCurrentThreadName("GBA Connection Waiter");

	sf::TcpListener server;
	sf::TcpListener clock_server;

	// "dolphin gba"
	if (server.listen(0xd6ba) != sf::Socket::Done)
		return;

	// "clock"
	if (clock_server.listen(0xc10c) != sf::Socket::Done)
		return;

	server.setBlocking(false);
	clock_server.setBlocking(false);

	auto new_client = std::make_unique<sf::TcpSocket>();
	while (server_running.IsSet())
	{
		if (server.accept(*new_client) == sf::Socket::Done)
		{
			std::lock_guard<std::mutex> lk(cs_gba);
			waiting_socks.push(std::move(new_client));

			new_client = std::make_unique<sf::TcpSocket>();
		}
		if (clock_server.accept(*new_client) == sf::Socket::Done)
		{
			std::lock_guard<std::mutex> lk(cs_gba_clk);
			waiting_clocks.push(std::move(new_client));

			new_client = std::make_unique<sf::TcpSocket>();
		}

		Common::SleepCurrentThread(1);
	}
}

void GBAConnectionWaiter_Shutdown()
{
	server_running.Clear();
	if (connectionThread.joinable())
		connectionThread.join();
}

static bool GetAvailableSock(std::unique_ptr<sf::TcpSocket>& sock_to_fill)
{
	bool sock_filled = false;

	std::lock_guard<std::mutex> lk(cs_gba);

	if (!waiting_socks.empty())
	{
		sock_to_fill = std::move(waiting_socks.front());
		waiting_socks.pop();
		sock_filled = true;
	}

	return sock_filled;
}

static bool GetNextClock(std::unique_ptr<sf::TcpSocket>& sock_to_fill)
{
	bool sock_filled = false;

	std::lock_guard<std::mutex> lk(cs_gba_clk);

	if (!waiting_clocks.empty())
	{
		sock_to_fill = std::move(waiting_clocks.front());
		waiting_clocks.pop();
		sock_filled = true;
	}

	return sock_filled;
}

GBASockServer::GBASockServer(int _iDeviceNumber)
{
	if (!connectionThread.joinable())
		connectionThread = std::thread(GBAConnectionWaiter);

	cmd = 0;
	num_connected = 0;
	last_time_slice = 0;
	booted = false;
	device_number = _iDeviceNumber;
}

GBASockServer::~GBASockServer()
{
	Disconnect();
}

void GBASockServer::Disconnect()
{
	if (client)
	{
		num_connected--;
		client->disconnect();
		client = nullptr;
	}
	if (clock_sync)
	{
		clock_sync->disconnect();
		clock_sync = nullptr;
	}
	last_time_slice = 0;
	booted = false;
}

void GBASockServer::ClockSync()
{
	if (!clock_sync)
		if (!GetNextClock(clock_sync))
			return;

	u32 time_slice = 0;

	if (last_time_slice == 0)
	{
		num_connected++;
		last_time_slice = CoreTiming::GetTicks();
		time_slice = (u32)(SystemTimers::GetTicksPerSecond() / 60);
	}
	else
	{
		time_slice = (u32)(CoreTiming::GetTicks() - last_time_slice);
	}

	time_slice = (u32)((u64)time_slice * 16777216 / SystemTimers::GetTicksPerSecond());
	last_time_slice = CoreTiming::GetTicks();
	char bytes[4] = { 0, 0, 0, 0 };
	bytes[0] = (time_slice >> 24) & 0xff;
	bytes[1] = (time_slice >> 16) & 0xff;
	bytes[2] = (time_slice >> 8) & 0xff;
	bytes[3] = time_slice & 0xff;

	sf::Socket::Status status = clock_sync->send(bytes, 4);
	if (status == sf::Socket::Disconnected)
	{
		clock_sync->disconnect();
		clock_sync = nullptr;
	}
}

void GBASockServer::Send(u8* si_buffer)
{
	if (!client)
		if (!GetAvailableSock(client))
			return;

	for (int i = 0; i < 5; i++)
		send_data[i] = si_buffer[i ^ 3];

	cmd = (u8)send_data[0];

#ifdef _DEBUG
	NOTICE_LOG(SERIALINTERFACE, "%01d cmd %02x [> %02x%02x%02x%02x]",
		device_number,
		(u8)send_data[0], (u8)send_data[1], (u8)send_data[2],
		(u8)send_data[3], (u8)send_data[4]);
#endif

	client->setBlocking(false);
	sf::Socket::Status status;
	if (cmd == CMD_WRITE)
		status = client->send(send_data, sizeof(send_data));
	else
		status = client->send(send_data, 1);

	if (cmd != CMD_STATUS)
		booted = true;

	if (status == sf::Socket::Disconnected)
		Disconnect();

	time_cmd_sent = CoreTiming::GetTicks();
}

int GBASockServer::Receive(u8* si_buffer)
{
	if (!client)
		if (!GetAvailableSock(client))
			return 5;

	size_t num_received = 0;
	u64 transferTime = GetTransferTime((u8)send_data[0]);
	bool block = (CoreTiming::GetTicks() - time_cmd_sent) > transferTime;
	if (cmd == CMD_STATUS && !booted)
		block = false;

	if (block)
	{
		sf::SocketSelector Selector;
		Selector.add(*client);
		Selector.wait(sf::milliseconds(1000));
	}

	sf::Socket::Status recv_stat = client->receive(recv_data, sizeof(recv_data), num_received);
	if (recv_stat == sf::Socket::Disconnected)
	{
		Disconnect();
		return 5;
	}

	if (recv_stat == sf::Socket::NotReady)
		num_received = 0;

	if (num_received > sizeof(recv_data))
		num_received = sizeof(recv_data);

	if (num_received > 0)
	{
#ifdef _DEBUG
		if ((u8)send_data[0] == 0x00 || (u8)send_data[0] == 0xff)
		{
			WARN_LOG(SERIALINTERFACE, "%01d                              [< %02x%02x%02x%02x%02x] (%lu)",
				device_number,
				(u8)recv_data[0], (u8)recv_data[1], (u8)recv_data[2],
				(u8)recv_data[3], (u8)recv_data[4],
				num_received);
		}
		else
		{
			ERROR_LOG(SERIALINTERFACE, "%01d                              [< %02x%02x%02x%02x%02x] (%lu)",
				device_number,
				(u8)recv_data[0], (u8)recv_data[1], (u8)recv_data[2],
				(u8)recv_data[3], (u8)recv_data[4],
				num_received);
		}
#endif

		for (int i = 0; i < 5; i++)
			si_buffer[i ^ 3] = recv_data[i];
	}

	 return (int)num_received;
}

CSIDevice_GBA::CSIDevice_GBA(SIDevices _device, int _iDeviceNumber)
	: ISIDevice(_device, _iDeviceNumber)
	, GBASockServer(_iDeviceNumber)
{
	waiting_for_response = false;
}

CSIDevice_GBA::~CSIDevice_GBA()
{
	GBASockServer::Disconnect();
}

int CSIDevice_GBA::RunBuffer(u8* _pBuffer, int _iLength)
{
	if (!waiting_for_response)
	{
		for (int i = 0; i < 5; i++)
			send_data[i] = _pBuffer[i ^ 3];

		num_data_received = 0;
		ClockSync();
		Send(_pBuffer);
		timestamp_sent = CoreTiming::GetTicks();
		waiting_for_response = true;
	}

	if (waiting_for_response && num_data_received == 0)
	{
		num_data_received = Receive(_pBuffer);
	}

	if ((GetTransferTime(send_data[0])) > (int)(CoreTiming::GetTicks() - timestamp_sent))
	{
		return 0;
	}
	else
	{
		if (num_data_received != 0)
			waiting_for_response = false;
		return num_data_received;
	}
}

int CSIDevice_GBA::TransferInterval()
{
	return GetTransferTime(send_data[0]);
}
