// -----------------------------------------------------------------------------
// offsets.cpp
// -----------------------------------------------------------------------------
// Implementation of OffsetsManager. All offsets are resolved locally:
//
//   1. Module globals by byte pattern scanning (patterns.h - the single
//      place to update when a game update breaks an offset).
//   2. Schema netvars from a live dump of the game's schema system (see
//      schema.cpp).
//
// Pattern resolution: a RIP-relative slot pattern resolves to
//   slot = match + instLength + rel32(match + relOffset)
// which is the module-relative offset of the referenced global. Depending
// on the global the slot either holds a pointer (entity list, local pawn)
// or is the struct itself (view matrix, view angles).
// -----------------------------------------------------------------------------

#include "offsets.h"

#include <map>

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

namespace core {

namespace {

// Fills dst with value only when dst is still zero (first source wins).
void FillIfZero(uintptr_t& dst, uintptr_t value)
{
	if (dst == 0 && value != 0) {
		dst = value;
	}
}

// Resolves one PatternDef and returns the module-relative slot offset,
// or 0 when the module or pattern was not found.
uintptr_t ResolveRelativeSlot(const ProcessMemory& memory, const PatternDef& def)
{
	const ModuleInfo module = memory.GetModule(def.module);
	if (module.base == 0 || module.size == 0) {
		util::LogWarning(std::string("pattern '") + def.name + "': module " + def.module + " not found");
		return 0;
	}

	const uintptr_t match = memory.FindPattern(module.base, module.size, def.pattern, def.mask);
	if (match == 0) {
		util::LogWarning(std::string("pattern '") + def.name + "': no match in " + def.module);
		return 0;
	}

	const int32_t rel = memory.Read<int32_t>(match + def.relOffset);
	const uintptr_t slot = match + def.instLength + static_cast<int64_t>(rel);
	if (slot < module.base || slot >= module.base + module.size) {
		return 0;
	}
	return slot - module.base;
}

// Resolves `chain` and returns the module-relative offset of the target
// global, or 0 when any step failed.
uintptr_t ResolveChained(const ProcessMemory& memory, const PatternDef& base, const ChainedStep& chain)
{
	const uintptr_t baseOffset = ResolveRelativeSlot(memory, base);
	if (baseOffset == 0) {
		return 0;
	}

	const ModuleInfo module = memory.GetModule(base.module);
	const uintptr_t match = memory.FindPattern(module.base, module.size, chain.pattern, chain.mask);
	if (match == 0) {
		util::LogWarning(std::string("pattern '") + base.name + "': chained scan failed");
		return 0;
	}

	const int32_t rel = memory.Read<int32_t>(match + chain.relOffset);
	const uintptr_t target = module.base + baseOffset + static_cast<int64_t>(rel);
	if (target < module.base || target >= module.base + module.size) {
		return 0;
	}
	return target - module.base;
}

} // namespace

// Returns true when the essential offsets were resolved.
bool GameOffsets::IsComplete() const
{
	return localPlayerPawn != 0 && viewAngles != 0 &&
		entityList != 0 && teamNum != 0 && health != 0 && gameSceneNode != 0;
}

// Loads and resolves all game offsets locally (patterns + schema dump).
bool OffsetsManager::Initialize(const ProcessMemory& memory)
{
	m_offsets = GameOffsets{};
	m_buildDatetime.clear();
	m_schemaSystemOffset = 0;
	m_buildInfoOffset = 0;

	if (!ResolveGlobalsBySignature(memory)) {
		util::LogFatal("failed to resolve module globals by pattern");
		return false;
	}

	ResolveBuildInfo(memory);

	if (!LoadOrDumpSchema(memory)) {
		return false;
	}

	if (!m_offsets.IsComplete()) {
		util::LogFatal("essential offsets are still unresolved");
		return false;
	}

	return true;
}

const GameOffsets& OffsetsManager::Get() const
{
	return m_offsets;
}

std::string OffsetsManager::GetBuildDatetime() const
{
	return m_buildDatetime;
}

bool OffsetsManager::ResolveGlobalsBySignature(const ProcessMemory& memory)
{
	bool entityListOk = false;
	for (const PatternDef& def : kEntityListPatterns) {
		const uintptr_t offset = ResolveRelativeSlot(memory, def);
		if (offset != 0) {
			m_offsets.entityList = offset;
			entityListOk = true;
			break;
		}
	}
	if (!entityListOk) {
		util::LogError("could not resolve dwEntityList (both patterns failed)");
		return false;
	}

	m_offsets.localPlayerPawn = 0;
	for (const PatternDef& def : kLocalControllerPatterns) {
		const uintptr_t offset = ResolveRelativeSlot(memory, def);
		if (offset != 0) {
			m_offsets.localPlayerPawn = offset;
			break;
		}
	}
	if (m_offsets.localPlayerPawn == 0) {
		m_offsets.localPlayerPawn = ResolveChained(memory, kPredictionPattern, kLocalPawnChain);
	}
	if (m_offsets.localPlayerPawn == 0) {
		m_offsets.localPlayerPawn = ResolveRelativeSlot(memory, kLocalPawnPattern);
		if (m_offsets.localPlayerPawn != 0) {
			util::LogWarning("dwLocalPlayerPawn resolved via the classic pattern; verify the value (it can be stale on recent builds)");
		}
	}
	if (m_offsets.localPlayerPawn == 0) {
		util::LogError("could not resolve the local player (controller / pawn)");
		return false;
	}

	m_offsets.viewAngles = ResolveChained(memory, kCsgoInputPattern, kViewAnglesChain);
	if (m_offsets.viewAngles == 0) {
		util::LogError("could not resolve dwViewAngles");
		return false;
	}

	m_offsets.viewMatrix = ResolveRelativeSlot(memory, kViewMatrixPattern);
	if (m_offsets.viewMatrix == 0) {
		util::LogError("could not resolve dwViewMatrix");
		return false;
	}

	m_offsets.globalVars = ResolveRelativeSlot(memory, kGlobalVarsPattern);

	m_schemaSystemOffset = ResolveRelativeSlot(memory, kSchemaSystemPattern);
	if (m_schemaSystemOffset == 0) {
		util::LogError("could not resolve the schema system instance");
		return false;
	}
	m_buildInfoOffset = ResolveRelativeSlot(memory, kBuildInfoPattern);
	if (m_buildInfoOffset == 0) {
		util::LogWarning("could not resolve the build info global (menu shows no build)");
	}

	m_engineBase = memory.GetModule("engine2.dll").base;
	m_offsets.networkGameClient = ResolveRelativeSlot(memory, kNetworkGameClientPattern);
	if (m_offsets.networkGameClient == 0) {
		util::LogWarning("could not resolve the network game client instance (web radar map name)");
	}

	return true;
}

bool OffsetsManager::ResolveBuildInfo(const ProcessMemory& memory)
{
	if (m_buildInfoOffset == 0) {
		return false;
	}

	const ModuleInfo engine = memory.GetModule("engine2.dll");
	if (engine.base == 0) {
		return false;
	}

	if (!ReadBuildInfo(memory, engine.base, m_buildInfoOffset, m_buildDatetime)) {
		m_buildDatetime.clear();
		util::LogWarning("could not read the engine build info struct");
		return false;
	}

	return true;
}

bool OffsetsManager::LoadOrDumpSchema(const ProcessMemory& memory)
{
	const ModuleInfo schemasystem = memory.GetModule("schemasystem.dll");
	if (schemasystem.base == 0) {
		util::LogError("schemasystem.dll not found");
		return false;
	}
	const uintptr_t schemaSystemAddress = schemasystem.base + m_schemaSystemOffset;

	std::vector<SchemaOffsetEntry> offsets;
	if (!DumpSchemaOffsets(memory, schemaSystemAddress, offsets)) {
		util::LogFatal("schema dump failed (game memory paged out? restart the game)");
		return false;
	}

	ApplySchemaOffsets(offsets);

	FillIfZero(m_offsets.boneArray, kBoneArrayOffset);
	return true;
}

uintptr_t OffsetsManager::GetEngineBase() const
{
	return m_engineBase;
}

void OffsetsManager::ApplySchemaOffsets(const std::vector<SchemaOffsetEntry>& offsets)
{
	std::map<std::pair<std::string, std::string>, uint64_t> lookup;
	auto insertIfAbsent = [&lookup](const SchemaOffsetEntry& entry) {
		const auto key = std::make_pair(entry.className, entry.member);
		if (lookup.find(key) == lookup.end()) {
			lookup[key] = entry.value;
		}
	};
	for (const SchemaOffsetEntry& entry : offsets) {
		if (entry.module == "client.dll") {
			insertIfAbsent(entry);
		}
	}
	for (const SchemaOffsetEntry& entry : offsets) {
		if (entry.module != "client.dll") {
			insertIfAbsent(entry);
		}
	}

	auto lookupField = [&lookup](const char* className, const char* member, uintptr_t& dst) {
		const auto it = lookup.find(std::make_pair(std::string(className), std::string(member)));
		if (it != lookup.end()) {
			dst = it->second;
		}
	};

	auto lookupFieldAny = [&lookup](const char* member, uintptr_t& dst,
		std::initializer_list<const char*> classes) {
		for (const char* className : classes) {
			const auto it = lookup.find(std::make_pair(std::string(className), std::string(member)));
			if (it != lookup.end()) {
				dst = it->second;
				return;
			}
		}
	};

	lookupField("CCSPlayerController", "m_hPlayerPawn", m_offsets.hPlayerPawn);
	lookupFieldAny("m_iszPlayerName", m_offsets.iszPlayerName,
		{ "CCSPlayerController", "CBasePlayerController" });
	lookupField("C_CSPlayerPawn", "m_iIDEntIndex", m_offsets.idEntIndex);
	lookupField("C_CSPlayerPawn", "m_iShotsFired", m_offsets.iShotsFired);
	lookupField("C_CSPlayerPawnBase", "m_aimPunchAngle", m_offsets.aimPunchAngle);
	lookupField("C_CSPlayerPawnBase", "m_aimPunchCache", m_offsets.aimPunchCache);
	lookupField("C_CSPlayerPawn", "m_pAimPunchServices", m_offsets.aimPunchServices);
	lookupField("CCSPlayer_AimPunchServices", "m_unpredictableBaseTick", m_offsets.unpredictableBaseTick);
	lookupField("C_BaseEntity", "m_iTeamNum", m_offsets.teamNum);
	lookupField("C_BaseEntity", "m_iHealth", m_offsets.health);
	lookupField("C_BaseEntity", "m_lifeState", m_offsets.lifeState);
	lookupField("C_BaseEntity", "m_pGameSceneNode", m_offsets.gameSceneNode);
	lookupField("C_BaseEntity", "m_vecVelocity", m_offsets.vecVelocity);
	lookupField("C_CSPlayerPawn", "m_angEyeAngles", m_offsets.eyeAngles);
	lookupField("C_CSPlayerPawn", "m_entitySpottedState", m_offsets.entitySpottedState);
	lookupField("EntitySpottedState_t", "m_bSpotted", m_offsets.spotted);
	lookupField("C_BasePlayerPawn", "m_vOldOrigin", m_offsets.oldOrigin);
	lookupFieldAny("m_vecAbsOrigin", m_offsets.vecAbsOrigin,
		{ "CGameSceneNode", "CSkeletonInstance" });
	lookupField("C_BaseModelEntity", "m_vecViewOffset", m_offsets.vecViewOffset);
	lookupField("CSkeletonInstance", "m_modelState", m_offsets.modelState);
	lookupField("CModelState", "m_hModel", m_offsets.hModel);
	lookupField("C_BaseModelEntity", "m_Glow", m_offsets.glow);
	lookupField("CGlowProperty", "m_glowColorOverride", m_offsets.glowColorOverride);
	lookupField("CGlowProperty", "m_bFlashing", m_offsets.glowFlashing);
	lookupField("CGlowProperty", "m_flGlowTime", m_offsets.glowTime);
	lookupField("CGlowProperty", "m_flGlowStartTime", m_offsets.glowStartTime);
	lookupField("CGlowProperty", "m_bGlowing", m_offsets.glowGlowing);
	lookupField("CGlowProperty", "m_iGlowType", m_offsets.glowType);
	lookupField("C_CSPlayerPawnBase", "m_flFlashMaxAlpha", m_offsets.flashMaxAlpha);
	lookupField("C_CSPlayerPawnBase", "m_flFlashDuration", m_offsets.flashDuration);
	lookupFieldAny("m_bIsDefusing", m_offsets.isDefusing,
		{ "C_CSPlayerPawn", "C_CSPlayerPawnBase", "CCSPlayerPawnBase" });

	lookupField("C_C4", "m_entitySpottedState", m_offsets.c4SpottedState);
	lookupField("C_PlantedC4", "m_entitySpottedState", m_offsets.plantedC4SpottedState);
	lookupField("C_BaseEntity", "m_hOwnerEntity", m_offsets.hOwnerEntity);
	lookupField("C_PlantedC4", "m_nBombSite", m_offsets.plantedC4BombSite);
	lookupField("C_PlantedC4", "m_flC4Blow", m_offsets.plantedC4Blow);
	lookupField("C_PlantedC4", "m_flTimerLength", m_offsets.plantedC4TimerLength);
	lookupField("C_PlantedC4", "m_bBombDefused", m_offsets.plantedC4BombDefused);
	lookupField("C_PlantedC4", "m_bHasExploded", m_offsets.plantedC4HasExploded);
	lookupField("CNetworkGameClient", "m_serverTickCount", m_offsets.serverTickCount);
	if (m_offsets.serverTickCount == 0) {
		m_offsets.serverTickCount = 0x24C;
		util::LogWarning("CNetworkGameClient::m_serverTickCount not in the dump; using the bundled 0x24C fallback");
	}

	lookupField("CNetworkGameClient", "m_szMapName", m_offsets.mapName);
	if (m_offsets.mapName == 0) {
		m_offsets.mapName = 0x218;
		util::LogWarning("CNetworkGameClient::m_szMapName not found in the dump; using the bundled 0x218 fallback");
	}
}

bool OffsetsManager::ResolveEntityListBySignature(const ProcessMemory& memory)
{
	for (const PatternDef& def : kEntityListPatterns) {
		const uintptr_t offset = ResolveRelativeSlot(memory, def);
		if (offset != 0) {
			m_offsets.entityList = offset;
			return true;
		}
	}
	return false;
}

} // namespace core
