#include "FrameRateManager.h"
#include "../Constants.h"
#include "GameState.h"
#include <stdio.h>
#include <windows.h>
#include "../framework.h"
#include <vector>
#include "detours.h"

namespace TLAC::Components
{
	FrameRateManager::FrameRateManager()
	{
		std::string utf8path = TLAC::framework::GetModuleDirectory() + "/config.ini";
		WCHAR utf16buf[256];
		MultiByteToWideChar(CP_UTF8, 0, utf8path.c_str(), -1, utf16buf, 256);

		float nMotionRate = GetPrivateProfileIntW(L"graphics", L"frm.motion.rate", 300, utf16buf);
		motionSpeedMultiplier = nMotionRate / 60.0f;
	}

	FrameRateManager::~FrameRateManager()
	{
	}

	const char* FrameRateManager::GetDisplayName()
	{
		return "frame_rate_manager";
	}


	float FrameRateManager::fspeed_error = 0; // compensation value for use in this frame
	float FrameRateManager::fspeed_error_next = 0; // save a compensation value to be used in the next frame

	float FrameRateManager::fspeedhook_lastresult = 0; // used by the ageage hair patch to be lazy and avoid setting up for a proper call

	float(*divaGetFrameSpeed)() = (float(*)())0x140192D50;

	// a version of the original function that tries to round the output to more closely match chara motion timings
	float hookedGetFrameSpeed()
	{
		float frameSpeed = divaGetFrameSpeed();

		// below is somewhat based (in concept) on 140194ad0 (motion quantisation thingy func)

		// add the error compensation from last frame
		frameSpeed += FrameRateManager::fspeed_error;

		// separate whole and fractional parts of speed
		// float speed_rounded = floorf(frameSpeed);
		// float speed_remainder = frameSpeed - speed_rounded;
		float speed_rounded;
		float speed_remainder = modff(frameSpeed, &speed_rounded);

		// save the remainder as error compensation for next frame
		// use == 0 to detect new frame and vars must be written
		if (FrameRateManager::fspeed_error_next == 0)
		{
			FrameRateManager::fspeed_error_next = speed_remainder;
			FrameRateManager::fspeedhook_lastresult = speed_rounded;
		}

		return speed_rounded;
	}

	void FrameRateManager::Initialize(ComponentsManager*)
	{
		pvFrameRate = (float*)PV_FRAME_RATE_ADDRESS;
		frameSpeed = (float*)FRAME_SPEED_ADDRESS;
		aetFrameDuration = (float*)AET_FRAME_DURATION_ADDRESS;

		// The default is expected to be 1.0 / 60.0
		defaultAetFrameDuration = *aetFrameDuration;

		// This const variable is stored inside a data segment so we don't want to throw any access violations
		DWORD oldProtect;
		VirtualProtect((void*)AET_FRAME_DURATION_ADDRESS, sizeof(float), PAGE_EXECUTE_READWRITE, &oldProtect);


		// fix auto speed for high fps
		InjectCode((void*)0x140192d7b, { 0x90, 0x90, 0x90 });

		// fix frame speed slider initial value (should ignore effect of auto speed)
		InjectCode((void*)0x140338f2f, { 0xf3, 0x0f, 0x10, 0x0d, 0x61, 0x18, 0xba, 0x00 }); // MOVSS XMM1, dword ptr [0x140eda798] (raw framespeed)
		InjectCode((void*)0x140338f37, { 0x48, 0x8b, 0x8f, 0x80, 0x01, 0x00, 0x00 });       // MOV RCX, qword ptr [0x180 + RDI]

		InjectCode((void*)0x140338ebe, { 0xf3, 0x0f, 0x10, 0x0d, 0xd2, 0x18, 0xba, 0x00 }); // MOVSS XMM1, dword ptr [0x140eda798] (raw framespeed)
		InjectCode((void*)0x140338ec6, { 0x48, 0x8b, 0x05, 0xfb, 0xb1, 0xe5, 0x00 });       // MOV RAX, qword ptr [0x1411940c8]
		InjectCode((void*)0x140338ecd, { 0x48, 0x8b, 0x88, 0x80, 0x01, 0x00, 0x00 });       // MOV RCX, qword ptr [0x180 + RAX]


		// fix AETs
		InjectCode((void*)0x140170394, { 0xF3, 0x0F, 0x5E, 0x05, 0x34, 0xA3, 0xD6, 0x00 }); // DIVSS XMM0, dword ptr [0x140eda6d0] (framerate)

		// fix edit PV AETs (thanks lyb)
		InjectCode((void*)0x140192d30, { 0xF3, 0x0F, 0x10, 0x05, 0x5C, 0x02, 0x00, 0x00 }); // MOVSS XMM0, dword ptr [0x140192f94]

		// fix ageage hair effect
		InjectCode((void*)0x14054352f, {                                  // MOV R9, &fspeedhook_lastresult
			0x49,
			0xB9,
			(uint8_t)((uint64_t)&fspeedhook_lastresult & 0xFF),
			(uint8_t)(((uint64_t)&fspeedhook_lastresult >> 8) & 0xFF),
			(uint8_t)(((uint64_t)&fspeedhook_lastresult >> 16) & 0xFF),
			(uint8_t)(((uint64_t)&fspeedhook_lastresult >> 24) & 0xFF),
			(uint8_t)(((uint64_t)&fspeedhook_lastresult >> 32) & 0xFF),
			(uint8_t)(((uint64_t)&fspeedhook_lastresult >> 40) & 0xFF),
			(uint8_t)(((uint64_t)&fspeedhook_lastresult >> 48) & 0xFF),
			(uint8_t)(((uint64_t)&fspeedhook_lastresult >> 56) & 0xFF),
		});
		InjectCode((void*)0x140543539, { 0xF3, 0x41, 0x0F, 0x59, 0x19 }); // MULSS XMM3, dword ptr [R9]
		InjectCode((void*)0x14054353e, { 0xEB, 0xB0 });                   // JMP 0x1405434f0

		// fix wind effect
		InjectCode((void*)0x14053ca71, { 0xEB, 0x3F });                                     // JMP 0x14053cab2
		InjectCode((void*)0x14053cab2, { 0xF3, 0x0F, 0x10, 0x05, 0xFA, 0x53, 0x46, 0x00 }); // MOVSS XMM0, dword ptr [0x1409a1eb4] (60.0f)
		InjectCode((void*)0x14053caba, { 0xE9, 0x42, 0xFE, 0xFF, 0xFF });                   // JMP 0x14053c901
		InjectCode((void*)0x14053c901, { 0xF3, 0x0F, 0x5E, 0x05, 0xC7, 0xDD, 0x99, 0x00 }); // DIVSS XMM0, dword ptr [0x140eda6d0] (framerate)
		InjectCode((void*)0x14053c909, { 0xE9, 0x68, 0x01, 0x00, 0x00 });                   // JMP 0x14053ca76


		// replace divaGetFrameSpeed with a version that rounds the output to fix issues
		DetourTransactionBegin();
		DetourUpdateThread(GetCurrentThread());
		DetourAttach(&(PVOID&)divaGetFrameSpeed, hookedGetFrameSpeed);
		DetourTransactionCommit();
	}

	void FrameRateManager::Update()
	{
		// *aetFrameDuration = 1.0f / GetGameFrameRate();

		if (*(GameState*)CURRENT_GAME_STATE_ADDRESS == GS_GAME)
		{
			*pvFrameRate = 60.0f * motionSpeedMultiplier;
		}
		else
		{
			*pvFrameRate = 60.0f;
		}

		if (*(SubGameState*)CURRENT_GAME_SUB_STATE_ADDRESS == SUB_GAME_MAIN || *(SubGameState*)CURRENT_GAME_SUB_STATE_ADDRESS == SUB_DEMO)
		{
			// enable dynamic framerate
			*(bool*)USE_AUTO_FRAMESPEED_ADDRESS = true;

			// target framerate
			*(float*)AUTO_FRAMESPEED_TARGET_FRAMERATE_ADDRESS = *pvFrameRate;

			// trying to fix meltdown's water
			//if ((uint64_t*)0x1411943f8 != nullptr)
				//*(float*)(*(uint64_t*)0x1411943f8 + 0x1c) = (60.0f / GetGameFrameRate()) / 15.0f;
				//*(float*)(*(uint64_t*)0x1411943f8 + 0x10) = *(float*)(*(uint64_t*)0x1411943f8);
		}
		else
		{
			// enable dynamic framerate
			*(bool*)USE_AUTO_FRAMESPEED_ADDRESS = true;

			// target framerate
			*(float*)AUTO_FRAMESPEED_TARGET_FRAMERATE_ADDRESS = 60.0f;
		}
	}

	void FrameRateManager::UpdateDraw2D()
	{
		// cycle the framespeed timing error once per frame
		fspeed_error = fspeed_error_next;
		fspeed_error_next = 0;
	}

	void FrameRateManager::InjectCode(void* address, const std::vector<uint8_t> data)
	{
		const size_t byteCount = data.size() * sizeof(uint8_t);

		DWORD oldProtect;
		VirtualProtect(address, byteCount, PAGE_EXECUTE_READWRITE, &oldProtect);
		memcpy(address, data.data(), byteCount);
		VirtualProtect(address, byteCount, oldProtect, nullptr);
	}
}
