/**
* Copyright (C) 2024 Elisha Riedlinger
*
* This software is  provided 'as-is', without any express  or implied  warranty. In no event will the
* authors be held liable for any damages arising from the use of this software.
* Permission  is granted  to anyone  to use  this software  for  any  purpose,  including  commercial
* applications, and to alter it and redistribute it freely, subject to the following restrictions:
*
*   1. The origin of this software must not be misrepresented; you must not claim that you  wrote the
*      original  software. If you use this  software  in a product, an  acknowledgment in the product
*      documentation would be appreciated but is not required.
*   2. Altered source versions must  be plainly  marked as such, and  must not be  misrepresented  as
*      being the original software.
*   3. This notice may not be removed or altered from any source distribution.
*/

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include "patches.h"
#include "Common\Utils.h"
#include "Logging\Logging.h"

// Forward declaration
DWORD CheckUpdateMemory(void *dataSrc, void *dataDest, size_t size, bool SearchMemory);

// ASM function to always draw all water chunks in certain hallways
__declspec(naked) void __stdcall DrawDistanceASM()
{
	__asm
	{
		mov ecx, dword ptr ds : [esp + 0x08]
		test ecx, ecx
		jna near Exit
		push edi
		mov edi, dword ptr ds : [esp + 0x08]
		mov eax, 0x00000001
		repe stosd
		pop edi
	Exit:
		ret
	}
}

// Patch SH2 code for Draw Distance
void PatchDrawDistance()
{
	// Loop variables
	DWORD LoopCounter = 0;
	const DWORD SizeOfBytes = 10;
	BYTE SrcByteData[SizeOfBytes] = { NULL };
	BYTE DestByteData[SizeOfBytes] = { NULL };
	DWORD StartAddr = 0x0047C000;
	DWORD EndAddr = 0x004FFFFF;
	bool ExitFlag = false;

	// Predefined code bytes
	constexpr BYTE DDStartAddr[] = { 0xC7, 0x05, 0x58 };
	constexpr BYTE DDSearchAddr[] = { 0x94, 0x00, 0x00, 0x60, 0xEA, 0x45 };

	// Get data bytes from code
	while (!ExitFlag && StartAddr < EndAddr && LoopCounter < 10)
	{
		LoopCounter++;

		// Get next address
		void *NextAddr = CheckMultiMemoryAddress((void*)0x0047C19D, (void*)0x0047C43D, (void*)0x0047C64D, (void*)DDSearchAddr, sizeof(DDSearchAddr), __FUNCTION__);

		// Search for address
		if (!NextAddr)
		{
			Logging::Log() << __FUNCTION__ << " searching for memory address!";
			NextAddr = GetAddressOfData(DDSearchAddr, sizeof(DDSearchAddr), 1, StartAddr, EndAddr - StartAddr);
		}

		// Checking address pointer
		if (!NextAddr)
		{
			Logging::Log() << __FUNCTION__ << " Error: could not find binary data!";
			return;
		}
		StartAddr = (DWORD)NextAddr + SizeOfBytes;
		NextAddr = (void*)((DWORD)NextAddr - 4);

		// Check if this is the correct address and store bytes
		if (CheckMemoryAddress(NextAddr, (void*)DDStartAddr, sizeof(DDStartAddr), __FUNCTION__))
		{
			ExitFlag = true;
			memcpy(SrcByteData, NextAddr, SizeOfBytes);
			memcpy(DestByteData, NextAddr, SizeOfBytes);
			float DrawDistance = 11000.0f;
			memcpy(DestByteData + 6, (void*)&DrawDistance, sizeof(float));
		}
	}

	// Check if data bytes are found
	if (SrcByteData[0] != DDStartAddr[0] || SrcByteData[1] != DDStartAddr[1] || SrcByteData[2] != DDStartAddr[2])
	{
		Logging::Log() << __FUNCTION__ << " Error: binary data does not match!";
		return;
	}

	// Logging
	Logging::Log() << "Increasing the Draw Distance...";

	bool SearchMemoryFlag = false;
	void *NextAddr = nullptr;

	// Address 1
	NextAddr = CheckMultiMemoryAddress((void*)0x0047C199, (void*)0x0047C439, (void*)0x0047C649, (void*)SrcByteData, SizeOfBytes, __FUNCTION__);
	SearchMemoryFlag = CheckUpdateMemory(NextAddr, DestByteData, SizeOfBytes, SearchMemoryFlag);

	// Address 2
	NextAddr = CheckMultiMemoryAddress((void*)0x0057E7C5, (void*)0x0057F075, (void*)0x0057E995, (void*)SrcByteData, SizeOfBytes, __FUNCTION__);
	SearchMemoryFlag = CheckUpdateMemory(NextAddr, DestByteData, SizeOfBytes, SearchMemoryFlag);

	// Address 3
	NextAddr = CheckMultiMemoryAddress((void*)0x00587F77, (void*)0x00588827, (void*)0x00588147, (void*)SrcByteData, SizeOfBytes, __FUNCTION__);
	SearchMemoryFlag = CheckUpdateMemory(NextAddr, DestByteData, SizeOfBytes, SearchMemoryFlag);

	// Address 4
	NextAddr = CheckMultiMemoryAddress((void*)0x00587FB7, (void*)0x00588867, (void*)0x00588187, (void*)SrcByteData, SizeOfBytes, __FUNCTION__);
	SearchMemoryFlag = CheckUpdateMemory(NextAddr, DestByteData, SizeOfBytes, SearchMemoryFlag);

	// Address 5
	NextAddr = CheckMultiMemoryAddress((void*)0x00594FE6, (void*)0x00595896, (void*)0x005951B6, (void*)SrcByteData, SizeOfBytes, __FUNCTION__);
	SearchMemoryFlag = CheckUpdateMemory(NextAddr, DestByteData, SizeOfBytes, SearchMemoryFlag);

	// Address 6
	NextAddr = CheckMultiMemoryAddress((void*)0x0059EC1B, (void*)0x0059F4CB, (void*)0x0059EDEB, (void*)SrcByteData, SizeOfBytes, __FUNCTION__);
	SearchMemoryFlag = CheckUpdateMemory(NextAddr, DestByteData, SizeOfBytes, SearchMemoryFlag);

	// Update all SH2 code with new DrawDistance values
	if (SearchMemoryFlag)
	{
		Logging::Log() << __FUNCTION__ << " searching for memory address!";
		if (!ReplaceMemoryBytes(SrcByteData, DestByteData, SizeOfBytes, 0x0047C000, 0x005FFFFF - 0x0047C000))
		{
			Logging::Log() << __FUNCTION__ << " Error: replacing pointer!";
		}
	}

	// Fix draw distance in water-filled hallways
    std::vector<DWORD> DrawFunctionAddrVec;
    {
        constexpr BYTE DrawFunctionSearchBytes[]{ 0x8D, 0x9E, 0xBC, 0x18, 0x00, 0x00, 0x6A, 0x04, 0x53, 0x8B, 0xF8, 0xE8 };
        DrawFunctionAddrVec.push_back(SearchAndGetAddresses(0x004E3611, 0x004E38C1, 0x004E3181, DrawFunctionSearchBytes, sizeof(DrawFunctionSearchBytes), 0x0B, __FUNCTION__));
    }
    {
        constexpr BYTE DrawFunctionSearchBytes[]{ 0x8D, 0x9E, 0x48, 0x0E, 0x00, 0x00, 0x6A, 0x03, 0x53, 0x8B, 0xF8, 0xE8 };
        DrawFunctionAddrVec.push_back(SearchAndGetAddresses(0x004E4E14, 0x004E50C4, 0x004E4984, DrawFunctionSearchBytes, sizeof(DrawFunctionSearchBytes), 0x0B, __FUNCTION__));
    }
    {
        constexpr BYTE DrawFunctionSearchBytes[]{ 0x8D, 0x9E, 0xD4, 0x16, 0x00, 0x00, 0x6A, 0x07, 0x53, 0x8B, 0xF8, 0xE8 };
        DrawFunctionAddrVec.push_back(SearchAndGetAddresses(0x004E5582, 0x004E5832, 0x004E50F2, DrawFunctionSearchBytes, sizeof(DrawFunctionSearchBytes), 0x0B, __FUNCTION__));
    }
    {
        constexpr BYTE DrawFunctionSearchBytes[]{ 0x8D, 0x9E, 0xE0, 0x22, 0x00, 0x00, 0x6A, 0x07, 0x53, 0x8B, 0xF8, 0xE8 };
        DrawFunctionAddrVec.push_back(SearchAndGetAddresses(0x004E5D02, 0x004E5FB2, 0x004E5872, DrawFunctionSearchBytes, sizeof(DrawFunctionSearchBytes), 0x0B, __FUNCTION__));
    }
    {
        constexpr BYTE DrawFunctionSearchBytes[]{ 0x8D, 0x9E, 0x30, 0x20, 0x00, 0x00, 0x6A, 0x08, 0x53, 0x8B, 0xF8, 0xE8 };
        DrawFunctionAddrVec.push_back(SearchAndGetAddresses(0x004E64B2, 0x004E6762, 0x004E6022, DrawFunctionSearchBytes, sizeof(DrawFunctionSearchBytes), 0x0B, __FUNCTION__));
    }
    {
        constexpr BYTE DrawFunctionSearchBytes[]{ 0x8D, 0x9E, 0xA0, 0x1A, 0x00, 0x00, 0x6A, 0x07, 0x53, 0x8B, 0xF8, 0xE8 };
        DrawFunctionAddrVec.push_back(SearchAndGetAddresses(0x004E6C42, 0x004E6EF2, 0x004E67B2, DrawFunctionSearchBytes, sizeof(DrawFunctionSearchBytes), 0x0B, __FUNCTION__));
    }
    {
        constexpr BYTE DrawFunctionSearchBytes[]{ 0x8D, 0x9E, 0x00, 0x19, 0x00, 0x00, 0x6A, 0x04, 0x53, 0x8B, 0xF8, 0xE8 };
        DrawFunctionAddrVec.push_back(SearchAndGetAddresses(0x004E7371, 0x004E7621, 0x004E6EE1, DrawFunctionSearchBytes, sizeof(DrawFunctionSearchBytes), 0x0B, __FUNCTION__));
    }
    {
        constexpr BYTE DrawFunctionSearchBytes[]{ 0x8D, 0xBE, 0xA4, 0x13, 0x00, 0x00, 0x6A, 0x05, 0x57, 0x8B, 0xD8, 0xE8 };
        DrawFunctionAddrVec.push_back(SearchAndGetAddresses(0x004E8003, 0x004E82B3, 0x004E7B73, DrawFunctionSearchBytes, sizeof(DrawFunctionSearchBytes), 0x0B, __FUNCTION__));
    }
    for (const DWORD addr : DrawFunctionAddrVec)
    {
        if (!addr)
        {
            Logging::Log() << __FUNCTION__ " Error: failed to find memory address!";
            return;
        }
        WriteCalltoMemory((BYTE*)addr, DrawDistanceASM);
    }
}

DWORD CheckUpdateMemory(void *dataSrc, void *dataDest, size_t size, bool SearchMemoryFlag)
{
	if (dataSrc)
	{
		UpdateMemoryAddress(dataSrc, dataDest, size);
	}
	else
	{
		SearchMemoryFlag = true;
	}
	return SearchMemoryFlag;
}

void RunDynamicDrawDistance()
{
	// Get dynamic draw distance address
	static float *Address = nullptr;
	if (!Address)
	{
		RUNONCE();

		// Get address for dynamic draw distance
		constexpr BYTE SearchBytes[]{ 0xFF, 0xFF, 0x8D, 0x44, 0x24, 0x0C, 0x50, 0x68 };
		Address = (float*)ReadSearchedAddresses(0x0047D824, 0x0047DAC4, 0x0047DCD4, SearchBytes, sizeof(SearchBytes), 0x08, __FUNCTION__);
		if (!Address)
		{
			Logging::Log() << __FUNCTION__ " Error: failed to find memory address!";
			return;
		}
		Address += 0x02;
	}

	// Set dynamic draw distance
	static bool ValueSet = false;
	if (GetRoomID() == R_FOREST_CEMETERY)
	{
		if (!ValueSet)
		{
			float Value = 44.0f;
			UpdateMemoryAddress(Address, &Value, sizeof(float));
			ValueSet = true;
		}
	}
	else if (GetRoomID() == R_EDI_BOSS_RM_2 || GetRoomID() == R_MAN_BLUE_CREEK_ENTRANCE)
	{
		if (!ValueSet)
		{
			float Value = 2.0f;
			UpdateMemoryAddress(Address, &Value, sizeof(float));
			ValueSet = true;
		}
	}
	else if (ValueSet)
	{
		float Value = 1.0f;
		UpdateMemoryAddress(Address, &Value, sizeof(float));
		ValueSet = false;
	}
}
