#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>
#include <string>
#include <vector>
#include <cstdarg>
#include <memory>
#include <ctime>
#include <psapi.h>      // For GetModuleInformation
#include <cstring>      // For strncmp
#include <iomanip>      // For std::setw
#include <sstream>

// Constants
const uintptr_t OFFSET_ADJUSTMENT = 0x10; // Adjust based on analysis

// Function to log messages to console and optionally to a file
void log_message(FILE* log_file, bool log_to_file, const char* format, ...) {
    va_list args;
    va_start(args, format);

    // Output to console
    vprintf(format, args);

    if (log_to_file && log_file != NULL) {
        // Make a copy of args for file logging
        va_list args_copy;
        va_copy(args_copy, args);
        vfprintf(log_file, format, args_copy);
        va_end(args_copy);
        fflush(log_file);
    }

    va_end(args);
}

// Function to get the Process ID (PID) by process name
DWORD get_process_id_by_name(const std::string& process_name, FILE* log_file) {
    PROCESSENTRY32A pe32;
    pe32.dwSize = sizeof(PROCESSENTRY32A);
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

    if (hSnapshot == INVALID_HANDLE_VALUE) {
        log_message(log_file, false, "Error: Unable to create tool help snapshot. Error Code: %lu\n", GetLastError());
        return 0;
    }

    if (!Process32FirstA(hSnapshot, &pe32)) {
        log_message(log_file, false, "Error: Unable to retrieve information on the first process. Error Code: %lu\n", GetLastError());
        CloseHandle(hSnapshot);
        return 0;
    }

    do {
        if (_stricmp(pe32.szExeFile, process_name.c_str()) == 0) {
            CloseHandle(hSnapshot);
            log_message(log_file, false, "Process '%s' found with PID %u\n", process_name.c_str(), pe32.th32ProcessID);
            return pe32.th32ProcessID;
        }
    } while (Process32NextA(hSnapshot, &pe32));

    CloseHandle(hSnapshot);
    log_message(log_file, false, "Error: Process '%s' not found\n", process_name.c_str());
    return 0;
}

// Function to get the base address of the main module (e.g., Fallout76.exe)
uintptr_t get_base_address(DWORD pid, FILE* log_file) {
    MODULEENTRY32A me32;
    me32.dwSize = sizeof(MODULEENTRY32A);
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);

    if (hSnapshot == INVALID_HANDLE_VALUE) {
        log_message(log_file, false, "Error: Unable to create module snapshot. Error Code: %lu\n", GetLastError());
        return 0;
    }

    if (!Module32FirstA(hSnapshot, &me32)) {
        log_message(log_file, false, "Error: Unable to retrieve information on the first module. Error Code: %lu\n", GetLastError());
        CloseHandle(hSnapshot);
        return 0;
    }

    uintptr_t base_address = reinterpret_cast<uintptr_t>(me32.modBaseAddr);
    CloseHandle(hSnapshot);
    log_message(log_file, false, "Base address of the main module: 0x%p\n", (void*)base_address);
    return base_address;
}

// Function to get the start and end addresses of a specific section (e.g., .data or .rdata)
bool get_section_limits(HANDLE hProcess, uintptr_t base_addr, const char* section_name, uintptr_t& section_start, uintptr_t& section_end, bool& is64Bit) {
    IMAGE_DOS_HEADER dos_header;
    if (!ReadProcessMemory(hProcess, (LPCVOID)base_addr, &dos_header, sizeof(dos_header), NULL)) {
        return false;
    }

    if (dos_header.e_magic != IMAGE_DOS_SIGNATURE) {
        return false;
    }

    IMAGE_NT_HEADERS32 nt_headers32;
    IMAGE_NT_HEADERS64 nt_headers64;

    // Read the magic number to determine 32 or 64 bit
    WORD magic;
    if (!ReadProcessMemory(hProcess, (LPCVOID)(base_addr + dos_header.e_lfanew + offsetof(IMAGE_NT_HEADERS, OptionalHeader)), &magic, sizeof(magic), NULL)) {
        return false;
    }

    if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
        is64Bit = true;
        // Read entire IMAGE_NT_HEADERS64
        if (!ReadProcessMemory(hProcess, (LPCVOID)(base_addr + dos_header.e_lfanew), &nt_headers64, sizeof(nt_headers64), NULL)) {
            return false;
        }
        // Allocate memory for section headers
        std::unique_ptr<IMAGE_SECTION_HEADER[]> section_headers(new IMAGE_SECTION_HEADER[nt_headers64.FileHeader.NumberOfSections]);
        SIZE_T bytes_read = 0;
        if (!ReadProcessMemory(
            hProcess,
            (LPCVOID)(base_addr + dos_header.e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER) + sizeof(IMAGE_OPTIONAL_HEADER64)),
            section_headers.get(),
            sizeof(IMAGE_SECTION_HEADER) * nt_headers64.FileHeader.NumberOfSections,
            &bytes_read) || bytes_read != sizeof(IMAGE_SECTION_HEADER) * nt_headers64.FileHeader.NumberOfSections) {
            return false;
        }

        // Iterate through sections to find the desired one
        for (unsigned int i = 0; i < nt_headers64.FileHeader.NumberOfSections; ++i) {
            if (strncmp(reinterpret_cast<char*>(section_headers[i].Name), section_name, IMAGE_SIZEOF_SHORT_NAME) == 0) {
                section_start = base_addr + section_headers[i].VirtualAddress;
                section_end = section_start + section_headers[i].Misc.VirtualSize;
                return true;
            }
        }
    }
    else if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
        is64Bit = false;
        // Read entire IMAGE_NT_HEADERS32
        if (!ReadProcessMemory(hProcess, (LPCVOID)(base_addr + dos_header.e_lfanew), &nt_headers32, sizeof(nt_headers32), NULL)) {
            return false;
        }
        // Allocate memory for section headers
        std::unique_ptr<IMAGE_SECTION_HEADER[]> section_headers(new IMAGE_SECTION_HEADER[nt_headers32.FileHeader.NumberOfSections]);
        SIZE_T bytes_read = 0;
        if (!ReadProcessMemory(
            hProcess,
            (LPCVOID)(base_addr + dos_header.e_lfanew + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER) + sizeof(IMAGE_OPTIONAL_HEADER32)),
            section_headers.get(),
            sizeof(IMAGE_SECTION_HEADER) * nt_headers32.FileHeader.NumberOfSections,
            &bytes_read) || bytes_read != sizeof(IMAGE_SECTION_HEADER) * nt_headers32.FileHeader.NumberOfSections) {
            return false;
        }

        // Iterate through sections to find the desired one
        for (unsigned int i = 0; i < nt_headers32.FileHeader.NumberOfSections; ++i) {
            if (strncmp(reinterpret_cast<char*>(section_headers[i].Name), section_name, IMAGE_SIZEOF_SHORT_NAME) == 0) {
                section_start = base_addr + section_headers[i].VirtualAddress;
                section_end = section_start + section_headers[i].Misc.VirtualSize;
                return true;
            }
        }
    }
    else {
        return false; // Unknown format
    }

    return false; // Section not found
}

// Function to find the address of a given ANSI string within a memory section
uintptr_t find_string_in_section(HANDLE hProcess, uintptr_t section_start, uintptr_t section_end, const std::string& target_string, FILE* log_file) {
    SIZE_T section_size = section_end - section_start;
    std::unique_ptr<BYTE[]> buffer(new BYTE[section_size]);
    SIZE_T bytes_read = 0;

    // Read the entire section into the buffer
    if (!ReadProcessMemory(hProcess, (LPCVOID)section_start, buffer.get(), section_size, &bytes_read)) {
        log_message(log_file, false, "Error: Unable to read memory section at 0x%p\n", (void*)section_start);
        return 0;
    }

    if (bytes_read < section_size) {
        log_message(log_file, false, "Warning: Read fewer bytes (%llu) than expected (%llu)\n", bytes_read, section_size);
    }

    // Search for the target string within the buffer
    for (SIZE_T i = 0; i <= bytes_read - target_string.length(); ++i) {
        if (memcmp(buffer.get() + i, target_string.c_str(), target_string.length()) == 0) {
            // Ensure the string is null-terminated
            if (i + target_string.length() < bytes_read && buffer[i + target_string.length()] == '\0') {
                uintptr_t string_addr = section_start + i;
                log_message(log_file, false, "Found string '%s' at address 0x%p\n", target_string.c_str(), (void*)string_addr);
                return string_addr;
            }
        }
    }

    log_message(log_file, false, "String '%s' not found in section 0x%p - 0x%p\n", target_string.c_str(), (void*)section_start, (void*)section_end);
    return 0;
}

// Function to find the address that references a given string address (i.e., where a pointer points to string_addr)
uintptr_t find_pointer_to_address(HANDLE hProcess, uintptr_t section_start, uintptr_t section_end, uintptr_t target_address, bool is64Bit, FILE* log_file) {
    SIZE_T pointer_size = is64Bit ? sizeof(uintptr_t) : sizeof(unsigned long);
    SIZE_T section_size = section_end - section_start;
    std::unique_ptr<BYTE[]> buffer(new BYTE[section_size]);
    SIZE_T bytes_read = 0;

    // Read the entire section into the buffer
    if (!ReadProcessMemory(hProcess, (LPCVOID)section_start, buffer.get(), section_size, &bytes_read)) {
        log_message(log_file, false, "Error: Unable to read memory section at 0x%p\n", (void*)section_start);
        return 0;
    }

    if (bytes_read < section_size) {
        log_message(log_file, false, "Warning: Read fewer bytes (%llu) than expected (%llu)\n", bytes_read, section_size);
    }

    // Iterate through the buffer to find instances of target_address
    for (SIZE_T i = 0; i <= bytes_read - pointer_size; ++i) {
        uintptr_t potential_ptr = 0;

        if (is64Bit) {
            memcpy(&potential_ptr, buffer.get() + i, sizeof(uintptr_t));
        }
        else {
            unsigned long temp_ptr = 0;
            memcpy(&temp_ptr, buffer.get() + i, sizeof(unsigned long));
            potential_ptr = static_cast<uintptr_t>(temp_ptr);
        }

        if (potential_ptr == target_address) {
            uintptr_t pointer_addr = section_start + i;
            log_message(log_file, false, "Found pointer to address 0x%p at 0x%p\n", (void*)target_address, (void*)pointer_addr);
            return pointer_addr;
        }
    }

    log_message(log_file, false, "Pointer to address 0x%p not found in section 0x%p - 0x%p\n", (void*)target_address, (void*)section_start, (void*)section_end);
    return 0;
}

int main() {
    // Define target process and settings
    const std::string process_name = "Fallout76.exe";
    const std::vector<std::string> gameSettings = {
        "fVATSMaxChance",
        "fVATSChancePerceptionMultiplier",
        "fVATSMaxEngageDistance",
        "fVATSChanceRaycastVisibilityMininmum",
        "fVATSPingCost",
        "fVATSCriticalChargeBase",
        "fVATSCriticalChargeBonusMult",
        "fVATSCriticalChargeLimit",
        "fVATSCriticalChargeMult",
        "fPlayerAutoReloadDelaySec",
        "bDisablePlayerCollision",
        "fAVDCarryWeightBase",
        "fAbsolutePlayerInventoryWeightLimit",
        "fInvisibilityMaxRefraction",
        "fInvisibilityMinRefraction",
        "fJetpackThrustInitial",
        "fJetpackThrustSustained",
        "fJetpackTimeToSustained",
        "bJetpackInitialThrustOnlyOnTakeoff",
        "fAimStabilityCrouchedScopeMult",
        "fPropModScaleWeapDurability",
        "fLockpickSkillSweetSpotBase",
        "fSweetSpotEasy",
        "fSweetSpotAverage",
        "fSweetSpotHard",
        "fSweetSpotVeryHard",
        "iHackingMinWords",
        "iHackingMaxWords",
        "fCombatMusicNearCombatInnerRadius",
        "fCombatMusicNearCombatOuterRadius",
        "fCombatMusicPlayerTargetedThreatRatio",
        "pkKeywords",
        "fAttackActionPointCost",
        "fspeed",
        "fweight",
        "iDurability",
        "ihealth",
        "bHoldInputToPower",
        "fMinPowerPerShot",
        "poAmmo",
        "paAimModel",
        "ppOverrideProjectile",
        "uNumProjectiles",
        "vaActorValues"
    };

    // Open log file
    FILE* log_file = nullptr;
    if (fopen_s(&log_file, "memscan.log", "w") != 0) {
        printf("Could not open log file for writing\n");
        return 1;
    }

    // Log start time
    time_t t = time(NULL);
    struct tm tm_info;
    if (localtime_s(&tm_info, &t) != 0) {
        log_message(log_file, false, "Error: Could not get local time.\n");
        fclose(log_file);
        return 1;
    }
    log_message(log_file, true, "Memory scan started at: %04d-%02d-%02d %02d:%02d:%02d\n",
        tm_info.tm_year + 1900, tm_info.tm_mon + 1, tm_info.tm_mday,
        tm_info.tm_hour, tm_info.tm_min, tm_info.tm_sec);

    // Get Process ID
    DWORD pid = get_process_id_by_name(process_name, log_file);
    if (pid == 0) {
        log_message(log_file, true, "Exiting program.\n");
        fclose(log_file);
        return 1;
    }

    // Get Base Address
    uintptr_t base_addr = get_base_address(pid, log_file);
    if (base_addr == 0) {
        log_message(log_file, true, "Exiting program.\n");
        fclose(log_file);
        return 1;
    }

    // Open Process with necessary permissions
    HANDLE hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, FALSE, pid);
    if (hProcess == NULL) {
        log_message(log_file, true, "Error: Could not open process %d. Error Code: %lu\n", pid, GetLastError());
        fclose(log_file);
        return 1;
    }

    // Determine if the process is 64-bit or 32-bit
    BOOL isWow64 = FALSE;
    if (!IsWow64Process(hProcess, &isWow64)) {
        log_message(log_file, true, "Error: Unable to determine process architecture. Error Code: %lu\n", GetLastError());
        CloseHandle(hProcess);
        fclose(log_file);
        return 1;
    }

    BOOL is64Bit = FALSE;
    SYSTEM_INFO systemInfo;
    GetNativeSystemInfo(&systemInfo);
    if (systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_AMD64 ||
        systemInfo.wProcessorArchitecture == PROCESSOR_ARCHITECTURE_ARM64) {
        is64Bit = !isWow64;
    }

    log_message(log_file, true, "Target process is %s-bit.\n", is64Bit ? "64" : "32");

    // Get .data section limits
    uintptr_t data_start = 0, data_end = 0;
    bool data_is64Bit = is64Bit;
    if (!get_section_limits(hProcess, base_addr, ".data", data_start, data_end, data_is64Bit)) {
        log_message(log_file, true, "Error: Could not find .data section.\n");
        CloseHandle(hProcess);
        fclose(log_file);
        return 1;
    }
    log_message(log_file, true, ".data section: Start = 0x%p, End = 0x%p\n", (void*)data_start, (void*)data_end);

    // Get .rdata section limits
    uintptr_t rdata_start = 0, rdata_end = 0;
    bool rdata_is64Bit = is64Bit;
    if (!get_section_limits(hProcess, base_addr, ".rdata", rdata_start, rdata_end, rdata_is64Bit)) {
        log_message(log_file, true, "Error: Could not find .rdata section.\n");
        CloseHandle(hProcess);
        fclose(log_file);
        return 1;
    }
    log_message(log_file, true, ".rdata section: Start = 0x%p, End = 0x%p\n", (void*)rdata_start, (void*)rdata_end);

    // Begin logging results
    fprintf(log_file, "\n$gameSettings = {\n");

    // Iterate through each string in gameSettings
    for (const auto& setting : gameSettings) {
        log_message(log_file, true, "\nProcessing string: '%s'\n", setting.c_str());

        // Step 1: Search for the string in .data and .rdata sections
        uintptr_t string_addr = 0;

        // Search in .data
        string_addr = find_string_in_section(hProcess, data_start, data_end, setting, log_file);

        // If not found in .data, search in .rdata
        if (string_addr == 0) {
            string_addr = find_string_in_section(hProcess, rdata_start, rdata_end, setting, log_file);
        }

        if (string_addr == 0) {
            log_message(log_file, true, "Error: String '%s' not found in any section.\n", setting.c_str());
            continue;
        }

        // Step 2: Search for references (pointers) to string_addr in .data and .rdata sections
        uintptr_t load_addr = 0;

        // Search in .data
        load_addr = find_pointer_to_address(hProcess, data_start, data_end, string_addr, is64Bit, log_file);

        // If not found in .data, search in .rdata
        if (load_addr == 0) {
            load_addr = find_pointer_to_address(hProcess, rdata_start, rdata_end, string_addr, is64Bit, log_file);
        }

        if (load_addr == 0) {
            log_message(log_file, true, "Error: Reference to string '%s' not found.\n", setting.c_str());
            continue;
        }

        // Step 3: Calculate function pointer by subtracting OFFSET_ADJUSTMENT
        uintptr_t func_ptr = load_addr - OFFSET_ADJUSTMENT;
        log_message(log_file, true, "Function pointer for '%s': 0x%p\n", setting.c_str(), (void*)func_ptr);

        // Step 4: Calculate offset relative to base address
        uintptr_t offset = func_ptr - base_addr;
        log_message(log_file, true, "Offset for '%s': 0x%p\n", setting.c_str(), (void*)offset);

        // Log the result in the log file
        fprintf(log_file, "    \"%s\" => 0x%p,\n", setting.c_str(), (void*)offset);
    }

    fprintf(log_file, "};\n\n");

    // Log end time
    t = time(NULL);
    if (localtime_s(&tm_info, &t) != 0) {
        log_message(log_file, false, "Error: Could not get local time.\n");
    }
    else {
        log_message(log_file, true, "Memory scan ended at: %04d-%02d-%02d %02d:%02d:%02d\n",
            tm_info.tm_year + 1900, tm_info.tm_mon + 1, tm_info.tm_mday,
            tm_info.tm_hour, tm_info.tm_min, tm_info.tm_sec);
    }

    // Cleanup
    CloseHandle(hProcess);
    fclose(log_file);

    return 0;
}