// -----------------------------------------------------------------------------
// webradar.cpp
// -----------------------------------------------------------------------------
// Implementation of the web radar publisher. The thread produces the radar
// state (map name, player pawns, local controller) as the valthrun protocol
// v2 JSON every 50 ms and sends it over an IXWebSocket connection to
// wss://radar.valth.run/publish (mbedTLS backend). Connect() performs the
// handshake and the initialize-publish session registration and waits for
// the server responses; the WebSocket's own thread dispatches them to
// HandleServerMessage.
//
// The JSON payload matches radar/shared/src/types.rs of valthrun-cs2
// (RADAR_PROTOCOL_VERSION 2):
//
//   {"type":"notify-radar-state","payload":{"state":{
//     "worldName":"de_dust2",
//     "playerPawns":[{
//       "controllerEntityId":5,"pawnEntityId":45,"teamId":2,
//       "playerName":"x","playerHealth":100,"playerHasDefuser":false,
//       "playerFlashtime":0,"weapon":0,
//       "position":[-2476,3239,10],"rotation":1.57
//     }],
//     "plantedC4":{"position":[-2476,3239,10],"bombSite":0,
//                  "state":{"state":"active","timeDetonation":30.1,
//                           "timeTotal":40.0,"defuser":null}},
//     "c4Entities":[{"entityId":123,"position":[...],"ownerEntityId":45}],
//     "localControllerEntityId":5
//   }}}
//
// Position is sent in absolute world units (Source coordinates); the
// browser frontend converts them with its per-map constants.
// -----------------------------------------------------------------------------

#include "webradar.h"

#include <chrono>
#include <cmath>
#include <condition_variable>
#include <mutex>
#include <sstream>
#include <windows.h>

#include <ixwebsocket/IXNetSystem.h>
#include <ixwebsocket/IXWebSocketMessageType.h>

#include "../config.h"
#include "../util/logger.h"

namespace features {

namespace {

// The central publish endpoint (see valthrun client-standalone main.rs).
constexpr const char* kPublishUrl = "wss://radar.valth.run/publish";

// How long Connect() waits for the handshake + session responses.
constexpr int kHandshakeTimeoutSecs = 15;

// Reconnect delay after a failed connect attempt.
constexpr DWORD kConnectRetryMs = 1000;

// Minimum spacing between handshake requests after a rejection. The server
// answers rejections instantly, so without this gate a rejected handshake
// would retry in a tight request/response loop.
constexpr DWORD kHandshakeRetryMs = 2000;

// The publish cadence used by the valthrun client (20 Hz).
constexpr DWORD kPublishIntervalMs = 50;

// How long the radar may stay off before its session is considered dead:
// after this, the stored session (id + auth token) is dropped so the next
// enable registers a fresh session id instead of resuming the old URL.
constexpr DWORD kFreshSessionAfterOffMs = 60000;   // 1 minute

// ---------------------------------------------------------------------------
// Minimal JSON writer. The project has no JSON DOM, so serialization is
// done with a tiny streaming writer here.
// ---------------------------------------------------------------------------
class JsonWriter
{
public:
	std::string Str() const { return m_out.str(); }

	void BeginObject() { m_out << "{"; m_first = true; }
	void EndObject()   { m_out << "}"; m_first = false; }

	void BeginArray()
	{
		if (!m_first) {
			m_out << ",";
		}
		m_first = true;
		m_out << "[";
	}
	void EndArray()    { m_out << "]"; m_first = false; }

	void BeginMember(const std::string& key)
	{
		if (!m_first) {
			m_out << ",";
		}
		m_first = false;
		WriteString(key);
		m_out << ":";
		m_first = true;
	}

	void String(const std::string& value)
	{
		if (!m_first) {
			m_out << ",";
		}
		m_first = false;
		WriteString(value);
	}

	template <typename T>
	void Number(T value)
	{
		if (!m_first) {
			m_out << ",";
		}
		m_first = false;
		m_out << static_cast<long long>(value);
	}

	void Number(double value)
	{
		if (!m_first) {
			m_out << ",";
		}
		m_first = false;
		if (!std::isfinite(value)) {
			m_out << "0";
			return;
		}
		m_out << value;
	}

	void Bool(bool value)
	{
		if (!m_first) {
			m_out << ",";
		}
		m_first = false;
		m_out << (value ? "true" : "false");
	}

	void Null()
	{
		if (!m_first) {
			m_out << ",";
		}
		m_first = false;
		m_out << "null";
	}

	// Emits an already-serialized JSON fragment as-is (no quoting or
	// escaping). Used for object fragments built by the state code, e.g.
	// the player pawn objects inside playerPawns.
	void Raw(const std::string& value)
	{
		if (!m_first) {
			m_out << ",";
		}
		m_first = false;
		m_out << value;
	}

private:
	void WriteString(const std::string& value)
	{
		m_out << "\"";
		for (char c : value) {
			switch (c) {
				case '"':  m_out << "\\\""; break;
				case '\\': m_out << "\\\\"; break;
				case '\n': m_out << "\\n"; break;
				case '\r': m_out << "\\r"; break;
				case '\t': m_out << "\\t"; break;
				default:
					if (static_cast<unsigned char>(c) < 0x20) {
						char buf[8];
						sprintf_s(buf, "\\u%04x", c);
						m_out << buf;
					} else {
						m_out << c;
					}
			}
		}
		m_out << "\"";
	}

	std::ostringstream m_out;
	bool m_first = true;
};

} // namespace

void WebRadar::Initialize(const core::ProcessMemory* memory, const core::GameOffsets* offsets,
	const sdk::EntityList* entities, const sdk::LocalPlayer* player,
	uintptr_t clientBase, uintptr_t engineBase)
{
	m_memory = memory;
	m_offsets = offsets;
	m_entities = entities;
	m_player = player;
	m_clientBase = clientBase;
	m_engineBase = engineBase;
}

void WebRadar::CreateSocketAndStart()
{
	ix::initNetSystem();

	m_websocket = std::make_shared<ix::WebSocket>();
	m_websocket->setUrl(kPublishUrl);
	m_websocket->setHandshakeTimeout(10);
	m_websocket->setPingInterval(10);

	m_websocket->setOnMessageCallback([this](const ix::WebSocketMessagePtr& msg) {
		try {
			switch (msg->type) {
				case ix::WebSocketMessageType::Open:
					m_initialized = false;
					{
						std::lock_guard<std::mutex> lock(m_handshakeMutex);
						m_handshakeDone = false;
						m_handshakeOk = false;
					}
					util::LogDebug("[webradar] socket open, sending handshake");
					m_lastHandshakeRequest = GetTickCount();
					m_websocket->send("{\"type\":\"request-initialize\",\"payload\":{\"clientVersion\":2}}");
					break;

				case ix::WebSocketMessageType::Message:
					if (HandleServerMessage(msg->str)) {
						std::lock_guard<std::mutex> lock(m_handshakeMutex);
						m_handshakeDone = true;
						m_handshakeOk = m_connected;
						m_handshakeCv.notify_all();
					}
					break;

				case ix::WebSocketMessageType::Error:
					util::LogError("[webradar] transport error: " + msg->errorInfo.reason);
					{
						std::lock_guard<std::mutex> lock(m_handshakeMutex);
						m_handshakeOk = false;
						m_handshakeDone = true;
						m_handshakeCv.notify_all();
					}
					break;

				case ix::WebSocketMessageType::Close:
					util::LogWarning("[webradar] connection closed (code=" +
						std::to_string(msg->closeInfo.code) + " reason=" + msg->closeInfo.reason + ")");
					{
						std::lock_guard<std::mutex> lock(m_handshakeMutex);
						m_handshakeOk = false;
						m_handshakeDone = true;
						m_handshakeCv.notify_all();
					}
					break;

				default:
					break;
			}
		} catch (const std::exception& e) {
			ForceOff(e.what());
		} catch (...) {
			ForceOff("unknown callback exception");
		}
	});

	m_websocket->start();
}

void WebRadar::Start()
{
	CreateSocketAndStart();
	CreateThread(nullptr, 0, &WebRadar::ThreadProc, this, 0, nullptr);
}

unsigned long __stdcall WebRadar::ThreadProc(void* param)
{
	WebRadar* self = static_cast<WebRadar*>(param);
	if (self == nullptr) {
		return 1;
	}

		while (true) {
		try {
			if (!cfg::g_config.webRadarEnabled) {
				if (self->m_connected) {
					self->m_radarOffSince = GetTickCount();
					self->Disconnect();
				} else if (self->m_radarOffSince == 0) {
					self->m_radarOffSince = GetTickCount();
				}

				if (GetTickCount() - self->m_radarOffSince >= kFreshSessionAfterOffMs) {
					self->ResetSession();
				}
				Sleep(250);
				continue;
			}

			if (!self->m_connected) {
				if (!self->Connect()) {
					Sleep(kConnectRetryMs);
					continue;
				}
			}

			if (!self->PublishState()) {
				self->Disconnect();
				self->InvalidateHandshake();
				Sleep(kConnectRetryMs);
				continue;
			}
			Sleep(kPublishIntervalMs);
		} catch (const std::exception& e) {
			self->ForceOff(e.what());
			self->Disconnect();
			self->InvalidateHandshake();
			Sleep(kConnectRetryMs);
		} catch (...) {
			self->ForceOff("unknown thread exception");
			self->Disconnect();
			self->InvalidateHandshake();
			Sleep(kConnectRetryMs);
		}
	}
	return 0;
}

// ---------------------------------------------------------------------------
// WebSocket transport (IXWebSocket + mbedTLS).
// ---------------------------------------------------------------------------

bool WebRadar::Connect()
{
	if (!m_websocket) {
		CreateSocketAndStart();
	}
	if (!m_websocket) {
		return false;
	}

	std::unique_lock<std::mutex> lock(m_handshakeMutex);

	if (m_handshakeOk) {
		return true;
	}

	util::LogDebug("[webradar] waiting for session handshake");
	m_handshakeDone = false;
	if (!m_handshakeCv.wait_for(lock, std::chrono::seconds(kHandshakeTimeoutSecs),
		[this] { return m_handshakeDone; })) {
		util::LogError("[webradar] handshake timed out");
		return false;
	}

	if (!m_handshakeOk) {
		util::LogError("[webradar] handshake failed");
		return false;
	}

	util::LogDebug("[webradar] session ready: " + GetSessionUrl());
	return true;
}

void WebRadar::InvalidateHandshake()
{
	std::lock_guard<std::mutex> lock(m_handshakeMutex);
	m_handshakeOk = false;
	m_handshakeDone = true;
	m_handshakeCv.notify_all();
}

void WebRadar::NoteResumeFailure()
{
	std::lock_guard<std::mutex> lock(m_sessionMutex);
	if (m_sessionToken.empty()) {
		return;
	}
	if (++m_resumeFailures < 3) {
		return;
	}
	m_resumeFailures = 0;
	util::LogWarning("[webradar] session auth token rejected repeatedly; starting a fresh session");
	m_sessionToken.clear();
	m_sessionId.clear();
	m_sessionUrl.clear();
}

void WebRadar::ResetSession()
{
	std::lock_guard<std::mutex> lock(m_sessionMutex);
	if (m_sessionId.empty() && m_sessionToken.empty()) {
		return;
	}
	m_sessionId.clear();
	m_sessionToken.clear();
	m_sessionUrl.clear();
	m_resumeFailures = 0;
	util::LogInfo("[webradar] radar off for over a minute; dropped the session (fresh id on next enable)");
}

void WebRadar::ForceOff(const std::string& reason)
{
	util::LogError("[webradar] error, turning radar off: " + reason);
	cfg::g_config.webRadarEnabled = false;
}

bool WebRadar::SendFrame(const std::string& json)
{
	if (!m_websocket || !m_connected) {
		return false;
	}

	const ix::WebSocketSendInfo info = m_websocket->send(json);
	if (!info.success) {
		util::LogWarning("[webradar] send failed");
		return false;
	}
	return true;
}

void WebRadar::Disconnect()
{
	m_connected = false;
	if (m_websocket) {
		try {
			m_websocket.reset();
		} catch (const std::exception& e) {
			util::LogError(std::string("[webradar] socket teardown failed: ") + e.what());
		} catch (...) {
			util::LogError("[webradar] socket teardown failed (unknown)");
		}
	}
}

bool WebRadar::HandleServerMessage(const std::string& json)
{
	if (json.find("\"type\":\"response-success\"") != std::string::npos) {
		if (m_initialized) {
			return false;
		}
		m_initialized = true;

		util::LogDebug("[webradar] handshake ok (server accepted), registering session");
		std::string message = "{\"type\":\"initialize-publish\",\"payload\":";
		{
			std::lock_guard<std::mutex> lock(m_sessionMutex);
			if (m_sessionToken.empty()) {
				message += "{}";
			} else {
				util::LogDebug("[webradar] resuming session with stored auth token");
				message += "{\"session_auth_token\":\"" + m_sessionToken + "\"}";
			}
		}
		message += "}";
		m_websocket->send(message);
		return false;
	}

	if (json.find("\"type\":\"response-initialize-publish\"") != std::string::npos) {
		const std::string sessionId = ExtractJsonString(json, "session_id");
		const std::string authToken = ExtractJsonString(json, "session_auth_token");
		{
			std::lock_guard<std::mutex> lock(m_sessionMutex);
			m_sessionId = sessionId;
			m_sessionToken = authToken;
			m_sessionUrl = "https://radar.valth.run/session/" + sessionId;
			m_resumeFailures = 0;
		}
		util::LogDebug("[webradar] session registered: session_id=" + sessionId);
		m_connected = true;
		return true;
	}

	if (json.find("\"type\":\"response-error\"") != std::string::npos ||
		json.find("\"type\":\"response-invalid-client-state\"") != std::string::npos ||
		json.find("\"type\":\"response-incompatible\"") != std::string::npos ||
		json.find("\"type\":\"response-generic-failure\"") != std::string::npos ||
		json.find("\"type\":\"response-session-invalid-id\"") != std::string::npos) {
		util::LogError("[webradar] server rejected the handshake: " + json);
		NoteResumeFailure();
		m_initialized = false;
		if (GetTickCount() - m_lastHandshakeRequest >= kHandshakeRetryMs) {
			m_lastHandshakeRequest = GetTickCount();
			m_websocket->send("{\"type\":\"request-initialize\",\"payload\":{\"clientVersion\":2}}");
		}
		return false;
	}

	return false;
}

std::string WebRadar::ExtractJsonString(const std::string& json, const std::string& key)
{
	const std::string needle = "\"" + key + "\":\"";
	const size_t pos = json.find(needle);
	if (pos == std::string::npos) {
		return "";
	}

	const size_t start = pos + needle.size();
	const size_t end = json.find('"', start);
	if (end == std::string::npos) {
		return "";
	}
	return json.substr(start, end - start);
}

std::string WebRadar::GetSessionUrl() const
{
	std::lock_guard<std::mutex> lock(m_sessionMutex);
	return m_sessionUrl;
}

// ---------------------------------------------------------------------------
// Radar state generation (no WebSocket needed).
// ---------------------------------------------------------------------------

std::string WebRadar::ReadWorldName()
{
	const unsigned long now = GetTickCount();
	if (now - m_lastWorldNameRead < 5000) {
		return m_worldName;
	}
	m_lastWorldNameRead = now;

	m_worldName.clear();
	if (m_engineBase == 0 || m_offsets->networkGameClient == 0 || m_offsets->mapName == 0) {
		return m_worldName;
	}

	const uintptr_t instance = m_memory->Read<uintptr_t>(m_engineBase + m_offsets->networkGameClient);
	if (instance == 0) {
		return m_worldName;
	}

	const uintptr_t mapNamePtr = m_memory->Read<uintptr_t>(instance + m_offsets->mapName);
	if (mapNamePtr == 0) {
		return m_worldName;
	}

	m_worldName = m_memory->ReadString(mapNamePtr, 128);
	util::LogDebug("[webradar] map name: '" + m_worldName + "'");
	return m_worldName;
}

int WebRadar::GetLocalControllerIndex()
{
	const uintptr_t localPawn = m_player->GetLocalPawn();
	if (localPawn == 0) {
		return -1;
	}

	for (int i = 0; i < sdk::kMaxPlayers; i++) {
		if (m_entities->GetPawn(i) == localPawn) {
			return i;
		}
	}
	return -1;
}

std::string WebRadar::ReadPlayerName(int slot)
{
	if (m_offsets->iszPlayerName == 0) {
		return "";
	}

	const uintptr_t controller = m_entities->GetRawEntity(slot);
	if (controller == 0) {
		return "";
	}
	return m_memory->ReadString(controller + m_offsets->iszPlayerName, 64);
}

unsigned short WebRadar::ReadWeaponId(uintptr_t /*pawn*/)
{
	return 0;
}

bool WebRadar::ReadHasDefuser(uintptr_t /*pawn*/)
{
	return false;
}

float WebRadar::ReadFlashTime(uintptr_t /*pawn*/)
{
	return 0.0f;
}

void WebRadar::BuildBombJson(std::string& outPlantedC4, std::string& outC4Entities)
{
	outPlantedC4.clear();
	outC4Entities.clear();

	if (GetTickCount() - m_lastBombScan > 1000) {
		m_lastBombScan = GetTickCount();
		ScanBombs();
	}

	if (m_c4Entity != 0) {
		sdk::Vec3 position;
		if (GetEntityPosition(m_c4Entity, position)) {
			JsonWriter w;
			w.BeginObject();
			w.BeginMember("entityId");
			w.Number(static_cast<unsigned int>(m_c4Index));
			w.BeginMember("position");
			w.BeginArray();
			w.Number(static_cast<double>(position.x));
			w.Number(static_cast<double>(position.y));
			w.Number(static_cast<double>(position.z));
			w.EndArray();
			w.BeginMember("ownerEntityId");
			if (m_offsets->hOwnerEntity == 0) {
				w.Null();
			} else {
				const uint32_t owner = m_memory->Read<uint32_t>(m_c4Entity + m_offsets->hOwnerEntity) & 0x7FFF;
				if (owner != 0) {
					w.Number(owner);
				} else {
					w.Null();
				}
			}
			w.EndObject();
			outC4Entities = w.Str();
		}
	}

	if (m_plantedC4Entity == 0) {
		return;
	}
	sdk::Vec3 position;
	if (!GetEntityPosition(m_plantedC4Entity, position)) {
		return;
	}

	const uint8_t bombSite = (m_offsets->plantedC4BombSite != 0)
		? m_memory->Read<uint8_t>(m_plantedC4Entity + m_offsets->plantedC4BombSite) : 0;
	const bool defused = (m_offsets->plantedC4BombDefused != 0) &&
		(m_memory->Read<uint8_t>(m_plantedC4Entity + m_offsets->plantedC4BombDefused) != 0);
	const bool exploded = (m_offsets->plantedC4HasExploded != 0) &&
		(m_memory->Read<uint8_t>(m_plantedC4Entity + m_offsets->plantedC4HasExploded) != 0);

	JsonWriter w;
	w.BeginObject();
	w.BeginMember("position");
	w.BeginArray();
	w.Number(static_cast<double>(position.x));
	w.Number(static_cast<double>(position.y));
	w.Number(static_cast<double>(position.z));
	w.EndArray();
	w.BeginMember("bombSite");
	w.Number(bombSite);
	w.BeginMember("state");
	w.BeginObject();
	w.BeginMember("state");
	if (defused) {
		w.String("defused");
	} else if (exploded) {
		w.String("detonated");
	} else {
		w.String("active");

		float timeTotal = (m_offsets->plantedC4TimerLength != 0)
			? m_memory->Read<float>(m_plantedC4Entity + m_offsets->plantedC4TimerLength) : 0.0f;
		float timeDetonation = timeTotal;
		float gameTime = 0.0f;
		if (m_offsets->globalVars != 0 && m_clientBase != 0) {
			const uintptr_t globals = m_memory->Read<uintptr_t>(m_clientBase + m_offsets->globalVars);
			if (globals != 0) {
				gameTime = m_memory->Read<float>(globals + 0x30);
			}
		}
		if (gameTime <= 0.0f && m_offsets->serverTickCount != 0 && m_engineBase != 0 &&
			m_offsets->networkGameClient != 0) {
			const uintptr_t instance = m_memory->Read<uintptr_t>(m_engineBase + m_offsets->networkGameClient);
			if (instance != 0) {
				const int serverTick = m_memory->Read<int>(instance + m_offsets->serverTickCount);
				if (serverTick > 0 && serverTick < 2000000000) {
					gameTime = static_cast<float>(serverTick) / 64.0f;
				}
			}
		}
		if (gameTime > 0.0f && m_offsets->plantedC4Blow != 0) {
			const float blow = m_memory->Read<float>(m_plantedC4Entity + m_offsets->plantedC4Blow);
			timeDetonation = blow - gameTime;
			if (timeDetonation < 0.0f) {
				timeDetonation = 0.0f;
			}
		}
		w.BeginMember("timeDetonation");
		w.Number(static_cast<double>(timeDetonation));
		w.BeginMember("timeTotal");
		w.Number(static_cast<double>(timeTotal));
		w.BeginMember("defuser");
		w.Null();
	}
	w.EndObject();
	w.EndObject();

	outPlantedC4 = w.Str();
}

void WebRadar::ScanBombs()
{
	const int c4Index = m_entities->FindEntityByClassName("C_C4");
	m_c4Index = (c4Index >= 0) ? static_cast<uintptr_t>(c4Index) : 0;
	m_c4Entity = (c4Index >= 0) ? m_entities->GetRawEntity(c4Index) : 0;

	const int plantedIndex = m_entities->FindEntityByClassName("C_PlantedC4");
	m_plantedC4Index = (plantedIndex >= 0) ? static_cast<uintptr_t>(plantedIndex) : 0;
	m_plantedC4Entity = (plantedIndex >= 0) ? m_entities->GetRawEntity(plantedIndex) : 0;

	std::stringstream ss;
	ss << "[webradar] bomb scan: c4Index=" << std::dec << c4Index
		<< " c4=0x" << std::hex << m_c4Entity
		<< " plantedC4Index=" << std::dec << plantedIndex
		<< " plantedC4=0x" << std::hex << m_plantedC4Entity;
	util::LogDebug(ss.str());
}

bool WebRadar::GetEntityPosition(uintptr_t entity, sdk::Vec3& outPosition) const
{
	outPosition = sdk::Vec3{};
	if (entity == 0 || m_offsets->gameSceneNode == 0 || m_offsets->vecAbsOrigin == 0) {
		return false;
	}

	const uintptr_t sceneNode = m_memory->Read<uintptr_t>(entity + m_offsets->gameSceneNode);
	if (sceneNode == 0) {
		util::LogDebug("[webradar] entity position: scene node is null");
		return false;
	}

	m_memory->ReadArray<float>(sceneNode + m_offsets->vecAbsOrigin, &outPosition.x, 3);
	return outPosition.x != 0.0f || outPosition.y != 0.0f || outPosition.z != 0.0f;
}

bool WebRadar::BuildPlayerJson(int slot, std::string& outFragment)
{
	const uintptr_t pawn = m_entities->GetPawn(slot);
	if (pawn == 0) {
		return false;
	}

	const int team = m_entities->GetTeam(slot);
	if (team != 2 && team != 3) {
		return false;
	}

	const int health = m_memory->Read<int>(pawn + m_offsets->health);
	const float yaw = m_memory->Read<float>(pawn + m_offsets->eyeAngles + 4);

	int pawnEntityId = 0;
	const uintptr_t controller = m_entities->GetRawEntity(slot);
	if (controller != 0 && m_offsets->hPlayerPawn != 0) {
		pawnEntityId = m_memory->Read<uint32_t>(controller + m_offsets->hPlayerPawn) & 0x7FFF;
	}

	sdk::Vec3 position = {};
	const uintptr_t sceneNode = m_memory->Read<uintptr_t>(pawn + m_offsets->gameSceneNode);
	if (sceneNode != 0 && m_offsets->vecAbsOrigin != 0) {
		m_memory->ReadArray<float>(sceneNode + m_offsets->vecAbsOrigin, &position.x, 3);
	}
	if (position.x == 0.0f && position.y == 0.0f && position.z == 0.0f) {
		m_entities->GetOrigin(slot, position);
	}

	JsonWriter w;
	w.BeginObject();
	w.BeginMember("controllerEntityId"); w.Number(slot);
	w.BeginMember("pawnEntityId");       w.Number(pawnEntityId);
	w.BeginMember("teamId");             w.Number(team);
	w.BeginMember("playerName");         w.String(ReadPlayerName(slot));
	w.BeginMember("playerHealth");       w.Number(health);
	w.BeginMember("playerHasDefuser");   w.Bool(ReadHasDefuser(pawn));
	w.BeginMember("playerFlashtime");    w.Number(static_cast<double>(ReadFlashTime(pawn)));
	w.BeginMember("weapon");             w.Number(ReadWeaponId(pawn));
	w.BeginMember("position");
	w.BeginArray();
	w.Number(static_cast<double>(position.x));
	w.Number(static_cast<double>(position.y));
	w.Number(static_cast<double>(position.z));
	w.EndArray();
	w.BeginMember("rotation");           w.Number(static_cast<double>(yaw));
	w.EndObject();

	outFragment = w.Str();
	return true;
}

bool WebRadar::PublishState()
{
	const int localController = GetLocalControllerIndex();

	JsonWriter w;
	w.BeginObject();
	w.BeginMember("type");
	w.String("notify-radar-state");
	w.BeginMember("payload");
	w.BeginObject();
	w.BeginMember("state");
	w.BeginObject();

	w.BeginMember("worldName");
	w.String(ReadWorldName());

	w.BeginMember("playerPawns");
	w.BeginArray();
	int count = 0;
	for (int i = 0; i < sdk::kMaxPlayers; i++) {
		std::string fragment;
		if (BuildPlayerJson(i, fragment)) {
			w.Raw(fragment);
			count++;
		}
	}
	w.EndArray();

	w.BeginMember("plantedC4");
	std::string plantedC4Fragment;
	std::string c4EntitiesFragments;
	BuildBombJson(plantedC4Fragment, c4EntitiesFragments);
	if (plantedC4Fragment.empty()) {
		w.Null();
	} else {
		w.Raw(plantedC4Fragment);
	}
	w.BeginMember("c4Entities");
	w.BeginArray();
	if (!c4EntitiesFragments.empty()) {
		w.Raw(c4EntitiesFragments);
	}
	w.EndArray();

	w.BeginMember("localControllerEntityId");
	w.Number(localController);

	w.EndObject();
	w.EndObject();
	w.EndObject();

	const std::string stateJson = w.Str();
	const bool sent = SendFrame(stateJson);

	static unsigned long publishCount = 0;
	if (sent && (++publishCount % 100) == 1) {
		util::LogDebug("[webradar] publishing state (" + std::to_string(count) +
			" players): " + stateJson);
	}
	return sent;
}

} // namespace features
