/*
	Copyright 2021 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 "ggpo.h"
#include "hw/maple/maple_cfg.h"
#include "hw/maple/maple_devs.h"
#include "input/gamepad_device.h"
#include "input/keyboard_device.h"
#include "input/mouse.h"
#include "cfg/option.h"
#include "oslib/oslib.h"
#include "oslib/i18n.h"
#include <algorithm>

namespace ggpo
{

bool inRollback;

static void getLocalInput(MapleInputState inputState[4])
{
	if (!config::ThreadedRendering)
		os_UpdateInputState();
	std::lock_guard<std::mutex> lock(relPosMutex);
	for (int player = 0; player < 4; player++)
	{
		MapleInputState& state = inputState[player];
		state.kcode = kcode[player];
		state.halfAxes[PJTI_L] = lt[player];
		state.halfAxes[PJTI_R] = rt[player];
		state.halfAxes[PJTI_L2] = lt2[player];
		state.halfAxes[PJTI_R2] = rt2[player];
		state.fullAxes[PJAI_X1] = joyx[player];
		state.fullAxes[PJAI_Y1] = joyy[player];
		state.fullAxes[PJAI_X2] = joyrx[player];
		state.fullAxes[PJAI_Y2] = joyry[player];
		state.fullAxes[PJAI_X3] = joy3x[player];
		state.fullAxes[PJAI_Y3] = joy3y[player];
		state.mouseButtons = mo_buttons[player];
		state.absPos.x = mo_x_abs[player];
		state.absPos.y = mo_y_abs[player];
		state.keyboard.shift = kb_shift[player];
		memcpy(state.keyboard.key, kb_key[player], sizeof(kb_key[player]));
		int relX = std::round(mo_x_delta[player]);
		int relY = std::round(mo_y_delta[player]);
		int wheel = std::round(mo_wheel_delta[player]);
		state.relPos.x += relX;
		state.relPos.y += relY;
		state.relPos.wheel += wheel;
		mo_x_delta[player] -= relX;
		mo_y_delta[player] -= relY;
		mo_wheel_delta[player] -= wheel;
	}
}

}

#ifdef USE_GGPO
#include "ggponet.h"
#include "emulator.h"
#include "ui/gui.h"
#include "ui/gui_util.h"
#include "hw/mem/mem_watch.h"
#include <string.h>
#include <chrono>
#include <thread>
#include <mutex>
#include <unordered_map>
#include <numeric>
#include "imgui.h"
#include "miniupnp.h"
#include "hw/naomi/naomi_cart.h"

//#define SYNC_TEST 1

#ifdef SYNC_TEST
#include <xxhash.h>
#endif

namespace ggpo
{
using namespace std::chrono;

constexpr int MAX_PLAYERS = 2;
constexpr int SERVER_PORT = 19713;

constexpr u32 BTN_TRIGGER_LEFT	= DC_BTN_BITMAPPED_LAST << 1;
constexpr u32 BTN_TRIGGER_RIGHT	= DC_BTN_BITMAPPED_LAST << 2;

#pragma pack(push, 1)
struct VerificationData
{
	const int protocol = 2;
	u8 gameMD5[16] { };
	u8 stateMD5[16] { };
} ;
#pragma pack(pop)

static GGPOSession *ggpoSession;
static int localPlayerNum;
static GGPOPlayerHandle localPlayer;
static GGPOPlayerHandle remotePlayer;
static bool synchronized;
static std::recursive_mutex ggpoMutex;
static std::array<int, 5> msPerFrame;
static int msPerFrameIndex;
static time_point<steady_clock> lastFrameTime;
static int msPerFrameAvg;
static bool _endOfFrame;
static MiniUPnP miniupnp;
static int analogAxes;
static bool absPointerPos;
static bool keyboardGame;
static bool mouseGame;
static int inputSize;
static void (*chatCallback)(int playerNum, const std::string& msg);

struct MemPages
{
	void load()
	{
		memwatch::ramWatcher.getPages(ram);
		memwatch::vramWatcher.getPages(vram);
		memwatch::aramWatcher.getPages(aram);
		memwatch::elanWatcher.getPages(elanram);
	}
	memwatch::PageMap ram;
	memwatch::PageMap vram;
	memwatch::PageMap aram;
	memwatch::PageMap elanram;
};
static std::unordered_map<int, MemPages> deltaStates;
static int lastSavedFrame = -1;

static int timesyncOccurred;

#pragma pack(push, 1)
struct Inputs
{
	u32 kcode:20;
	u32 mouseButtons:4;
	u32 kbModifiers:8;

	union {
		struct {
			u8 x;
			u8 y;
		} analog;
		struct {
			s16 x;
			s16 y;
		} absPos;
		struct {
			s16 x;
			s16 y;
			s16 wheel;
		} relPos;
		u8 keys[6];
	} u;
};
static_assert(sizeof(Inputs) == 10, "wrong Inputs size");
static_assert(BTN_TRIGGER_RIGHT < (1 << 20));

struct GameEvent
{
	enum : char {
		Chat,
		VF4Card
	} type;
	union {
		struct {
			u8 playerNum;
			char message[512 - sizeof(playerNum) - sizeof(type)];
		} chat;
		struct {
			u8 playerNum;
			u8 data[128];
		} card;
	} u;

	constexpr static int chatMessageLen(int len) { return len - sizeof(u.chat.playerNum) - sizeof(type); }
};
#pragma pack(pop)

/*
 * begin_game callback - This callback has been deprecated.  You must
 * implement it, but should ignore the 'game' parameter.
 */
static bool begin_game(const char *)
{
	DEBUG_LOG(NETWORK, "Game begin");
	rend_allow_rollback();
	return true;
}

/*
 * on_event - Notification that something has happened.  See the GGPOEventCode
 * structure for more information.
 */
static bool on_event(GGPOEvent *info)
{
	using namespace i18n;
	switch (info->code) {
	case GGPO_EVENTCODE_CONNECTED_TO_PEER:
		INFO_LOG(NETWORK, "Connected to peer %d", info->u.connected.player);
		os_notify(T("Connected to peer"), 2000);
		break;
	case GGPO_EVENTCODE_SYNCHRONIZING_WITH_PEER:
		INFO_LOG(NETWORK, "Synchronizing with peer %d", info->u.synchronizing.player);
		os_notify(T("Synchronizing with peer"), 2000);
		break;
	case GGPO_EVENTCODE_SYNCHRONIZED_WITH_PEER:
		INFO_LOG(NETWORK, "Synchronized with peer %d", info->u.synchronized.player);
		os_notify(T("Synchronized with peer"), 2000);
		break;
	case GGPO_EVENTCODE_RUNNING:
		INFO_LOG(NETWORK, "Running");
		os_notify(T("Running"), 2000);
		synchronized = true;
		break;
	case GGPO_EVENTCODE_DISCONNECTED_FROM_PEER:
		INFO_LOG(NETWORK, "Disconnected from peer %d", info->u.disconnected.player);
		throw FlycastException(Ts("Disconnected from peer"));
		break;
	case GGPO_EVENTCODE_TIMESYNC:
		INFO_LOG(NETWORK, "Timesync: %d frames ahead", info->u.timesync.frames_ahead);
		timesyncOccurred += 5;
		std::this_thread::sleep_for(std::chrono::milliseconds(1000 / (msPerFrameAvg >= 25 ? 30 : 60)));
		break;
	case GGPO_EVENTCODE_CONNECTION_INTERRUPTED:
		INFO_LOG(NETWORK, "Connection interrupted with player %d", info->u.connection_interrupted.player);
		os_notify(T("Connection interrupted"), 2000);
		break;
	case GGPO_EVENTCODE_CONNECTION_RESUMED:
		INFO_LOG(NETWORK, "Connection resumed with player %d", info->u.connection_resumed.player);
		os_notify(T("Connection resumed"), 2000);
		break;
	}
	return true;
}

/*
 * advance_frame - Called during a rollback.  You should advance your game
 * state by exactly one frame.  Before each frame, call ggpo_synchronize_input
 * to retrieve the inputs you should use for that frame.  After each frame,
 * you should call ggpo_advance_frame to notify GGPO.net that you're
 * finished.
 *
 * The flags parameter is reserved.  It can safely be ignored at this time.
 */
static bool advance_frame(int)
{
	INFO_LOG(NETWORK, "advance_frame");
	settings.aica.muteAudio = true;
	rend_enable_renderer(false);
	inRollback = true;

	emu.run();
	ggpo_advance_frame(ggpoSession);

	settings.aica.muteAudio = false;
	rend_enable_renderer(true);
	inRollback = false;
	_endOfFrame = false;

	return true;
}

/*
 * load_game_state - GGPO.net will call this function at the beginning
 * of a rollback.  The buffer and len parameters contain a previously
 * saved state returned from the save_game_state function.  The client
 * should make the current game state match the state contained in the
 * buffer.
 */
static bool load_game_state(unsigned char *buffer, int len)
{
	INFO_LOG(NETWORK, "load_game_state");

	rend_start_rollback();
	// FIXME dynarecs
	Deserializer deser(buffer, len, true);
	int frame;
	deser >> frame;
	memwatch::unprotect();
	for (int f = lastSavedFrame - 1; f >= frame; f--)
	{
		const MemPages& pages = deltaStates[f];
		for (const auto& pair : pages.ram)
			memcpy(memwatch::ramWatcher.getMemPage(pair.first), &pair.second.data[0], PAGE_SIZE);
		for (const auto& pair : pages.vram)
			memcpy(memwatch::vramWatcher.getMemPage(pair.first), &pair.second.data[0], PAGE_SIZE);
		for (const auto& pair : pages.aram)
			memcpy(memwatch::aramWatcher.getMemPage(pair.first), &pair.second.data[0], PAGE_SIZE);
		for (const auto& pair : pages.elanram)
			memcpy(memwatch::elanWatcher.getMemPage(pair.first), &pair.second.data[0], PAGE_SIZE);
		DEBUG_LOG(NETWORK, "Restored frame %d pages: %d ram, %d vram, %d eram, %d aica ram", f, (u32)pages.ram.size(),
					(u32)pages.vram.size(), (u32)pages.elanram.size(), (u32)pages.aram.size());
	}
	dc_deserialize(deser);
	if (deser.size() != (u32)len)
	{
		ERROR_LOG(NETWORK, "load_game_state len %d used %d", len, (int)deser.size());
		die("fatal");
	}
	rend_allow_rollback();	// ggpo might load another state right after this one
	memwatch::reset();
	memwatch::protect();
	return true;
}

/*
 * save_game_state - The client should allocate a buffer, copy the
 * entire contents of the current game state into it, and copy the
 * length into the *len parameter.  Optionally, the client can compute
 * a checksum of the data and store it in the *checksum argument.
 */
static bool save_game_state(unsigned char **buffer, int *len, int *checksum, int frame)
{
	verify(!emu.getSh4Executor()->IsCpuRunning());
	lastSavedFrame = frame;
	// TODO this is way too much memory
	size_t allocSize = settings.platform.isNaomi() ? 20_MB : 10_MB;
	*buffer = (unsigned char *)malloc(allocSize);
	if (*buffer == nullptr)
	{
		WARN_LOG(NETWORK, "Memory alloc failed");
		*len = 0;
		return false;
	}
	Serializer ser(*buffer, allocSize, true);
	try {
		ser << frame;
		dc_serialize(ser);
		*len = ser.size();
	} catch (const Serializer::Exception& e) {
		WARN_LOG(NETWORK, "Save state failed: %s", e.what());
		*len = 0;
		return false;
	}
#ifdef SYNC_TEST
	*checksum = XXH3_64bits(*buffer, usedSize);
#endif
	memwatch::protect();
	if (frame > 0)
	{
#ifdef SYNC_TEST
		if (deltaStates.count(frame - 1) != 0)
		{
			MemPages memPages;
			memPages.load();
			const MemPages& savedPages = deltaStates[frame - 1];
			//verify(memPages.ram.size() == savedPages.ram.size());
			if (memPages.ram.size() != savedPages.ram.size())
			{
				ERROR_LOG(NETWORK, "old ram size %d new %d", (u32)savedPages.ram.size(), (u32)memPages.ram.size());
				if (memPages.ram.size() > savedPages.ram.size())
					for (const auto& pair : memPages.ram)
					{
						if (savedPages.ram.count(pair.first) == 0)
							ERROR_LOG(NETWORK, "new page @ %x", pair.first);
						else
							DEBUG_LOG(NETWORK, "page ok @ %x", pair.first);
					}
				die("fatal");
			}
			for (const auto& pair : memPages.ram)
			{
				verify(savedPages.ram.count(pair.first) == 1);
				verify(memcmp(&pair.second[0], &savedPages.ram.find(pair.first)->second[0], PAGE_SIZE) == 0);
			}
			verify(memPages.vram.size() == savedPages.vram.size());
			for (const auto& pair : memPages.vram)
			{
				verify(savedPages.vram.count(pair.first) == 1);
				verify(memcmp(&pair.second[0], &savedPages.vram.find(pair.first)->second[0], PAGE_SIZE) == 0);
			}
			//verify(memPages.aram.size() == savedPages.aram.size());
			if (memPages.aram.size() != savedPages.aram.size())
			{
				ERROR_LOG(NETWORK, "old aram size %d new %d", (u32)savedPages.aram.size(), (u32)memPages.aram.size());
				die("fatal");
			}
			for (const auto& pair : memPages.aram)
			{
				verify(savedPages.aram.count(pair.first) == 1);
				verify(memcmp(&pair.second[0], &savedPages.aram.find(pair.first)->second[0], PAGE_SIZE) == 0);
			}
		}
#endif
		// Save the delta to frame-1
		deltaStates[frame - 1].load();
		DEBUG_LOG(NETWORK, "Saved frame %d pages: %d ram, %d vram, %d eram, %d aica ram", frame - 1, (u32)deltaStates[frame - 1].ram.size(),
				(u32)deltaStates[frame - 1].vram.size(), (u32)deltaStates[frame - 1].elanram.size(), (u32)deltaStates[frame - 1].aram.size());
	}

	return true;
}

/*
 * log_game_state - Used in diagnostic testing.  The client should use
 * the ggpo_log function to write the contents of the specified save
 * state in a human readible form.
 */
static bool log_game_state(char *filename, unsigned char *buffer, int len)
{
#ifdef SYNC_TEST
	static int lastLoggedFrame = -1;
	static u8 *lastState;
	int frame = *(u32 *)buffer;
	DEBUG_LOG(NETWORK, "log_game_state frame %d len %d", frame, len);
	if (lastLoggedFrame == frame) {
		for (int i = 0; i < len; i++)
			if (buffer[i] != lastState[i])
			{
				WARN_LOG(NETWORK, "States for frame %d differ at offset %d: now %x prev %x", frame, i, *(u32 *)&buffer[i & ~3], *(u32 *)&lastState[i & ~3]);
				break;
			}
	}
	lastState = buffer;
	lastLoggedFrame = frame;
#endif

	return true;
}

/*
 * free_buffer - Frees a game state allocated in save_game_state.  You
 * should deallocate the memory contained in the buffer.
 */
static void free_buffer(void *buffer)
{
	if (buffer != nullptr)
	{
		Deserializer deser(buffer, 1_MB, true);
		int frame;
		deser >> frame;
		deltaStates.erase(frame);
		free(buffer);
	}
}

static void on_message(u8 *msg, int len)
{
	if (len == 0)
		return;
	GameEvent *event = (GameEvent *)msg;
	switch (event->type)
	{
	case GameEvent::Chat:
		if (chatCallback != nullptr && GameEvent::chatMessageLen(len) > 0)
			chatCallback(event->u.chat.playerNum, std::string(event->u.chat.message, GameEvent::chatMessageLen(len)));
		break;

	case GameEvent::VF4Card:
		setRfidCardData(event->u.card.playerNum, event->u.card.data);
		break;

	default:
		WARN_LOG(NETWORK, "Unknown app message type %d", event->type);
		break;
	}
}

void startSession(int localPort, int localPlayerNum)
{
	GGPOSessionCallbacks cb{};
	cb.begin_game      = begin_game;
	cb.advance_frame   = advance_frame;
	cb.load_game_state = load_game_state;
	cb.save_game_state = save_game_state;
	cb.free_buffer     = free_buffer;
	cb.on_event        = on_event;
	cb.log_game_state  = log_game_state;
	cb.on_message      = on_message;

#ifdef SYNC_TEST
	GGPOErrorCode result = ggpo_start_synctest(&ggpoSession, &cb, settings.content.gameId.c_str(), MAX_PLAYERS, sizeof(kcode[0]), 1);
	if (result != GGPO_OK)
	{
		WARN_LOG(NETWORK, "GGPO start sync session failed: %d", result);
		ggpoSession = nullptr;
		throw FlycastException("GGPO start sync session failed");
	}
	ggpo_idle(ggpoSession, 0);
	ggpo::localPlayerNum = localPlayerNum;
	GGPOPlayer player{ sizeof(GGPOPlayer), GGPO_PLAYERTYPE_LOCAL, localPlayerNum + 1 };
	result = ggpo_add_player(ggpoSession, &player, &localPlayer);
	player.player_num = (1 - localPlayerNum) + 1;
	result = ggpo_add_player(ggpoSession, &player, &remotePlayer);
	synchronized = true;
	analogAxes = 0;
	NOTICE_LOG(NETWORK, "GGPO synctest session started");
#else
	if (settings.platform.isConsole())
		analogAxes = config::GGPOAnalogAxes;
	else
	{
		analogAxes = 0;
		absPointerPos = false;
		keyboardGame = false;
		mouseGame = false;
		if (settings.input.lightgunGame)
			absPointerPos = true;
		else if (settings.input.keyboardGame)
			keyboardGame = true;
		else if (settings.input.mouseGame)
			mouseGame = true;
		else if (NaomiGameInputs != nullptr)
		{
			for (const auto& axis : NaomiGameInputs->axes)
			{
				if (axis.name == nullptr)
					break;
				if (axis.type == Full)
					analogAxes = std::max(analogAxes, (int)axis.axis + 1);
			}
		}
		NOTICE_LOG(NETWORK, "GGPO: Using %d full analog axes", analogAxes);
	}
	inputSize = sizeof(kcode[0]) + analogAxes + (int)absPointerPos * sizeof(Inputs::u.absPos)
		+ (int)keyboardGame * sizeof(Inputs::u.keys) + (int)mouseGame * sizeof(Inputs::u.relPos);

	VerificationData verif;
	MD5Sum().add(settings.network.md5.bios)
			.add(settings.network.md5.game)
			.getDigest(verif.gameMD5);
	auto& digest = settings.network.md5.savestate;
	if (std::find_if(std::begin(digest), std::end(digest), [](u8 b) { return b != 0; }) != std::end(digest))
		memcpy(verif.stateMD5, digest, sizeof(digest));
	else
	{
		MD5Sum().add(settings.network.md5.nvmem)
				.add(settings.network.md5.nvmem2)
				.add(settings.network.md5.eeprom)
				.add(settings.network.md5.vmu)
				.getDigest(verif.stateMD5);
	}

	GGPOErrorCode result = ggpo_start_session(&ggpoSession, &cb, settings.content.gameId.c_str(), MAX_PLAYERS, inputSize, localPort,
			&verif, sizeof(verif));
	if (result != GGPO_OK)
	{
		WARN_LOG(NETWORK, "GGPO start session failed: %d", result);
		ggpoSession = nullptr;
		throw FlycastException("GGPO network initialization failed");
	}

	// automatically disconnect clients after 3000 ms and start our count-down timer
	// for disconnects after 1000 ms.   To completely disable disconnects, simply use
	// a value of 0 for ggpo_set_disconnect_timeout.
	ggpo_set_disconnect_timeout(ggpoSession, 3000);
	ggpo_set_disconnect_notify_start(ggpoSession, 1000);

	ggpo::localPlayerNum = localPlayerNum;
	GGPOPlayer player{ sizeof(GGPOPlayer), GGPO_PLAYERTYPE_LOCAL, localPlayerNum + 1 };
	result = ggpo_add_player(ggpoSession, &player, &localPlayer);
	if (result != GGPO_OK)
	{
		WARN_LOG(NETWORK, "GGPO cannot add local player: %d", result);
		stopSession();
		throw FlycastException("GGPO cannot add local player");
	}
	ggpo_set_frame_delay(ggpoSession, localPlayer, config::GGPODelay.get());

	size_t colon = config::NetworkServer.get().find(':');
	std::string peerIp = config::NetworkServer.get().substr(0, colon);
	if (peerIp.empty())
		peerIp = "127.0.0.1";
	u32 peerPort;
	if (colon == std::string::npos)
	{
		if (peerIp == "127.0.0.1")
			peerPort = localPort ^ 1;
		else
			peerPort = SERVER_PORT;
	}
	else
	{
		peerPort = atoi(config::NetworkServer.get().substr(colon + 1).c_str());
	}
	player.type = GGPO_PLAYERTYPE_REMOTE;
	strcpy(player.u.remote.ip_address, peerIp.c_str());
	player.u.remote.port = peerPort;
	player.player_num = (1 - localPlayerNum) + 1;
	result = ggpo_add_player(ggpoSession, &player, &remotePlayer);
	if (result != GGPO_OK)
	{
		WARN_LOG(NETWORK, "GGPO cannot add remote player: %d", result);
		stopSession();
		throw FlycastException("GGPO cannot add remote player");
	}
	DEBUG_LOG(NETWORK, "GGPO session started");
#endif
}

void stopSession()
{
	std::lock_guard<std::recursive_mutex> lock(ggpoMutex);
	if (ggpoSession == nullptr)
		return;
	ggpo_close_session(ggpoSession);
	ggpoSession = nullptr;
	miniupnp.Term();
	emu.setNetworkState(false);
	memwatch::unprotect();
	memwatch::reset();
}

void getInput(MapleInputState inputState[4])
{
	std::lock_guard<std::recursive_mutex> lock(ggpoMutex);
	if (ggpoSession == nullptr)
	{
		getLocalInput(inputState);
		return;
	}
	for (int player = 0; player < 4; player++)
		inputState[player] = {};

	std::vector<u8> inputData(inputSize * MAX_PLAYERS);
	// should not call any callback
	GGPOErrorCode error = ggpo_synchronize_input(ggpoSession, (void *)&inputData[0], inputData.size(), nullptr);
	if (error != GGPO_OK)
	{
		stopSession();
		throw FlycastException("GGPO error");
	}

	for (int player = 0; player < MAX_PLAYERS; player++)
	{
		MapleInputState& state = inputState[player];
		const Inputs *inputs = (Inputs *)&inputData[player * inputSize];
		state.kcode = ~inputs->kcode;
		if (analogAxes > 0)
		{
			state.fullAxes[PJAI_X1] = inputs->u.analog.x << 8;
			if (analogAxes >= 2)
				state.fullAxes[PJAI_Y1] = inputs->u.analog.y << 8;
		}
		else if (absPointerPos)
		{
			state.absPos.x = inputs->u.absPos.x;
			state.absPos.y = inputs->u.absPos.y;
		}
		else if (keyboardGame)
		{
			memcpy(state.keyboard.key, inputs->u.keys, sizeof(state.keyboard.key));
			state.keyboard.shift = inputs->kbModifiers;
		}
		else if (mouseGame)
		{
			state.relPos.x = inputs->u.relPos.x;
			state.relPos.y = inputs->u.relPos.y;
			state.relPos.wheel = inputs->u.relPos.wheel;
			state.mouseButtons = ~inputs->mouseButtons;
		}
		state.halfAxes[PJTI_R] = (state.kcode & BTN_TRIGGER_RIGHT) == 0 ? 0xffff : 0;
		state.halfAxes[PJTI_L] = (state.kcode & BTN_TRIGGER_LEFT) == 0 ? 0xffff : 0;
	}
}

bool nextFrame()
{
	if (!_endOfFrame)
		return false;
	_endOfFrame = false;
	if (inRollback)
		return true;
	auto now = std::chrono::steady_clock::now();
	if (lastFrameTime != time_point<steady_clock>())
	{
		msPerFrame[msPerFrameIndex++] = duration_cast<milliseconds>(now - lastFrameTime).count();
		if (msPerFrameIndex >= (int)msPerFrame.size())
			msPerFrameIndex = 0;
		msPerFrameAvg = std::accumulate(msPerFrame.begin(), msPerFrame.end(), 0) / msPerFrame.size();
	}
	lastFrameTime = now;

	std::lock_guard<std::recursive_mutex> lock(ggpoMutex);
	if (ggpoSession == nullptr)
		return false;
	// will call save_game_state
	GGPOErrorCode error = ggpo_advance_frame(ggpoSession);

	// may rollback
	if (error == GGPO_OK)
		error = ggpo_idle(ggpoSession, 0);
	if (error != GGPO_OK)
	{
		stopSession();
		if (error == GGPO_ERRORCODE_INPUT_SIZE_DIFF)
			throw FlycastException("GGPO analog settings are different from peer");
		else
			throw FlycastException("GGPO error");
	}

	// may call save_game_state
	do {
		if (!config::ThreadedRendering)
			os_UpdateInputState();
		Inputs inputs;
		inputs.kcode = ~kcode[0];
		if (rt[0] >= 0x4000)
			inputs.kcode |= BTN_TRIGGER_RIGHT;
		else
			inputs.kcode &= ~BTN_TRIGGER_RIGHT;
		if (lt[0] >= 0x4000)
			inputs.kcode |= BTN_TRIGGER_LEFT;
		else
			inputs.kcode &= ~BTN_TRIGGER_LEFT;
		inputs.mouseButtons = 0;
		inputs.kbModifiers = 0;
		if (analogAxes > 0)
		{
			inputs.u.analog.x = joyx[0] >> 8;
			if (analogAxes >= 2)
				inputs.u.analog.y = joyy[0] >> 8;
		}
		else if (absPointerPos)
		{
			inputs.u.absPos.x = mo_x_abs[0];
			inputs.u.absPos.y = mo_y_abs[0];
		}
		else if (keyboardGame)
		{
			inputs.kbModifiers = kb_shift[0];
			memcpy(inputs.u.keys, kb_key[0], sizeof(kb_key[0]));
		}
		else if (mouseGame)
		{
			std::lock_guard<std::mutex> lock(relPosMutex);
			inputs.mouseButtons = ~mo_buttons[0];
			inputs.u.relPos.x = std::round(mo_x_delta[0]);
			inputs.u.relPos.y = std::round(mo_y_delta[0]);
			inputs.u.relPos.wheel = std::round(mo_wheel_delta[0]);
			mo_x_delta[0] -= inputs.u.relPos.x;
			mo_y_delta[0] -= inputs.u.relPos.y;
			mo_wheel_delta[0] -= inputs.u.relPos.wheel;
		}
		error = ggpo_add_local_input(ggpoSession, localPlayer, &inputs, inputSize);
		if (error == GGPO_OK)
			break;
		if (error != GGPO_ERRORCODE_PREDICTION_THRESHOLD)
		{
			WARN_LOG(NETWORK, "ggpo_add_local_input failed %d", error);
			stopSession();
			throw FlycastException("GGPO error");
		}
		DEBUG_LOG(NETWORK, "ggpo_add_local_input prediction barrier reached");
		std::this_thread::sleep_for(std::chrono::milliseconds(5));
		error = ggpo_idle(ggpoSession, 0);
		if (error != GGPO_OK)
		{
			stopSession();
			throw FlycastException("GGPO error");
		}
	} while (active());
#ifdef SYNC_TEST
	u32 input = ~kcode[1 - localPlayerNum];
	GGPOErrorCode result = ggpo_add_local_input(ggpoSession, remotePlayer, &input, sizeof(input));
	if (result != GGPO_OK)
		WARN_LOG(NETWORK, "ggpo_add_local_input(2) failed %d", result);
#endif
	if (active()) {
		emu.getSh4Executor()->Start();
		return true;
	}
	return false;
}

bool active()
{
	return ggpoSession != nullptr;
}

std::future<bool> startNetwork()
{
	synchronized = false;
	return std::async(std::launch::async, []{
		{
			ThreadName _("GGPO-start");
			std::lock_guard<std::recursive_mutex> lock(ggpoMutex);
#ifdef SYNC_TEST
			startSession(0, 0);
#else
			if (config::EnableUPnP)
			{
				miniupnp.Init();
				miniupnp.AddPortMapping(SERVER_PORT, false);
			}

			try {
				if (config::ActAsServer)
					startSession(SERVER_PORT, 0);
				else
					// Use SERVER_PORT-1 as local port if connecting to ourselves
					startSession(config::NetworkServer.get().empty() || config::NetworkServer.get() == "127.0.0.1" ? SERVER_PORT - 1 : SERVER_PORT, 1);
			} catch (...) {
				miniupnp.Term();
				throw;
			}
#endif
		}
		while (!synchronized && active())
		{
			{
				std::lock_guard<std::recursive_mutex> lock(ggpoMutex);
				if (ggpoSession == nullptr)
					break;
				GGPOErrorCode result = ggpo_idle(ggpoSession, 0);
				if (result == GGPO_ERRORCODE_VERIFICATION_ERROR)
					throw FlycastException("Peer verification failed");
				else if (result != GGPO_OK)
				{
					WARN_LOG(NETWORK, "ggpo_idle failed %d", result);
					throw FlycastException("GGPO error");
				}
			}
			std::this_thread::sleep_for(std::chrono::milliseconds(20));
		}
#ifdef SYNC_TEST
		// save initial state (frame 0)
		if (active())
		{
			MapleInputState state[4];
			getInput(state);
		}
#endif
		if (active() && (settings.content.gameId == "VIRTUA FIGHTER 4 JAPAN"
				|| settings.content.gameId == "VF4 EVOLUTION JAPAN"
				|| settings.content.gameId == "VF4 FINAL TUNED JAPAN"))
		{
			// Send the local P1 card
			const u8 *cardData = getRfidCardData(0);
			if (cardData != nullptr)
			{
				GameEvent event;
				event.type = GameEvent::VF4Card;
				event.u.card.playerNum = config::ActAsServer ? 0 : 1;
				memcpy(event.u.card.data, cardData, sizeof(event.u.card.data));
				ggpo_send_message(ggpoSession, &event, sizeof(event.type) + sizeof(event.u.card), true);
			}
		}
		emu.setNetworkState(active());
		return active();
	});
}

void displayStats()
{
	if (!active())
		return;
	GGPONetworkStats stats;
	ggpo_get_network_stats(ggpoSession, remotePlayer, &stats);

	ImguiStyleVar _(ImGuiStyleVar_WindowRounding, 0);
	ImguiStyleVar _1(ImGuiStyleVar_WindowBorderSize, 0);
	ImGui::SetNextWindowPos(ImVec2(10, 10));
	ImGui::SetNextWindowSize(ScaledVec2(95, 0));
	ImGui::SetNextWindowBgAlpha(0.7f);
	ImGui::Begin("##ggpostats", NULL, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs);
	ImguiStyleColor _2(ImGuiCol_PlotHistogram, ImVec4(0.557f, 0.268f, 0.965f, 1.f));

	// Send Queue
	ImGui::Text("Send Q");
	ImGui::ProgressBar(stats.network.send_queue_len / 10.f, ImVec2(-1, uiScaled(10.f)), "");

	// Frame Delay
	ImGui::Text("Delay");
	std::string delay = std::to_string(config::GGPODelay.get());
	ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(delay.c_str()).x);
	ImGui::Text("%s", delay.c_str());

	// Ping
	ImGui::Text("Ping");
	std::string ping = std::to_string(stats.network.ping);
	ImGui::SameLine(ImGui::GetContentRegionAvail().x - ImGui::CalcTextSize(ping.c_str()).x);
	ImGui::Text("%s", ping.c_str());

	// Predicted Frames
	if (stats.sync.predicted_frames >= 7)
		// red
	    ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(1, 0, 0, 1));
	else if (stats.sync.predicted_frames >= 5)
		// yellow
	    ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(.9f, .9f, .1f, 1));
	ImGui::Text("Predicted");
	ImGui::ProgressBar(stats.sync.predicted_frames / 7.f, ImVec2(-1, uiScaled(10.f)), "");
	if (stats.sync.predicted_frames >= 5)
		ImGui::PopStyleColor();

	// Frames behind
	int timesync = timesyncOccurred;
	if (timesync > 0)
		ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 0, 0, 1));
	ImGui::Text("Behind");
	ImGui::ProgressBar(0.5f + stats.timesync.local_frames_behind / 16.f, ImVec2(-1, uiScaled(10.f)), "");
	if (timesync > 0)
	{
		ImGui::PopStyleColor();
		timesyncOccurred--;
	}

	ImGui::End();
}

void endOfFrame()
{
	if (active())
	{
		_endOfFrame = true;
		emu.getSh4Executor()->Stop();
	}
}

void sendChatMessage(int playerNum, const std::string& msg) {
	if (!active())
		return;
	GameEvent event;
	event.type = GameEvent::Chat;
	event.u.chat.playerNum = playerNum;
	size_t msgLen = std::min(msg.length(), sizeof(event.u.chat.message));
	memcpy(event.u.chat.message, msg.c_str(), msgLen);
	ggpo_send_message(ggpoSession, &event, sizeof(GameEvent) - sizeof(event.u.chat.message) + msgLen, true);
}

void receiveChatMessages(void (*callback)(int playerNum, const std::string& msg))
{
	chatCallback = callback;
}

}

#else // LIBRETRO

namespace ggpo
{

void stopSession() {
}

void getInput(MapleInputState inputState[4])
{
	getLocalInput(inputState);
}

bool nextFrame() {
	return true;
}

bool active() {
	return false;
}

std::future<bool> startNetwork() {
	return std::async(std::launch::deferred, []{ return false; });
}

void displayStats() {
}

void endOfFrame() {
}

void sendChatMessage(int playerNum, const std::string& msg) {
}

void receiveChatMessages(void (*callback)(int playerNum, const std::string& msg)) {
}

}
#endif
