// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id: 41e530ef45d070bb0c6598af3af75fe63d051a8b $
//
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 2006-2025 by The Odamex Team.
//
// This program 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.
//
// This program 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.
//
// DESCRIPTION:
//     Defines needed by the WDL stats logger.
//
//-----------------------------------------------------------------------------

#include <ctime>

#include "odamex.h"

#include "g_levelstate.h"
#include "m_wdlstats.h"

#include "c_dispatch.h"
#include "p_local.h"

#define WDLSTATS_VERSION 6

extern Players players;

EXTERN_CVAR(sv_gametype)
EXTERN_CVAR(sv_hostname)
EXTERN_CVAR(sv_teamspawns)
EXTERN_CVAR(sv_playerbeacons)
EXTERN_CVAR(g_sides)
EXTERN_CVAR(g_lives)

//// Strings for WDL events
//static const char* wdlevstrings[] = {
//    "DAMAGE",         "CARRIERDAMAGE",     "KILL",
//    "CARRIERKILL",    "ENVIRODAMAGE",      "ENVIROCARRIERDAMAGE",
//    "ENVIROKILL",     "ENVIROCARRIERKILL", "TOUCH",
//    "PICKUPTOUCH",    "CAPTURE",           "PICKUPCAPTURE",
//    "ASSIST",         "RETURNFLAG",        "PICKUPITEM",
//    "SPREADACCURACY", "SSACCURACY",        "TRACERACCURACY",
//    "PROJACCURACY",   "SPAWNPLAYER",       "SPAWNITEM",
//    "JOINGAME",       "DISCONNECT",        "PLAYERBEACON",
//    "CARRIERBEACON",  "PROJFIRE",
//    //"RJUMPGO",
//    //"RJUMPLAND",
//    //"RJUMPAPEX",
//    //"MOBBEACON",
//    //"SPAWNMOB",
//};

std::string M_GetCurrentWadHashes();

static struct WDLState
{
	// Directory to log stats to.
	std::string logdir;

	// True if we're recording stats for this game.
	bool recording;

	// The starting gametic of the most recent log.
	int begintic;

	// [Blair] Toggle for whether that recording has playerbeacons enabled.
	bool enablebeacons;
} wdlstate;

// A single tracked player
struct WDLPlayer
{
	int id;
	int pid;
	std::string netname;
	team_t team;
};

// WDL Players that we're keeping track of.
typedef std::vector<WDLPlayer> WDLPlayers;
static WDLPlayers wdlplayers;

// A single tracked spawn
struct WDLPlayerSpawn
{
	int id;
	int x;
	int y;
	int z;
	team_t team;
};

[[nodiscard]]
bool operator==(const WDLPlayerSpawn& lhs, const WDLPlayerSpawn& rhs)
{
	return lhs.id == rhs.id && lhs.team == rhs.team && lhs.x == rhs.x && lhs.y == rhs.y &&
	       lhs.z == rhs.z;
}

// WDL player spawns that we're keeping track of.
typedef std::vector<WDLPlayerSpawn> WDLPlayerSpawns;
static WDLPlayerSpawns wdlplayerspawns;

// A single tracked item spawn
struct WDLItemSpawn
{
	int id;
	int x;
	int y;
	int z;
	WDLPowerups item;
};

// WDL item spawns that we're keeping track of.
typedef std::vector<WDLItemSpawn> WDLItemSpawns;
static WDLItemSpawns wdlitemspawns;

// A single tracked flag socket
struct WDLFlagLocation
{
	team_t team;
	int x;
	int y;
	int z;
};

// Flags that we're keeping track of.
typedef std::vector<WDLFlagLocation> WDLFlagLocations;
static WDLFlagLocations wdlflaglocations;

// A single event.
struct WDLEvent
{
	WDLEvents ev;
	int activator;
	int target;
	int gametic;
	fixed_t apos[3];
	fixed_t tpos[3];
	int arg0;
	int arg1;
	int arg2;
	int arg3;
};

auto inline format_as(const WDLEvent& ev)
{
	//                 "ev,ac,tg,gt,ax,ay,az,tx,ty,tz,a0,a1,a2,a3"
	return fmt::format("{},{},{},{},{},{},{},{},{},{},{},{},{},{}", ev.ev,
	                   ev.activator, ev.target, ev.gametic, ev.apos[0], ev.apos[1],
	                   ev.apos[2], ev.tpos[0], ev.tpos[1], ev.tpos[2], ev.arg0,
	                   ev.arg1, ev.arg2, ev.arg3);
}

// Events that we're keeping track of.
typedef std::vector<WDLEvent> WDLEventLog;
static WDLEventLog wdlevents;

// Turn an event enum into a string.
//static const char* WDLEventString(WDLEvents i)
//{
//	if (i >= ARRAY_LENGTH(::wdlevstrings) || i < 0)
//		return "UNKNOWN";
//	return ::wdlevstrings[i];
//}

static void AddWDLPlayer(player_t* player)
{
	// Don't add player if their name is already in the vector.
	// [Blair] Check the player's team too as version six tracks all
	// connects/disconnects/team switches
	for (const auto& wdlplayer : ::wdlplayers)
	{
		if (wdlplayer.netname == player->userinfo.netname &&
		    wdlplayer.team == player->userinfo.team && wdlplayer.pid == player->id)
			return;
	}

	WDLPlayer wdlplayer = {
	    static_cast<int>(::wdlplayers.size() + 1),
	    player->id,
	    player->userinfo.netname,
	    player->userinfo.team,
	};
	::wdlplayers.push_back(wdlplayer);
}

static void AddWDLPlayerSpawn(const mapthing2_t* mthing)
{

	team_t team = TEAM_NONE;

	if (sv_teamspawns != 0)
	{
		if (mthing->type == 5080)
			team = TEAM_BLUE;
		else if (mthing->type == 5081)
			team = TEAM_RED;
		else if (mthing->type == 5083)
			team = TEAM_GREEN;
	}

	// [Blair] Add player spawns to the table with team info.
	for (const auto& spawn : ::wdlplayerspawns)
	{
		if (spawn.x == mthing->x && spawn.y == mthing->y && spawn.z == mthing->z &&
		    spawn.team == team)
			return;
	}

	WDLPlayerSpawn wdlplayerspawn = {static_cast<int>(::wdlplayerspawns.size() + 1), mthing->x, mthing->y,
	                                 mthing->z, team};
	::wdlplayerspawns.push_back(wdlplayerspawn);
}

static void AddWDLFlagLocation(const mapthing2_t* mthing, team_t team)
{
	// [Blair] Add flag pedestals to the table.
	for (const auto& loc : ::wdlflaglocations)
	{
		if (loc.x == mthing->x && loc.y == mthing->y && loc.z == mthing->z &&
		    loc.team == team)
			return;
	}

	WDLFlagLocation wdlflaglocation = {team, mthing->x, mthing->y, mthing->z};
	::wdlflaglocations.push_back(wdlflaglocation);
}

static void RemoveWDLPlayerSpawn(const mapthing2_t* mthing)
{
	bool found = false;
	WDLPlayerSpawn w;

	for (const auto& spawn : ::wdlplayerspawns)
	{
		if (spawn.x == mthing->x && spawn.y == mthing->y && spawn.z == mthing->z)
		{
			w = spawn;
			found = true;
			break;
		}
	}

	if (!found)
		return;

	wdlplayerspawns.erase(std::find(::wdlplayerspawns.begin(), ::wdlplayerspawns.end(), w));

	return;
}

int GetItemSpawn(int x, int y, int z, WDLPowerups item)
{
	for (const auto& spawn : ::wdlitemspawns)
	{
		if (spawn.x == x && spawn.y == y && spawn.z == z)
			return spawn.id;
	}
	return 0;
}

void M_LogWDLItemSpawn(AActor* target, WDLPowerups type)
{
	// [Blair] Add item spawn to the table.
	// Don't add an overlapping item spawn, treat it as one.
	for (const auto& spawn : ::wdlitemspawns)
	{
		if (spawn.x == target->x && spawn.y == target->y && spawn.z == target->z &&
		    spawn.item == type)
			return;
	}

	WDLItemSpawn wdlitemspawn = {static_cast<int>(::wdlitemspawns.size() + 1), target->x, target->y,
	                             target->z, type};
	::wdlitemspawns.push_back(wdlitemspawn);
}

WDLPowerups M_GetWDLItemByMobjType(const mobjtype_t type)
{
	// [Blair] Return a WDL item based on the actor that was spawned.
	// Helps with pickup table lookups.

	WDLPowerups itemid;

	switch (type)
	{
	case MT_MISC0:
		itemid = WDL_PICKUP_GREENARMOR;
		break;
	case MT_MISC1:
		itemid = WDL_PICKUP_BLUEARMOR;
		break;
	case MT_MISC2:
		itemid = WDL_PICKUP_HEALTHBONUS;
		break;
	case MT_MISC3:
		itemid = WDL_PICKUP_ARMORBONUS;
		break;
	case MT_MISC10:
		itemid = WDL_PICKUP_STIMPACK;
		break;
	case MT_MISC11:
		itemid = WDL_PICKUP_MEDKIT;
		break;
	case MT_MISC12:
		itemid = WDL_PICKUP_SOULSPHERE;
		break;
	case MT_INV:
		itemid = WDL_PICKUP_INVULNSPHERE;
		break;
	case MT_MISC13:
		itemid = WDL_PICKUP_BERSERK;
		break;
	case MT_INS:
		itemid = WDL_PICKUP_INVISSPHERE;
		break;
	case MT_MISC14:
		itemid = WDL_PICKUP_RADSUIT;
		break;
	case MT_MISC15:
		itemid = WDL_PICKUP_COMPUTERMAP;
		break;
	case MT_MISC16:
		itemid = WDL_PICKUP_GOGGLES;
		break;
	case MT_MEGA:
		itemid = WDL_PICKUP_MEGASPHERE;
		break;
	case MT_CLIP:
		itemid = WDL_PICKUP_CLIP;
		break;
	case MT_MISC17:
		itemid = WDL_PICKUP_AMMOBOX;
		break;
	case MT_MISC18:
		itemid = WDL_PICKUP_ROCKET;
		break;
	case MT_MISC19:
		itemid = WDL_PICKUP_ROCKETBOX;
		break;
	case MT_MISC20:
		itemid = WDL_PICKUP_CELL;
		break;
	case MT_MISC21:
		itemid = WDL_PICKUP_CELLPACK;
		break;
	case MT_MISC22:
		itemid = WDL_PICKUP_SHELLS;
		break;
	case MT_MISC23:
		itemid = WDL_PICKUP_SHELLBOX;
		break;
	case MT_MISC24:
		itemid = WDL_PICKUP_BACKPACK;
		break;
	case MT_MISC25:
		itemid = WDL_PICKUP_BFG;
		break;
	case MT_CHAINGUN:
		itemid = WDL_PICKUP_CHAINGUN;
		break;
	case MT_MISC26:
		itemid = WDL_PICKUP_CHAINSAW;
		break;
	case MT_MISC27:
		itemid = WDL_PICKUP_ROCKETLAUNCHER;
		break;
	case MT_MISC28:
		itemid = WDL_PICKUP_PLASMAGUN;
		break;
	case MT_SHOTGUN:
		itemid = WDL_PICKUP_SHOTGUN;
		break;
	case MT_SUPERSHOTGUN:
		itemid = WDL_PICKUP_SUPERSHOTGUN;
		break;
	case MT_CAREPACK:
		itemid = WDL_PICKUP_CAREPACKAGE;
		break;
	default:
		itemid = WDL_PICKUP_UNKNOWN;
		break;
	}

	return itemid;
}

// Generate a log filename based on the current time.
static std::string GenerateTimestamp()
{
	time_t ti = time(NULL);
	struct tm* lt = localtime(&ti);

	char buf[128];
	if (!strftime(&buf[0], ARRAY_LENGTH(buf), "%Y.%m.%d.%H.%M.%S", lt))
		return "";

	return std::string(buf, strlen(&buf[0]));
}

static void WDLStatsHelp()
{
	PrintFmt(PRINT_HIGH,
	         "wdlstats - Starts logging WDL statistics to the given directory.  Unless "
	         "you are running a WDL server, you probably are not interested in this.\n\n"
	         "Usage:\n"
	         "  ] wdlstats <DIRNAME>\n"
	         "  Starts logging WDL statistics in the directory DIRNAME.\n");
}

BEGIN_COMMAND(wdlstats)
{
	if (argc < 2)
	{
		WDLStatsHelp();
		return;
	}

	// Setting the stats dir tells us that we intend to log.
	::wdlstate.logdir = argv[1];

	// Ensure our path ends with a slash.
	if (::wdlstate.logdir.back() != PATHSEPCHAR)
		::wdlstate.logdir += PATHSEPCHAR;

	PrintFmt(PRINT_HIGH,
	         "wdlstats: Enabled, will log to directory \"{}\" on next map change.\n",
	         wdlstate.logdir);
}
END_COMMAND(wdlstats)

void M_StartWDLLog(bool newmap)
{
	if (::wdlstate.logdir.empty())
	{
		::wdlstate.recording = false;
		return;
	}

	/* This used to only support CTF
	*  but now, this supports all game modes.
	*  Also, we now require data that is created in
	*  game states that aren't levelstate, so we grab the
	*  levelstate from before the game finished.
	if (sv_gametype != 3)
	{
	    ::wdlstate.recording = false;
	    Printf(
	        PRINT_HIGH,
	        "wdlstats: Not logging, incorrect gametype.\n"
	    );
	    return;
	}


	// Ensure that we're not in an invalid warmup state.
	if (::levelstate.getState() != LevelState::INGAME)
	{
	    // [AM] This is a little too much inside baseball to print about.
	    ::wdlstate.recording = false;
	    return;
	}
	*/

	// Start with a fresh slate of events.
	::wdlevents.clear();

	// And a fresh set of players.
	::wdlplayers.clear();

	if (newmap)
	{
		::wdlflaglocations.clear();
		::wdlitemspawns.clear();
		::wdlplayerspawns.clear();
	}

	// set playerbeacons
	if (sv_playerbeacons)
		::wdlstate.enablebeacons = true;
	else
		::wdlstate.enablebeacons = false;

	// Turn on recording.
	::wdlstate.recording = true;

	// Set our starting tic.
	::wdlstate.begintic = ::gametic;

	PrintFmt(PRINT_HIGH, "wdlstats: Started, will log to directory \"{}\".\n",
	       wdlstate.logdir);
}

/**
 * Log a damage event.
 *
 * Because damage can come in multiple pieces, this checks for an existing
 * event this tic and adds to it if it finds one.
 *
 * Returns true if the function successfully appended to an existing event,
 * otherwise false if we need to generate a new event.
 */
static bool LogDamageEvent(WDLEvents event, player_t* activator, player_t* target,
                           int arg0, int arg1, int arg2)
{
	WDLEventLog::reverse_iterator it = ::wdlevents.rbegin();
	for (; it != ::wdlevents.rend(); ++it)
	{
		if ((*it).gametic != ::gametic)
		{
			// We're too late for events from last tic, so we must have a
			// new event.
			return false;
		}

		// Event type is the same?
		if ((*it).ev != event)
			continue;

		// Activator is the same?
		if ((*it).activator != activator->id)
			continue;

		// Target is the same?
		if ((*it).target != target->id)
			continue;

		// Update our existing event.
		(*it).arg0 += arg0;
		(*it).arg1 += arg1;
		return true;
	}

	// We ran through all our events, must be a new event.
	return false;
}

/**
 * Log a shot attempt made by a player.
 *
 * If there's already an accuracy record for this gametic with a populated actor
 * then create a new one because the shot hit more than 1 player.
 */
bool LogAccuracyShot(WDLEvents event, player_t* activator, int mod, angle_t angle)
{
	// See if we have an existing accuracy event for this tic.
	// If not, we need to create a new one
	// If there is an existing accuracy event for this tic and it has a target,
	// then there were more than 1 hits, create a new event.
	WDLEventLog::reverse_iterator it = ::wdlevents.rbegin();
	for (; it != ::wdlevents.rend(); ++it)
	{
		if ((*it).gametic != ::gametic)
		{
			// Whoops, we went a whole gametic without seeing an accuracy
			// to our name.
			break;
		}

		// Event type is the same?
		if ((*it).ev != event)
			continue;

		// Activator is the same?
		if ((*it).activator != activator->id)
			continue;

		// We found an existing accuracy event for this tic.
		// Do nothing.
		return true;
	}

	return false;
}

/**
 * Log a hit shot by a player
 *
 * Looks for an accuracy log somewhere in the backlog, if there is none, it
 * logs a message but continues.
 */
bool LogAccuracyHit(WDLEvents event, player_t* activator, player_t* target, int mod,
                    int hits)
{
	// See if we have an existing accuracy event for this tic.
	WDLEventLog::reverse_iterator it = ::wdlevents.rbegin();
	for (; it != ::wdlevents.rend(); ++it)
	{
		if ((*it).gametic != ::gametic)
		{
			// Whoops, we went a whole gametic without seeing an accuracy
			// to our name.
			break;
		}

		// Event type is the same?
		if ((*it).ev != event)
			continue;

		// Activator is the same?
		if ((*it).activator != activator->id)
			continue;

		// Target is the same?
		if ((*it).target != target->id && (*it).target != 0)
			continue;

		// Target
		int tx = 0;
		int ty = 0;
		int tz = 0;
		if (target != NULL)
		{
			tx = target->mo->x;
			ty = target->mo->y;
			tz = target->mo->z;
		}
		else
		{
			// Can't log a hit if it didn't hit anybody...
			return true;
		}

		// We found an existing accuracy event for this tic - increment the number of
		// shots hit if its a spread type
		(*it).target = target->id;
		(*it).arg2 += hits;
		(*it).tpos[0] = tx;
		(*it).tpos[1] = ty;
		(*it).tpos[2] = tz;
		return true;
	}
	// Not sure what happened but it can't find the event. Create one.
	return false;
}

// [Blair] Helper function to determine max amount of shots that a mod shoots at a time.
int GetMaxShotsForMod(int mod)
{
	switch (mod)
	{
	case MOD_FIST:
	case MOD_PISTOL:
	case MOD_CHAINGUN:
	case MOD_ROCKET:
	case MOD_R_SPLASH:
	case MOD_CHAINSAW:
	case MOD_PLASMARIFLE:
	case MOD_BFG_BOOM:
		return 1;
	case MOD_SHOTGUN:
		return 7;
	case MOD_BFG_SPLASH:
		return 40;
	case MOD_SSHOTGUN:
		return 20;
	}

	return 1;
}

/**
 * Log a WDL flag location.
 *
 *
 * Logs the initial flag location on spawn and puts it in the flag locations table.
 */
void M_LogWDLFlagLocation(mapthing2_t* activator, team_t team)
{
	AddWDLFlagLocation(activator, team);
}

/**
 * Log a WDL item respawn event.
 *
 *
 * Logs each time an item respawned during a WDL log recording.
 */
void M_LogWDLItemRespawnEvent(AActor* activator)
{
	if (!::wdlstate.recording)
		return;

	// Activator
	fixed_t itemspawnid = 0;
	WDLPowerups itemtype = WDL_PICKUP_UNKNOWN;

	int ax = 0;
	int ay = 0;
	int az = 0;
	if (activator != NULL)
	{
		itemtype = M_GetWDLItemByMobjType(activator->type);

		// Add the activator's body information.
		ax = activator->x;
		ay = activator->y;
		az = activator->z;

		// Add the id from the pickups table.
		itemspawnid = GetItemSpawn(ax, ay, az, itemtype);
	}

	// Add the event to the log.
	WDLEvent evt = {WDL_EVENT_SPAWNITEM, 0,     0,        ::gametic, {ax, ay, az},
	                {0, 0, 0},           itemtype, itemspawnid, 0,         0};
	::wdlevents.push_back(evt);
}

/**
 * Log a WDL pickup event.
 *
 *
 * This will log a player item or weapon pickup, and check it against the current pickup
 * spawn table to determine if it needs to be added. This does have a chance to record a
 * ton of moving pickups on a conveyer belt or something, but whatever consumes the data
 * can ignore item pickups that only get picked up at the same location once if item
 * respawn is on.
 */
void M_LogWDLPickupEvent(player_t* activator, AActor* target, WDLPowerups pickuptype,
                         bool dropped)
{
	if (!::wdlstate.recording)
		return;

	int dropitem = 0;

	if (dropped)
		dropitem = 1;

	// Activator
	fixed_t aid = 0;
	int ax = 0;
	int ay = 0;
	int az = 0;
	if (activator != NULL)
	{
		// Add the activator.
		AddWDLPlayer(activator);
		aid = activator->id;

		// Add the activator's body information.
		if (activator->mo)
		{
			ax = activator->mo->x >> FRACBITS;
			ay = activator->mo->y >> FRACBITS;
			az = activator->mo->z >> FRACBITS;
		}
	}

	// Target
	fixed_t tid = 0;
	fixed_t itemspawnid = 0;
	int tx = 0;
	int ty = 0;
	int tz = 0;
	if (target != NULL)
	{
		tx = target->x;
		ty = target->y;
		tz = target->z;

		// Add the target.
		if (!dropped)
			itemspawnid = GetItemSpawn(tx, ty, tz, pickuptype);
	}

	// Add the event to the log.
	WDLEvent evt = {
	    WDL_EVENT_PICKUPITEM, aid,        tid,         ::gametic, {ax, ay, az},
	    {tx, ty, tz},         pickuptype, itemspawnid, dropitem,  0};
	::wdlevents.push_back(evt);
}

/**
 * Log a WDL event.
 *
 * The particulars of what you pass to this needs to be checked against the document.
 */
void M_LogWDLEvent(WDLEvents event, player_t* activator, player_t* target, int arg0,
                   int arg1, int arg2, int arg3)
{
	if (!::wdlstate.recording)
		return;

	if (event == WDL_EVENT_PLAYERBEACON && !::wdlstate.enablebeacons)
		return;

	// Activator
	fixed_t aid = 0;
	int ax = 0;
	int ay = 0;
	int az = 0;
	if (activator != NULL)
	{
		// Add the activator.
		AddWDLPlayer(activator);
		aid = activator->id;

		// Add the activator's body information.
		if (activator->mo)
		{
			ax = activator->mo->x >> FRACBITS;
			ay = activator->mo->y >> FRACBITS;
			az = activator->mo->z >> FRACBITS;
		}
	}

	// Target
	fixed_t tid = 0;
	int tx = 0;
	int ty = 0;
	int tz = 0;
	if (target != NULL)
	{
		// Add the target.
		AddWDLPlayer(target);
		tid = target->id;

		// Add the target's body information.
		if (target->mo)
		{
			tx = target->mo->x >> FRACBITS;
			ty = target->mo->y >> FRACBITS;
			tz = target->mo->z >> FRACBITS;
		}
	}

	// Damage events are handled specially.
	if (activator && target &&
	    (event == WDL_EVENT_DAMAGE || event == WDL_EVENT_CARRIERDAMAGE))
	{
		if (LogDamageEvent(event, activator, target, arg0, arg1, arg2))
			return;
	}

	if (activator && !target &&
	    (event == WDL_EVENT_SSACCURACY || event == WDL_EVENT_SPREADACCURACY ||
	     event == WDL_EVENT_PROJACCURACY || event == WDL_EVENT_TRACERACCURACY) &&
	    arg2 <= 0)
	{
		if (LogAccuracyShot(event, activator, arg1, arg0))
			return;
	}

	if (activator && target &&
	    (event == WDL_EVENT_SSACCURACY || event == WDL_EVENT_SPREADACCURACY ||
	     event == WDL_EVENT_PROJACCURACY || event == WDL_EVENT_TRACERACCURACY) &&
	    arg2 > 0)
	{
		if (LogAccuracyHit(event, activator, target, arg1, arg2))
			return;
	}

	// Add the event to the log.
	WDLEvent evt = {event,        aid,  tid,  ::gametic, {ax, ay, az},
	                {tx, ty, tz}, arg0, arg1, arg2,      arg3};
	::wdlevents.push_back(evt);
}

/**
 * Log a WDL event when you have actor pointers.
 */
void M_LogActorWDLEvent(WDLEvents event, AActor* activator, AActor* target, int arg0,
                        int arg1, int arg2, int arg3)
{
	if (!::wdlstate.recording)
		return;

	player_t* ap = NULL;
	if (activator != NULL && activator->type == MT_PLAYER)
		ap = activator->player;

	player_t* tp = NULL;
	if (target != NULL && target->type == MT_PLAYER)
		tp = target->player;

	M_LogWDLEvent(event, ap, tp, arg0, arg1, arg2, arg3);
}

void M_LogWDLPlayerSpawn(mapthing2_t* mthing)
{
	AddWDLPlayerSpawn(mthing);
}

void M_RemoveWDLPlayerSpawn(mapthing2_t* mthing)
{
	RemoveWDLPlayerSpawn(mthing);
}

void M_HandleWDLNameChange(team_t team, std::string oldname, std::string newname, int pid)
{
	if (!::wdlstate.recording)
		return;

	for (auto& player : ::wdlplayers)
	{
		// Attempt a rename but don't go nuts.
		if (player.pid == pid && player.netname == oldname && player.team == team)
		{
			player.netname = newname;
			return;
		}
	}
}

int M_GetPlayerSpawn(int x, int y)
{
	if (!::wdlstate.recording)
		return 0;

	for (const auto& spawn : ::wdlplayerspawns)
	{
		if (spawn.x == x && spawn.y == y)
			return spawn.id;
	}
	return 0;
}

int M_GetPlayerId(player_t* player, team_t team)
{
	if (!::wdlstate.recording)
		return 0;

	// This gets called before the log function itself, so add it.
	AddWDLPlayer(player);

	// Make real good sure its in there.
	WDLPlayers::const_iterator it = ::wdlplayers.begin();
	for (; it != ::wdlplayers.end(); ++it)
	{
		if ((*it).pid == player->id && (*it).netname == player->userinfo.netname &&
		    (*it).team == team)
			return (*it).id;
	}
	return 0;
}

void M_CommitWDLLog()
{
	if (!::wdlstate.recording || wdlevents.empty() ||
	    ::levelstate.getState() != LevelState::INGAME)
		return;

	// See if we can write a file.
	std::string timestamp = GenerateTimestamp();
	std::string filename = ::wdlstate.logdir + "wdl_" + timestamp + ".log";

	// [Blair] Serialize the hashes before reading.
	uint64_t reconsthash1 =
	    (uint64_t)(::level.level_fingerprint[0]) |
		(uint64_t)(::level.level_fingerprint[1]) << 8 |
	    (uint64_t)(::level.level_fingerprint[2]) << 16 |
	    (uint64_t)(::level.level_fingerprint[3]) << 24 |
	    (uint64_t)(::level.level_fingerprint[4]) << 32 |
	    (uint64_t)(::level.level_fingerprint[5]) << 40 |
	    (uint64_t)(::level.level_fingerprint[6]) << 48 |
		(uint64_t)(::level.level_fingerprint[7]) << 56;

	uint64_t reconsthash2 =
		(uint64_t)(::level.level_fingerprint[8]) |
	    (uint64_t)(::level.level_fingerprint[9]) << 8 |
	    (uint64_t)(::level.level_fingerprint[10]) << 16 |
	    (uint64_t)(::level.level_fingerprint[11]) << 24 |
	    (uint64_t)(::level.level_fingerprint[12]) << 32 |
	    (uint64_t)(::level.level_fingerprint[13]) << 40 |
	    (uint64_t)(::level.level_fingerprint[14]) << 48 |
	    (uint64_t)(::level.level_fingerprint[15]) << 56;

	// [Blair] Make the in-file timestamp ISO 8601 instead of a homegrown one.
	// However, keeping the homegrown one for filename as ISO 8601 characters
	// aren't supported in Windows filenames.
	time_t now;
	time(&now);
	char iso8601buf[sizeof "2011-10-08T07:07:09Z"];
	strftime(iso8601buf, sizeof iso8601buf, "%Y-%m-%dT%H:%M:%SZ", gmtime(&now));

	FILE* fh = fopen(filename.c_str(), "w+");
	if (fh == NULL)
	{
		::wdlstate.recording = false;
		PrintFmt(PRINT_HIGH, "wdlstats: Could not save\"%s\" for writing.\n",
		         filename);
		return;
	}

	// Header (metadata)
	fmt::print(fh, "version={}\n", WDLSTATS_VERSION);
	fmt::print(fh, "time={}\n", iso8601buf);
	fmt::print(fh, "levelnum={}\n", ::level.levelnum);
	fmt::print(fh, "levelname={}\n", ::level.level_name);
	fmt::print(fh, "levelhash={:016x}{:016x}\n", reconsthash1, reconsthash2);
	fmt::print(fh, "gametype={}\n", ::sv_gametype.str());
	fmt::print(fh, "lives={}\n", ::g_lives.str());
	fmt::print(fh, "attackdefend={}\n", ::g_sides.str());
	fmt::print(fh, "duration={}\n", ::gametic - ::wdlstate.begintic);
	fmt::print(fh, "endgametic={}\n", ::gametic);
	fmt::print(fh, "round={}\n", ::levelstate.getRound());
	fmt::print(fh, "winresult={}\n", static_cast<int>(::levelstate.getWinInfo().type));
	fmt::print(fh, "winid={}\n", ::levelstate.getWinInfo().id);
	fmt::print(fh, "hostname={}\n", ::sv_hostname.str());

	// Players
	fmt::print(fh, "players\n");
	for (const auto& pl : ::wdlplayers)
		fmt::print(fh, "{},{},{},{}\n", pl.id, pl.pid, static_cast<int>(pl.team), pl.netname);

	// ItemSpawns
	fmt::print(fh, "itemspawns\n");
	for (const auto& is : ::wdlitemspawns)
		fmt::print(fh, "{},{},{},{},{}\n", is.id, is.x, is.y, is.z, static_cast<int>(is.item));

	// PlayerSpawns
	fmt::print(fh, "playerspawns\n");
	for (const auto& ps : ::wdlplayerspawns)
		fmt::print(fh, "{},{},{},{},{}\n", ps.id, static_cast<int>(ps.team), ps.x, ps.y, ps.z);

	if (sv_gametype == GM_CTF)
	{
		// FlagLocation
		fmt::print(fh, "flaglocations\n");
		for (const auto& fl : ::wdlflaglocations)
			fmt::print(fh, "{},{},{},{}\n", static_cast<int>(fl.team), fl.x, fl.y, fl.z);
	}

	// Wads
	fmt::print(fh, "wads\n");
	fmt::print(fh, "{}", M_GetCurrentWadHashes());

	// Events
	fmt::print(fh, "events\n");
	for (const auto& ev : ::wdlevents)
		fmt::print(fh, "{}\n", ev);

	fclose(fh);

	// Turn off stat recording global - it must be turned on again by the
	// log starter next go-around.
	::wdlstate.recording = false;

	PrintFmt(PRINT_HIGH, "wdlstats: Log saved as \"{}\".\n", filename);
}

static void PrintWDLEvent(const WDLEvent& evt)
{
	PrintFmt(PRINT_HIGH, "{}\n", evt);
}

static void WDLInfoHelp()
{
	PrintFmt(PRINT_HIGH,
	         "wdlinfo - Looks up internal information about logged WDL events\n\n"
	         "Usage:\n"
	         "  ] wdlinfo event <ID>\n"
	         "  Print the event by ID.\n\n"
	         "  ] wdlinfo size\n"
	         "  Return the size of the internal event array.\n\n"
	         "  ] wdlinfo state\n"
	         "  Return relevant WDL stats state.\n\n"
	         "  ] wdlinfo tail\n"
	         "  Print the last 10 events.\n");
}

BEGIN_COMMAND(wdlinfo)
{
	if (argc < 2)
	{
		WDLInfoHelp();
		return;
	}

	if (stricmp(argv[1], "size") == 0)
	{
		// Count total events.
		PrintFmt(PRINT_HIGH, "{} events found\n", ::wdlevents.size());
		return;
	}
	else if (stricmp(argv[1], "state") == 0)
	{
		// Count total events.
		PrintFmt(PRINT_HIGH, "Currently recording?: {}\n",
		         ::wdlstate.recording ? "Yes" : "No");
		PrintFmt(PRINT_HIGH, "Directory to write logs to: \"{}\"\n",
		         ::wdlstate.logdir);
		PrintFmt(PRINT_HIGH, "Log starting gametic: {}\n", ::wdlstate.begintic);
		return;
	}
	else if (stricmp(argv[1], "tail") == 0)
	{
		// [Blair] C++ doesn't like when you access an iterator on an empty vector.
		if (::wdlevents.empty())
		{
			PrintFmt(PRINT_HIGH, "No events to show.\n");
			return;
		}
		// Show last 10 events.
		WDLEventLog::const_iterator it = ::wdlevents.end() - 10;
		if (it < ::wdlevents.begin())
			it = wdlevents.begin();

		PrintFmt(PRINT_HIGH, "Showing last {} events:\n",
		         ::wdlevents.end() - it);
		for (; it != ::wdlevents.end(); ++it)
			PrintWDLEvent(*it);
		return;
	}

	if (argc < 3)
	{
		WDLInfoHelp();
		return;
	}

	if (stricmp(argv[1], "event") == 0)
	{
		int id = atoi(argv[2]);
		if (id >= static_cast<int>(::wdlevents.size()))
		{
			PrintFmt(PRINT_HIGH, "Event number {} not found\n", id);
			return;
		}
		WDLEvent evt = ::wdlevents.at(id);
		PrintWDLEvent(evt);
		return;
	}

	// Unknown command.
	WDLInfoHelp();
}
END_COMMAND(wdlinfo)
