// -----------------------------------------------------------------------------
// main.cpp
// -----------------------------------------------------------------------------
// Entry point of CS2 Muhittin. Attaches to cs2.exe (waiting for it if
// needed and re-attaching after restarts/crashes), resolves all game offsets
// locally (byte patterns + live schema dump, no network), resolves the
// aim bone indices from the local player's model, starts every feature
// thread and runs the hotkey listener + game monitor that drive the on-screen
// menu and keep the cheat working across matches without a restart.
//
// Hotkeys (defaults, all adjustable in config.h):
//   HOME        -> cycle wallhack glow level (All -> Enemy -> Team -> Off)
//   END         -> toggle minimap radar
//   PAGE DOWN   -> toggle no-flash
//   DELETE      -> cycle aim mode (Chest -> Stomach -> Off -> Head -> Random)
//   NUMPAD 0/2/4/7/5 -> aim mode Off/Stomach/Chest/Head/Random
//   PAGE UP     -> toggle web radar
//   INSERT      -> toggle aimbot (M1-hold spray lock + MMB-hold hard lock)
//   MMB         -> triggerbot (auto fire)
//   ALT         -> force aim (nearest enemy)
//   CAPS LOCK   -> flickshot (snap to aim bone + fire one shot)
// -----------------------------------------------------------------------------

#include <windows.h>
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <sstream>
#include <vector>

#include "config.h"
#include "core/memory.h"
#include "core/offsets.h"
#include "sdk/entitylist.h"
#include "sdk/localplayer.h"
#include "util/console.h"
#include "util/input.h"
#include "util/logger.h"

#include "features/aimbot.h"
#include "features/flickshot.h"
#include "features/forceaim.h"
#include "features/noflash.h"
#include "features/radar.h"
#include "features/triggerbot.h"
#include "features/wallhack.h"
#include "features/webradar.h"

// Namespace shortcuts for readability.
using core::GameOffsets;
using core::OffsetsManager;
using core::ProcessMemory;
using sdk::EntityList;
using sdk::LocalPlayer;

namespace {

// Shared memory reader and offset manager (single instances).
ProcessMemory g_memory;
OffsetsManager g_offsetsManager;

// Game readers bound after attach.
EntityList g_entities;
LocalPlayer g_localPlayer;
uintptr_t g_clientBase = 0;

// Feature instances.
features::Aimbot g_aimbot;
features::Flickshot g_flickshot;
features::ForceAim g_forceAim;
features::Triggerbot g_triggerbot;
features::NoFlash g_noFlash;
features::Wallhack g_wallhack;
features::Radar g_radar;
features::WebRadar g_webRadar;

// Target of FindGameWindowProc (the EnumWindows callback).
HWND g_foundGameWindow = nullptr;

// The exe is a GUI-subsystem app (no console at launch), so Windows never
// hands it to Windows Terminal. AllocConsole() creates a classic conhost
// window regardless of the machine's default terminal setting, which keeps
// the window sizing APIs (MoveWindow / screen buffer resize) working on
// every Windows 10/11 machine. The standard streams are redirected to the
// new console so std::cout / std::cerr keep working.
void CreateConsole()
{
	if (!AllocConsole()) {
		return;
	}
	FILE* stream = nullptr;
	freopen_s(&stream, "CONOUT$", "w", stdout);
	freopen_s(&stream, "CONOUT$", "w", stderr);
	freopen_s(&stream, "CONIN$", "r", stdin);
}

// Prints one menu line with the original cheat's coloring:
// green when enabled, red when disabled (red already means off, so no
// ON/OFF suffix is printed).
void PrintToggleLine(const char* hotkey, const char* label, bool enabled)
{
	const char* color = enabled ? "green" : "red";
	util::PrintText(color, hotkey);
	util::PrintText(color, " ");
	util::PrintText(color, label);
}

// Converts the engine's build datetime to the menu format
// "2026.12.31 23:59" (numeric month, no seconds), shown in the local time
// of the machine the cheat runs on. The engine string may start with a
// weekday ("Thu Sep 12 2019 18:39:17") or not; the month token is scanned
// for and the following day, year and time are used. The string is the
// build machine's local time, treated as US Pacific (Valve build machines)
// with its DST transitions (UTC-8 in winter, UTC-7 in summer); it is
// shifted to the machine's own timezone before formatting. Returns the
// input untouched when the layout does not match.
std::string FormatBuildDatetime(const std::string& datetime)
{
	static const char* kMonths[] = {
		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
	};

	std::istringstream in(datetime);
	std::vector<std::string> tokens;
	std::string token;
	while (in >> token) {
		tokens.push_back(token);
	}

	int month = 0;
	size_t monthIndex = 0;
	for (size_t i = 0; i < tokens.size() && month == 0; i++) {
		for (int m = 0; m < 12; m++) {
			if (tokens[i] == kMonths[m]) {
				month = m + 1;
				monthIndex = i;
				break;
			}
		}
	}
	if (month == 0 || monthIndex + 3 >= tokens.size()) {
		return datetime;
	}

	const int day = std::atoi(tokens[monthIndex + 1].c_str());
	const int year = std::atoi(tokens[monthIndex + 2].c_str());
	const std::string& time = tokens[monthIndex + 3];
	if (day <= 0 || day > 31 || year < 2000 || year > 2100 || time.size() < 5) {
		return datetime;
	}

	SYSTEMTIME pacificTime = {};
	pacificTime.wYear = static_cast<WORD>(year);
	pacificTime.wMonth = static_cast<WORD>(month);
	pacificTime.wDay = static_cast<WORD>(day);
	pacificTime.wHour = static_cast<WORD>(std::atoi(time.substr(0, 2).c_str()));
	pacificTime.wMinute = static_cast<WORD>(std::atoi(time.substr(3, 2).c_str()));
	pacificTime.wSecond = (time.size() > 6)
		? static_cast<WORD>(std::atoi(time.substr(6, 2).c_str())) : 0;

	TIME_ZONE_INFORMATION pacificZone = {};
	pacificZone.Bias = 480;
	pacificZone.StandardBias = 0;
	pacificZone.DaylightBias = -60;
	pacificZone.StandardDate.wMonth = 11;
	pacificZone.StandardDate.wDayOfWeek = 0;
	pacificZone.StandardDate.wDay = 1;
	pacificZone.StandardDate.wHour = 2;
	pacificZone.DaylightDate.wMonth = 3;
	pacificZone.DaylightDate.wDayOfWeek = 0;
	pacificZone.DaylightDate.wDay = 2;
	pacificZone.DaylightDate.wHour = 2;

	SYSTEMTIME utcTime = {};
	if (!TzSpecificLocalTimeToSystemTime(&pacificZone, &pacificTime, &utcTime)) {
		return datetime;
	}

	SYSTEMTIME localTime = {};
	if (!SystemTimeToTzSpecificLocalTime(NULL, &utcTime, &localTime)) {
		return datetime;
	}

	std::stringstream out;
	out << localTime.wYear << "." << (localTime.wMonth < 10 ? "0" : "")
		<< localTime.wMonth << "." << (localTime.wDay < 10 ? "0" : "")
		<< localTime.wDay << " " << (localTime.wHour < 10 ? "0" : "")
		<< localTime.wHour << ":" << (localTime.wMinute < 10 ? "0" : "")
		<< localTime.wMinute;
	return out.str();
}

// Redraws the complete on-screen menu (layout identical to the original).
void PrintSettings()
{
	system("cls");
	util::PrintText("aqua", std::string(cfg::kAppName) + " v" + cfg::kVersion);

	if (!g_offsetsManager.GetBuildDatetime().empty()) {
		util::PrintText("aqua", " - " + FormatBuildDatetime(g_offsetsManager.GetBuildDatetime()));
	}
	util::PrintText("aqua", u8"\n12游戏吧永久地址发布页：www.12gameb.co");
	util::PrintText("red", "\n");
	util::PrintText("red", "\n");

	const char* glowColor = cfg::g_config.glowLevel == cfg::GlowLevel::Off ? "red" : "green";
	util::PrintText(glowColor, u8"[HOME] 透视 - ");
	util::PrintText(glowColor, cfg::g_config.GetGlowLevelName());
	util::PrintText("red", "\n");

	PrintToggleLine("[END]", u8"雷达", cfg::g_config.radarEnabled);
	util::PrintText("red", "\n");

	PrintToggleLine("[INSERT]", u8"自动瞄准", cfg::g_config.aimbotEnabled);
	util::PrintText("red", "\n");

	std::string aimLine = u8"[DELETE] [NUMPAD 0 2 4 7 5] 瞄准部位 - ";
	aimLine += cfg::g_config.GetAimModeName();
	if (cfg::g_config.aimMode == cfg::AimMode::Off) {
		util::PrintText("red", aimLine.c_str());
	} else {
		util::PrintText("green", aimLine.c_str());
	}
	util::PrintText("red", "\n");

	PrintToggleLine("[PAGE DOWN]", u8"防闪", cfg::g_config.noFlashEnabled);
	util::PrintText("red", "\n");

	PrintToggleLine("[PAGE UP]", u8"网页雷达", cfg::g_config.webRadarEnabled);
	if (cfg::g_config.webRadarEnabled) {
		const std::string url = g_webRadar.GetSessionUrl();
		if (!url.empty()) {
			util::PrintText("red", "\n");
			util::PrintText("default", "  ");
			util::PrintText("aqua", url.c_str());
		}
	}
	util::PrintText("red", "\n");

	util::PrintText("blue", u8"[MOUSE3 - 按住] 自动扳机");
	util::PrintText("red", "\n");

	util::PrintText("blue", u8"[ALT - 按住] 强制瞄准");
	util::PrintText("red", "\n");

	util::PrintText("blue", u8"[CAPS LOCK] 瞬狙");

	util::PrintText("default", "\n");
}

// Cycles the aim mode in the original cheat's order:
// Chest -> Stomach -> Off -> Head -> Random (wraps around).
void CycleAimMode()
{
	switch (cfg::g_config.aimMode) {
		case cfg::AimMode::Head:    cfg::g_config.aimMode = cfg::AimMode::Chest; break;
		case cfg::AimMode::Chest:   cfg::g_config.aimMode = cfg::AimMode::Stomach; break;
		case cfg::AimMode::Stomach: cfg::g_config.aimMode = cfg::AimMode::Off; break;
		case cfg::AimMode::Off:     cfg::g_config.aimMode = cfg::AimMode::Head; break;
		case cfg::AimMode::Random:  cfg::g_config.aimMode = cfg::AimMode::Head; break;
	}
}

// Cycles the wallhack glow level: All -> Enemy -> Team -> Off (wraps).
void CycleGlowLevel()
{
	switch (cfg::g_config.glowLevel) {
		case cfg::GlowLevel::All:   cfg::g_config.glowLevel = cfg::GlowLevel::Enemy; break;
		case cfg::GlowLevel::Enemy: cfg::g_config.glowLevel = cfg::GlowLevel::Team; break;
		case cfg::GlowLevel::Team:  cfg::g_config.glowLevel = cfg::GlowLevel::Off; break;
		case cfg::GlowLevel::Off:   cfg::g_config.glowLevel = cfg::GlowLevel::All; break;
	}
}

// EnumWindows callback: finds the first visible top-level window of the
// process whose PID is passed as lparam.
BOOL CALLBACK FindGameWindowProc(HWND hwnd, LPARAM lparam)
{
	DWORD windowPid = 0;
	GetWindowThreadProcessId(hwnd, &windowPid);
	if (windowPid == static_cast<DWORD>(lparam) && IsWindowVisible(hwnd)) {
		g_foundGameWindow = hwnd;
		return FALSE;
	}
	return TRUE;
}

// Returns the game's main window (0 when not found).
HWND FindGameWindow()
{
	g_foundGameWindow = nullptr;
	const DWORD pid = g_memory.GetPid();
	if (pid != 0) {
		EnumWindows(FindGameWindowProc, static_cast<LPARAM>(pid));
	}
	return g_foundGameWindow;
}

// Brings the cheat console to the front after a hotkey was handled, but
// only when the game is NOT the foreground window: while the desktop (or
// any other app) is focused, the console is usually hidden behind the game
// window, so the menu would update invisibly. Never steals focus from the
// game itself.
void BringMenuToFrontIfHidden()
{
	const HWND console = GetConsoleWindow();
	if (GetForegroundWindow() == console) {
		return;
	}

	const HWND game = FindGameWindow();
	if (game != nullptr && GetForegroundWindow() == game) {
		return;
	}

	SetWindowPos(console, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW);
	SetWindowPos(console, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
	SetForegroundWindow(console);
}

// Handles every hotkey: toggles, aim mode selection and value adjustment.
// Returns true when any hotkey was handled (the caller may then make the
// menu visible again).
bool HandleHotkeys()
{
	const cfg::Config& config = cfg::g_config;

	if (util::WasKeyPressed(config.glowToggleKey)) {
		CycleGlowLevel();
		PrintSettings();
		return true;
	}
	if (util::WasKeyPressed(config.radarToggleKey)) {
		cfg::g_config.radarEnabled = !cfg::g_config.radarEnabled;
		PrintSettings();
		return true;
	}
	if (util::WasKeyPressed(config.noFlashToggleKey)) {
		cfg::g_config.noFlashEnabled = !cfg::g_config.noFlashEnabled;
		PrintSettings();
		return true;
	}
	if (util::WasKeyPressed(config.webRadarToggleKey)) {
		cfg::g_config.webRadarEnabled = !cfg::g_config.webRadarEnabled;
		PrintSettings();
		return true;
	}
	if (util::WasKeyPressed(config.aimbotToggleKey)) {
		cfg::g_config.aimbotEnabled = !cfg::g_config.aimbotEnabled;
		PrintSettings();
		return true;
	}

	if (util::WasKeyPressed(config.aimModeKey)) {
		CycleAimMode();
		PrintSettings();
		return true;
	}
	if (util::WasKeyPressed(config.aimOffKey)) {
		cfg::g_config.aimMode = cfg::AimMode::Off;
		PrintSettings();
		return true;
	}
	if (util::WasKeyPressed(config.aimStomachKey)) {
		cfg::g_config.aimMode = cfg::AimMode::Stomach;
		PrintSettings();
		return true;
	}
	if (util::WasKeyPressed(config.aimChestKey)) {
		cfg::g_config.aimMode = cfg::AimMode::Chest;
		PrintSettings();
		return true;
	}
	if (util::WasKeyPressed(config.aimHeadKey)) {
		cfg::g_config.aimMode = cfg::AimMode::Head;
		PrintSettings();
		return true;
	}
	if (util::WasKeyPressed(config.aimRandomKey)) {
		cfg::g_config.aimMode = cfg::AimMode::Random;
		PrintSettings();
		return true;
	}

	return false;
}

// Resolves the aim bone indices from a player model. Called once at startup
// and retried from the main loop while unresolved: when the cheat starts
// before a match the local pawn does not exist yet and force aim / flickshot
// would stay dead forever (GetAimBone() returns -1 then). All player models
// share the same skeleton layout, so resolving from any valid pawn (the
// local one first, any other player as fallback) is equivalent and makes
// the bones ready as soon as the cheat is running.
void ResolveAimBones()
{
	uintptr_t pawn = g_localPlayer.GetLocalPawn();
	if (pawn == 0) {
		for (int i = 0; i < sdk::kMaxPlayers; i++) {
			const uintptr_t candidate = g_entities.GetPawn(i);
			if (candidate != 0 && g_entities.IsValidPawn(candidate, 0)) {
				pawn = candidate;
				break;
			}
		}
	}
	if (pawn == 0) {
		return;
	}

	cfg::g_config.boneHead = g_entities.FindBoneByName(pawn, "head_0");
	cfg::g_config.boneChest = g_entities.FindBoneByName(pawn, "spine_3");
	cfg::g_config.boneStomach = g_entities.FindBoneByName(pawn, "spine_1");

	util::LogDebug("aim bones resolved (head=" + std::to_string(cfg::g_config.boneHead) +
		", chest=" + std::to_string(cfg::g_config.boneChest) +
		", stomach=" + std::to_string(cfg::g_config.boneStomach) + ")");
}

// Binds all readers and feature instances to the shared state.
void InitializeReaders()
{
	const GameOffsets& offsets = g_offsetsManager.Get();
	g_entities.Initialize(&g_memory, &offsets, g_clientBase);
	g_localPlayer.Initialize(&g_memory, &offsets, g_clientBase);

	g_aimbot.Initialize(&g_memory, &offsets, &g_entities, &g_localPlayer);
	g_flickshot.Initialize(&g_memory, &offsets, &g_entities, &g_localPlayer);
	g_forceAim.Initialize(&g_memory, &offsets, &g_entities, &g_localPlayer);
	g_triggerbot.Initialize(&g_memory, &offsets, &g_entities, &g_localPlayer);
	g_noFlash.Initialize(&g_memory, &offsets, &g_localPlayer);
	g_wallhack.Initialize(&g_memory, &offsets, &g_entities, &g_localPlayer);
	g_radar.Initialize(&g_memory, &offsets, &g_entities, &g_localPlayer);
	g_webRadar.Initialize(&g_memory, &offsets, &g_entities, &g_localPlayer,
		g_clientBase, g_offsetsManager.GetEngineBase());
}

// Binds the game readers to the current process/module state. Called at
// startup and again after the game restarts: client.dll can be loaded at a
// new ASLR base then, so the module base and the entity-list pointer are
// re-resolved and every reader is re-bound. Aim bones are reset so they are
// lazily re-resolved for the new match (see the main loop).
bool BindToGame()
{
	const core::ModuleInfo client = g_memory.GetModule("client.dll");
	if (client.base == 0) {
		return false;
	}
	g_clientBase = client.base;

	if (!g_offsetsManager.ResolveEntityListBySignature(g_memory)) {
		util::LogWarning("entity list signature re-resolution failed, retrying");
		return false;
	}

	InitializeReaders();

	cfg::g_config.boneHead = -1;
	cfg::g_config.boneChest = -1;
	cfg::g_config.boneStomach = -1;

	std::stringstream ss;
	ss << "bound to client.dll (base=0x" << std::hex << g_clientBase << ")";
	util::LogDebug(ss.str());
	return true;
}

// Starts every feature thread.
void StartFeatures()
{
	g_aimbot.Start();
	g_flickshot.Start();
	g_forceAim.Start();
	g_triggerbot.Start();
	g_noFlash.Start();
	g_wallhack.Start();
	g_radar.Start();
	g_webRadar.Start();
}

} // namespace

// Entry point: attach, resolve, start features, run the hotkey listener.
int main()
{
	CreateConsole();

	util::PrepareConsole();
	util::LogDebug("CS2 Muhittin starting");

	bool attachNotified = false;
	while (!g_memory.Attach("cs2.exe")) {
		if (!attachNotified) {
			std::cout << "cs2.exe not found. Start the game (the cheat attaches automatically)." << std::endl;
			attachNotified = true;
		}
		Sleep(1000);
	}
	util::LogDebug("attached to cs2.exe");

	if (!g_offsetsManager.Initialize(g_memory)) {
		util::LogFatal("offset resolution failed, exiting");
		Sleep(2000);
		return 1;
	}

	Sleep(600);

	while (!BindToGame()) {
		Sleep(500);
	}
	ResolveAimBones();
	PrintSettings();
	StartFeatures();

	util::LogDebug("all features started");

	DWORD lastBoneAttempt = 0;
	while (true) {
		if (!g_memory.IsProcessAlive()) {
			if (g_memory.IsAttached()) {
				g_memory.Detach();
				g_clientBase = 0;
				util::LogDebug("cs2.exe exited, waiting for it to come back");
			}
			if (!g_memory.Attach("cs2.exe")) {
				Sleep(1000);
				continue;
			}
			util::LogDebug("re-attached to cs2.exe");
		}

		if (g_clientBase == 0) {
			if (BindToGame()) {
				PrintSettings();
				util::LogDebug("game state re-bound");
			} else {
				Sleep(500);
				continue;
			}
		}

		if (cfg::g_config.boneHead < 0 && GetTickCount() - lastBoneAttempt > 500) {
			lastBoneAttempt = GetTickCount();
			ResolveAimBones();
		}

		if (HandleHotkeys()) {
			BringMenuToFrontIfHidden();
		}

		static std::string lastWebRadarUrl;
		const std::string webRadarUrl = g_webRadar.GetSessionUrl();
		if (!webRadarUrl.empty() && webRadarUrl != lastWebRadarUrl) {
			lastWebRadarUrl = webRadarUrl;
			PrintSettings();
		}

		Sleep(25);
	}
	return 0;
}
