
#define _CRT_SECURE_NO_WARNINGS
#include "MainLoop.h"
#include "Mutex.h"
#include "FireDemo.h"
#include "FrameBuffer.h"
#include "Window.h"
#include "Input.h"
#include "Timestamp.h"
#include <stdio.h>
#include <wchar.h>
#include <stdlib.h>

// Live++ API needed for hooks
#include "LivePP/API/x64/LPP_API_x64_CPP.h"

// global constants
static const int g_width = 1280;
static const int g_height = 720;

// global state
static Mutex* g_mutex = nullptr;
static FireDemo* g_fireDemo = nullptr;
static FrameBuffer* g_frameBuffer = nullptr;
static Window* g_window = nullptr;
static Input g_hotReloadInput(Input::Type::HotReload);
static Input g_hotRestartInput(Input::Type::HotRestart);


// Live++: connection callback invoked by optional OnConnection()
static void ConnectionCallback(void* /*context*/, lpp::LppConnectionStatus status)
{
	if (status == lpp::LPP_CONNECTION_STATUS_SUCCESS)
	{
		printf("\n--- Connection callback: SUCCESS ---\n");
	}
	else if (status == lpp::LPP_CONNECTION_STATUS_FAILURE)
	{
		printf("\n--- Connection callback: FAILURE ---\n");
	}
	else if (status == lpp::LPP_CONNECTION_STATUS_UNEXPECTED_VERSION_BLOB)
	{
		printf("\n--- Connection callback: INCOMPATIBLE AGENT AND BROKER ---\n");
	}
	else if (status == lpp::LPP_CONNECTION_STATUS_VERSION_MISMATCH)
	{
		printf("\n--- Connection callback: INCOMPATIBLE AGENT AND BROKER VERSIONS ---\n");
	}
}


// Live++: these functions demonstrate pre-patch and post-patch hooks that allow you to re-initialize state in order to support structural layout changes
static void FunctionCalledBeforePatching(lpp::LppHotReloadPrepatchHookId, const wchar_t* const recompiledModulePath, const wchar_t* const* const modifiedFiles, unsigned int modifiedFilesCount, const wchar_t* const* const, unsigned int)
{
	printf("Pre-patch: %S\n", recompiledModulePath);
	printf("%u modified files:\n", modifiedFilesCount);

	for (unsigned int i = 0u; i < modifiedFilesCount; ++i)
	{
		printf("  File %S\n", modifiedFiles[i]);
	}

	// Live++: as hook demonstration, we tear down and rebuild the g_fireDemo state in case either FireDemo.h or FireDemo.cpp was changed.
	// this allows us to add new members to FireDemo (e.g. a third cube), and properly initialize and use them.
	// note that hooks are called from a Live++ thread and not the main thread, so we need to synchronize deletion and creation of the FireDemo with accesses during Update() and Render().
	for (unsigned int i = 0u; i < modifiedFilesCount; ++i)
	{
		const wchar_t* const path = modifiedFiles[i];
		const bool modifiedCPP = (wcsstr(path, L"FireDemo.cpp") != nullptr);
		if (modifiedCPP)
		{
			Mutex::ScopedLock lock(g_mutex);

			delete g_fireDemo;
			g_fireDemo = nullptr;
		}
	}
}

static void FunctionCalledAfterPatching(lpp::LppHotReloadPostpatchHookId, const wchar_t* const recompiledModulePath, const wchar_t* const* const modifiedFiles, unsigned int modifiedFilesCount, const wchar_t* const* const, unsigned int)
{
	printf("Post-patch: %S\n", recompiledModulePath);
	printf("%u modified files:\n", modifiedFilesCount);

	for (unsigned int i = 0u; i < modifiedFilesCount; ++i)
	{
		printf("  File %S\n", modifiedFiles[i]);
	}

	// Live++: as hook demonstration, we tear down and rebuild the g_fireDemo state in case either FireDemo.h or FireDemo.cpp was changed.
	// this allows us to add new members to FireDemo (e.g. a third cube), and properly initialize and use them.
	// note that hooks are called from a Live++ thread and not the main thread, so we need to synchronize deletion and creation of the FireDemo with accesses during Update() and Render().
	for (unsigned int i = 0u; i < modifiedFilesCount; ++i)
	{
		const wchar_t* const path = modifiedFiles[i];
		const bool modifiedCPP = (wcsstr(path, L"FireDemo.cpp") != nullptr);
		if (modifiedCPP)
		{
			Mutex::ScopedLock lock(g_mutex);

			g_fireDemo = new FireDemo(g_width, g_height);
		}
	}
}

LPP_HOTRELOAD_PREPATCH_HOOK(FunctionCalledBeforePatching);
LPP_HOTRELOAD_POSTPATCH_HOOK(FunctionCalledAfterPatching);


void MainLoop::Init(const char* windowTitle)
{
	g_mutex = new Mutex;
	g_fireDemo = new FireDemo(g_width, g_height);
	g_frameBuffer = new FrameBuffer(g_width, g_height);
	g_window = new Window(windowTitle, g_frameBuffer);

	// move the console next to the main window
	{
		HWND nativeConsoleWindow = ::GetConsoleWindow();

		const int mainWindowX = g_window->GetPositionX() + g_width;
		const int mainWindowY = g_window->GetPositionY();
		::SetWindowPos(nativeConsoleWindow, HWND_TOP, mainWindowX, mainWindowY, 920, g_window->GetHeight(), SWP_NOZORDER);
	}
}


void MainLoop::Exit(void)
{
	delete g_window;
	delete g_frameBuffer;
	delete g_fireDemo;
	delete g_mutex;
}


void MainLoop::Announce(lpp::LppSynchronizedAgent& agent, const char* const pathToReadmeFile)
{
	// load the text to be announced from the given README file
	FILE* file = ::fopen(pathToReadmeFile, "rb");
	::fseek(file, 0, SEEK_END);
	const long fileSize = ::ftell(file);
	::fseek(file, 0, SEEK_SET);

	char* text = static_cast<char*>(::malloc(fileSize + 1u));

	::fread(text, fileSize, 1, file);
	::fclose(file);

	text[fileSize] = '\0';

	// print to the console
	printf("%s", text);

	// log to the Live++ UI
	agent.LogMessageANSI(text);

	::free(text);

	agent.OnConnection(nullptr, &ConnectionCallback);
}


bool MainLoop::PollInput(lpp::LppSynchronizedAgent& agent)
{
	g_hotReloadInput.Poll();
	g_hotRestartInput.Poll();

	if (g_hotReloadInput.WentDown())
	{
		// Live++: schedule a hot-reload when 'F1' is pressed.
		// a hot-reload can be scheduled anytime you want: when pressing a key, clicking a button, etc.
		agent.ScheduleReload();
	}

	if (g_hotRestartInput.WentDown())
	{
		// Live++: schedule a hot-restart when 'F2' is pressed.
		// a hot-restart can be scheduled anytime you want: when pressing a key, clicking a button, etc.
		agent.ScheduleRestart(lpp::LPP_RESTART_OPTION_ALL_PROCESSES);
	}

	g_hotReloadInput.Next();
	g_hotRestartInput.Next();

	return g_window->PollMessages();
}


void MainLoop::Update(void)
{
	// update delta time
	static unsigned long long last = 0ull;

	const unsigned long long now = Timestamp::Get();
	const double deltaTime = Timestamp::ToSeconds(now - last);
	last = now;

	// Live++: protect against concurrent access in the pre-patch and post-patch hooks
	Mutex::ScopedLock lock(g_mutex);

	if (g_fireDemo)
	{
		g_fireDemo->Update(static_cast<float>(deltaTime));
	}
}


void MainLoop::Render(void)
{
	// Live++: protect against concurrent access in the pre-patch and post-patch hooks
	Mutex::ScopedLock lock(g_mutex);

	if (g_fireDemo)
	{
		g_fireDemo->Render();
		g_frameBuffer->FillFromPalettizedFrameBuffer(g_fireDemo->GetPalettizedFrameBuffer(), g_fireDemo->GetPalette());
	}
}


void MainLoop::RenderPlugin(void* plugin)
{
	using PluginRenderFunction = void __cdecl (unsigned int width, unsigned int height, unsigned char* rgbaBuffer);
	PluginRenderFunction* pluginRenderFunction = reinterpret_cast<PluginRenderFunction*>(::GetProcAddress(static_cast<HMODULE>(plugin), "PluginRender"));

	if (pluginRenderFunction)
	{
		pluginRenderFunction(g_frameBuffer->GetWidth(), g_frameBuffer->GetHeight(), g_frameBuffer->GetRGBABuffer());
	}
}


void MainLoop::Present(void)
{
	g_window->Present();
}
