// -----------------------------------------------------------------------------
// aimbot.cpp
// -----------------------------------------------------------------------------
// Implementation of the aimbot (ported from the CS:GO version of the cheat).
// Behavior matches the original:
//   - The target is the enemy under the crosshair (m_iIDEntIndex). Once a
//     lock is acquired it stays on that enemy even if the crosshair drifts
//     off, until the hold key is released.
//   - Hold MOUSE1 while spraying (m_iShotsFired > 1): recoil-controlled
//     aim lock while firing.
//   - Hold MOUSE3: hard aim lock on the crosshair enemy.
//   - The aim bone comes from the selected mode (Head / Chest / Stomach /
//     Random via DELETE or the NUMPAD keys; Off disables the lock).
//   - Recoil punch (m_aimPunchAngle) is subtracted from the aim angle, like
//     the original's calcang (punch * 2).
//   - Smoothing moves the view by smoothAmount/100 of the remaining delta
//     each pass (100 = snap, same semantics as the original cheat).
// -----------------------------------------------------------------------------

#include "aimbot.h"
#include "aimhelper.h"

#include <windows.h>
#include <cstdlib>
#include <ctime>
#include <sstream>

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

namespace features {

namespace {

// Returns a random aim bone for the "Random" mode (head, chest, stomach or
// spine_2 mapped to chest), picked once per lock pass like the original
// cheat's randint(4, 8).
int PickRandomBone()
{
	static bool seeded = false;
	if (!seeded) {
		srand(static_cast<unsigned>(time(nullptr)));
		seeded = true;
	}

	const int pick = rand() % 4;
	switch (pick) {
		case 0: return cfg::g_config.boneHead;
		case 1: return cfg::g_config.boneChest;
		case 2: return cfg::g_config.boneStomach;
		case 3: return cfg::g_config.boneChest;
		default: return cfg::g_config.boneHead;
	}
}

// ---------------------------------------------------------------------------
// Recoil punch cache layout constants (not schema offsets; see ReadPunch).
//
// The punch vector ("CUtlVector") layout is Count(+0, u32) then Data(+8,
// ptr) - NOT the classic Data/Count order - and every record is a 12-byte
// QAngle; the LAST record holds the punch of the most recent shot. The
// count sanity bound rejects garbage reads (a valid count never approaches
// 0xFFFF).
// ---------------------------------------------------------------------------
constexpr uintptr_t kPunchDataMember = 0x8;    // Data pointer member offset
constexpr uintptr_t kPunchRecordSize = 12;     // QAngle: 3 floats
constexpr uint32_t kPunchCountMax = 0xFFFF;    // count sanity bound

// Valve removed m_aimPunchCache from the schema (it moved into
// CCSPlayer_AimPunchServices), so the cache location can no longer be
// resolved from the schema dump. The candidates below are community-verified
// offsets for the cache relative to the services object, differing per
// build: a2x's dump used m_unpredictableBaseTick - 0x18, an April 2026
// report used m_unpredictableBaseTick + 0x38, another used a plain 0x88.
constexpr uintptr_t kPunchRelBaseTickMinus = 0x18;
constexpr uintptr_t kPunchRelBaseTickPlus = 0x38;
constexpr uintptr_t kPunchRelServices = 0x88;

} // namespace

void Aimbot::Initialize(const core::ProcessMemory* memory, const core::GameOffsets* offsets,
	const sdk::EntityList* entities, const sdk::LocalPlayer* player)
{
	m_memory = memory;
	m_offsets = offsets;
	m_entities = entities;
	m_player = player;
}

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

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

	while (true) {
		if (!cfg::g_config.aimbotEnabled) {
			Sleep(500);
			continue;
		}

		self->LockWhileHeld(cfg::g_config.aimbotFireKey, true);
		self->LockWhileHeld(cfg::g_config.aimbotLockKey, false);

		Sleep(5);
	}
	return 0;
}

void Aimbot::LockWhileHeld(int virtualKey, bool requireShotsFired)
{
	const uintptr_t meBase = m_player->GetLocalPawn();
	if (meBase == 0) {
		return;
	}
	const int myTeam = m_player->GetTeam();

	int lockedSlot = -1;
	while (util::IsKeyDown(virtualKey)) {
		const int crosshairId = m_player->GetCrosshairId();
		if (crosshairId >= 0 && crosshairId < sdk::kMaxRawEntities) {
			const uintptr_t pawn = m_entities->GetPawn(crosshairId);
			if (pawn != 0 && pawn != meBase && m_entities->IsValidPawn(pawn, myTeam)) {
				lockedSlot = crosshairId;
			}
		}
		if (lockedSlot < 0) {
			Sleep(5);
			continue;
		}

		const uintptr_t lockedPawn = m_entities->GetPawn(lockedSlot);
		if (lockedPawn == 0 || lockedPawn == meBase || !m_entities->IsValidPawn(lockedPawn, myTeam)) {
			lockedSlot = -1;
			Sleep(5);
			continue;
		}

		if (requireShotsFired && m_player->GetShotsFired() <= 1) {
			Sleep(5);
			continue;
		}

		AimLock(lockedSlot);
		Sleep(5);
	}
}

void Aimbot::AimLock(int slot)
{
	const uintptr_t meBase = m_player->GetLocalPawn();
	if (meBase == 0) {
		return;
	}

	if (cfg::g_config.aimMode == cfg::AimMode::Off) {
		return;
	}

	int aimBone = cfg::g_config.GetAimBone();
	if (cfg::g_config.aimMode == cfg::AimMode::Random) {
		aimBone = PickRandomBone();
	}
	if (aimBone < 0) {
		aimBone = cfg::g_config.boneHead;
	}
	if (aimBone < 0) {
		return;
	}

	sdk::Vec3 myPos;
	if (!m_player->GetPosition(myPos)) {
		return;
	}

	sdk::Vec3 targetPos;
	bool usedFallback = false;
	if (!m_entities->GetBonePosition(slot, aimBone, targetPos)) {
		if (!m_entities->GetOrigin(slot, targetPos)) {
			return;
		}
		targetPos.z += aim::FallbackBoneHeight(
			aimBone, cfg::g_config.boneHead, cfg::g_config.boneStomach);
		usedFallback = true;
	}

	if (!usedFallback) {
		sdk::Vec3 enemyOrigin;
		if (m_entities->GetOrigin(slot, enemyOrigin)) {
			const float dz = targetPos.z - enemyOrigin.z;
			const float dx = std::fabs(targetPos.x - enemyOrigin.x);
			const float dy = std::fabs(targetPos.y - enemyOrigin.y);
			if (dz < -20.0f || dz > 150.0f || dx > 1000.0f || dy > 1000.0f) {
				targetPos = enemyOrigin;
				targetPos.z += aim::FallbackBoneHeight(
					aimBone, cfg::g_config.boneHead, cfg::g_config.boneStomach);
				usedFallback = true;
			}
		}
	}

	const uintptr_t enemyPawn = m_entities->GetPawn(slot);
	sdk::Vec3 enemyVelocity;
	sdk::Vec3 myVelocity;
	if (enemyPawn != 0) {
		m_memory->ReadArray<float>(enemyPawn + m_offsets->vecVelocity, &enemyVelocity.x, 3);
	}
	m_memory->ReadArray<float>(meBase + m_offsets->vecVelocity, &myVelocity.x, 3);
	aim::ApplyVelocityCompensation(targetPos, enemyVelocity, myVelocity, cfg::g_config.smoothAmount);

	sdk::Vec3 aimAngle = aim::CalculateAngle(myPos, targetPos, m_player->GetViewOrigin());

	const float punchScale = cfg::g_config.aimbotPunchScale;
	sdk::Vec3 punch{};
	const int punchTier = ReadPunch(meBase, punch);
	if (punchTier == 5) {
		aimAngle.x += punch.x * punchScale;
		aimAngle.y += punch.y * punchScale;
	} else if (punchTier != 0) {
		aimAngle.x -= punch.x * punchScale;
		aimAngle.y -= punch.y * punchScale;
	}
	aim::Normalize(aimAngle);

	const float factor = static_cast<float>(cfg::g_config.smoothAmount) / 100.0f;

	sdk::Vec3 currentAngles;
	if (!m_player->GetAngles(currentAngles)) {
		return;
	}
	const sdk::Vec3 finalAngle = aim::SmoothedStep(currentAngles, aimAngle, factor);
	m_player->SetAngles(finalAngle);

	{
		std::stringstream ss;
		ss << "[aimbot] lock slot=" << slot << " bone=" << aimBone
			<< (usedFallback ? " fb=1" : "")
			<< " smooth=" << cfg::g_config.smoothAmount
			<< " punch=(" << punch.x << ", " << punch.y << ")@tier" << punchTier
			<< " eye=(" << myPos.x << ", " << myPos.y << ", " << (myPos.z + m_player->GetViewOrigin()) << ")"
			<< " tgt=(" << targetPos.x << ", " << targetPos.y << ", " << targetPos.z << ")"
			<< " aim=(" << aimAngle.x << ", " << aimAngle.y << ")"
			<< " cur=(" << currentAngles.x << ", " << currentAngles.y << ")"
			<< " final=(" << finalAngle.x << ", " << finalAngle.y << ")";
		util::LogDebug(ss.str());
	}
}

int Aimbot::ReadPunch(uintptr_t meBase, sdk::Vec3& outPunch)
{
	if (m_offsets->aimPunchCache != 0) {
		const uint32_t cacheCount = m_memory->Read<uint32_t>(meBase + m_offsets->aimPunchCache);
		const uintptr_t cacheData = m_memory->Read<uintptr_t>(meBase + m_offsets->aimPunchCache + kPunchDataMember);
		if (cacheData != 0 && cacheCount > 0 && cacheCount < kPunchCountMax) {
			m_memory->ReadArray<float>(
				cacheData + static_cast<uintptr_t>(cacheCount - 1) * kPunchRecordSize, &outPunch.x, 2);
			return 1;
		}
	}

	if (m_offsets->aimPunchServices != 0) {
		const uintptr_t services = m_memory->Read<uintptr_t>(meBase + m_offsets->aimPunchServices);
		if (services != 0) {
			const uintptr_t candidates[3] = {
				(m_offsets->unpredictableBaseTick != 0) ? services + m_offsets->unpredictableBaseTick - kPunchRelBaseTickMinus : 0,
				(m_offsets->unpredictableBaseTick != 0) ? services + m_offsets->unpredictableBaseTick + kPunchRelBaseTickPlus : 0,
				services + kPunchRelServices,
			};
			for (int i = 0; i < 3; i++) {
				const uintptr_t cache = candidates[i];
				if (cache == 0) {
					continue;
				}
				const uint32_t cacheCount = m_memory->Read<uint32_t>(cache);
				const uintptr_t cacheData = m_memory->Read<uintptr_t>(cache + kPunchDataMember);
				if (cacheData != 0 && cacheCount > 0 && cacheCount < kPunchCountMax) {
					m_memory->ReadArray<float>(
						cacheData + static_cast<uintptr_t>(cacheCount - 1) * kPunchRecordSize, &outPunch.x, 2);
					return 2 + i;
				}
			}
		}
	}

	if (m_offsets->aimPunchAngle != 0) {
		m_memory->ReadArray<float>(meBase + m_offsets->aimPunchAngle, &outPunch.x, 2);
		return 5;
	}

	return 0;
}

} // namespace features