#include "STDInclude.hpp"

namespace Components
{
	WINDOW* Console::OutputWindow;
	WINDOW* Console::InputWindow;
	WINDOW* Console::InfoWindow;

	int Console::OutputTop = 0;
	int Console::OutBuffer = 0;
	int Console::LastRefresh = 0;

	int Console::Height = 25;
	int Console::Width = 80;

	char Console::LineBuffer[1024] = { 0 };
	char Console::LineBuffer2[1024] = { 0 };
	int Console::LineBufferIndex = 0;

	bool Console::HasConsole = false;

	std::thread Console::ConsoleThread;

	Game::SafeArea Console::OriginalSafeArea;

	char** Console::GetAutoCompleteFileList(const char *path, const char *extension, Game::FsListBehavior_e behavior, int *numfiles, int allocTrackType)
	{
		if (path == reinterpret_cast<char*>(0xBAADF00D) || path == reinterpret_cast<char*>(0xCDCDCDCD) || IsBadReadPtr(path, 1)) return nullptr;
		return Game::FS_GetFileList(path, extension, behavior, numfiles, allocTrackType);
	}

	void Console::ToggleConsole()
	{
		// possibly cls.keyCatchers?
		Utils::Hook::Xor<DWORD>(0xB2C538, 1);

		// g_consoleField
		Game::Field_Clear((void*)0xA1B6B0);

		// show console output?
		Utils::Hook::Set<BYTE>(0xA15F38, 0);
	}

	void Console::RefreshStatus()
	{
		std::string mapname = Dvar::Var("mapname").Get<const char*>();
		std::string hostname = Colors::Strip(Dvar::Var("sv_hostname").Get<const char*>());
		SetConsoleTitleA(hostname.data());

		int clientCount = 0;
		int maxclientCount = *Game::svs_numclients;

		if (maxclientCount)
		{
			for (int i = 0; i < maxclientCount; ++i)
			{
				if (Game::svs_clients[i].state >= 3)
				{
					++clientCount;
				}
			}
		}
		else
		{
			//maxclientCount = Dvar::Var("sv_maxclients").Get<int>();
			maxclientCount = Game::Party_GetMaxPlayers(*Game::partyIngame);
			clientCount = Game::PartyHost_CountMembers(reinterpret_cast<Game::PartyData_s*>(0x1081C00));
		}

		wclear(Console::InfoWindow);
		wprintw(Console::InfoWindow, "%s : %d/%d players : map %s", hostname.data(), clientCount, maxclientCount, (mapname.size() ? mapname.data() : "none"));
		wnoutrefresh(Console::InfoWindow);
	}

	void Console::ShowPrompt()
	{
		wattron(Console::InputWindow, COLOR_PAIR(10) | A_BOLD);
		wprintw(Console::InputWindow, "%s> ", VERSION_STR);
	}

	void Console::RefreshOutput()
	{
		prefresh(Console::OutputWindow, ((Console::OutputTop > 0) ? (Console::OutputTop - 1) : 0), 0, 1, 0, Console::Height - 2, Console::Width);
	}

	void Console::ScrollOutput(int amount)
	{
		Console::OutputTop += amount;

		if (Console::OutputTop > OUTPUT_MAX_TOP)
		{
			Console::OutputTop = OUTPUT_MAX_TOP;
		}
		else if (Console::OutputTop < 0)
		{
			Console::OutputTop = 0;
		}

		// make it only scroll the top if there's more than HEIGHT lines
		if (Console::OutBuffer >= 0)
		{
			Console::OutBuffer += amount;

			if (Console::OutBuffer >= Console::Height)
			{
				Console::OutBuffer = -1;
			}

			if (Console::OutputTop < Console::Height)
			{
				Console::OutputTop = 0;
			}
		}
	}

	const char* Console::Input()
	{
		if (!Console::HasConsole)
		{
			Console::ShowPrompt();
			wrefresh(Console::InputWindow);
			Console::HasConsole = true;
		}

		int currentTime = static_cast<int>(GetTickCount64()); // Make our compiler happy
		if ((currentTime - Console::LastRefresh) > 250)
		{
			Console::RefreshOutput();
			Console::RefreshStatus();
			Console::LastRefresh = currentTime;
		}

		int c = wgetch(Console::InputWindow);

		if (c == ERR)
		{
			return NULL;
		}

		switch (c)
		{
		case '\r':
		case 459: // keypad enter
		{
			wattron(Console::OutputWindow, COLOR_PAIR(10) | A_BOLD);
			wprintw(Console::OutputWindow, "%s", "]");

			if (Console::LineBufferIndex)
			{
				wprintw(Console::OutputWindow, "%s", Console::LineBuffer);
			}

			wprintw(Console::OutputWindow, "%s", "\n");
			wattroff(Console::OutputWindow, A_BOLD);
			wclear(Console::InputWindow);

			Console::ShowPrompt();

			wrefresh(Console::InputWindow);

			Console::ScrollOutput(1);
			Console::RefreshOutput();

			if (Console::LineBufferIndex)
			{
				strcpy_s(Console::LineBuffer2, Console::LineBuffer);
				strcat_s(Console::LineBuffer, "\n");
				Console::LineBufferIndex = 0;
				return Console::LineBuffer;
			}

			break;
		}
		case 'c' - 'a' + 1: // ctrl-c
		case 27:
		{
			Console::LineBuffer[0] = '\0';
			Console::LineBufferIndex = 0;

			wclear(Console::InputWindow);

			Console::ShowPrompt();

			wrefresh(Console::InputWindow);
			break;
		}
		case 8: // backspace
		{
			if (Console::LineBufferIndex > 0)
			{
				Console::LineBufferIndex--;
				Console::LineBuffer[Console::LineBufferIndex] = '\0';

				wprintw(Console::InputWindow, "%c %c", static_cast<char>(c), static_cast<char>(c));
				wrefresh(Console::InputWindow);
			}
			break;
		}
		case KEY_PPAGE:
		{
			Console::ScrollOutput(-1);
			Console::RefreshOutput();
			break;
		}
		case KEY_NPAGE:
		{
			Console::ScrollOutput(1);
			Console::RefreshOutput();
			break;
		}
		case KEY_UP:
		{
			wclear(Console::InputWindow);
			Console::ShowPrompt();
			wprintw(Console::InputWindow, "%s", Console::LineBuffer2);
			wrefresh(Console::InputWindow);

			strcpy_s(Console::LineBuffer, Console::LineBuffer2);
			Console::LineBufferIndex = strlen(Console::LineBuffer);
			break;
		}
		default:
			if (c <= 127 && Console::LineBufferIndex < 1022)
			{
				// temporary workaround , find out what overwrites our index later on
				//consoleLineBufferIndex = strlen(consoleLineBuffer);

				Console::LineBuffer[Console::LineBufferIndex++] = static_cast<char>(c);
				Console::LineBuffer[Console::LineBufferIndex] = '\0';
				wprintw(Console::InputWindow, "%c", static_cast<char>(c));
				wrefresh(Console::InputWindow);
			}
			break;
		}

		return NULL;
	}

	void Console::Destroy()
	{
		delwin(Console::OutputWindow);
		delwin(Console::InputWindow);
		delwin(Console::InfoWindow);
		endwin();

		Console::OutputWindow = nullptr;
		Console::InputWindow = nullptr;
		Console::InfoWindow = nullptr;
	}

	void Console::Create()
	{
		Console::OutputTop = 0;
		Console::OutBuffer = 0;
		Console::LastRefresh = 0;
		Console::LineBufferIndex = 0;
		Console::HasConsole = false;

		CONSOLE_SCREEN_BUFFER_INFO info;
		if (GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info))
		{
			Console::Width = info.dwSize.X;
			Console::Height = info.srWindow.Bottom - info.srWindow.Top + 1;
		}
		else
		{
			Console::Height = 25;
			Console::Width = 80;
		}

		initscr();
		raw();
		noecho();

		Console::OutputWindow = newpad(OUTPUT_HEIGHT, Console::Width);
		Console::InputWindow = newwin(1, Console::Width, Console::Height - 1, 0);
		Console::InfoWindow = newwin(1, Console::Width, 0, 0);

		scrollok(Console::OutputWindow, true);
		scrollok(Console::InputWindow, true);
		nodelay(Console::InputWindow, true);
		keypad(Console::InputWindow, true);

		if (has_colors())
		{
			start_color();
			init_pair(1, COLOR_BLACK, COLOR_WHITE);
			init_pair(2, COLOR_WHITE, COLOR_BLACK);
			init_pair(3, COLOR_RED, COLOR_BLACK);
			init_pair(4, COLOR_GREEN, COLOR_BLACK);
			init_pair(5, COLOR_YELLOW, COLOR_BLACK);
			init_pair(6, COLOR_BLUE, COLOR_BLACK);
			init_pair(7, COLOR_CYAN, COLOR_BLACK);
			init_pair(8, COLOR_RED, COLOR_BLACK);
			init_pair(9, COLOR_WHITE, COLOR_BLACK);
			init_pair(10, COLOR_WHITE, COLOR_BLACK);
		}

		wbkgd(Console::InfoWindow, COLOR_PAIR(1));

		wrefresh(Console::InfoWindow);
		wrefresh(Console::InputWindow);

		Console::RefreshOutput();
	}

	void Console::Error(const char* format, ...)
	{
		static char buffer[32768];

		va_list va;
		va_start(va, format);
		_vsnprintf_s(buffer, sizeof(buffer), format, va);
		va_end(va);

		Game::Com_Printf(0, "ERROR:\n");
		Game::Com_Printf(0, buffer);

		Console::RefreshOutput();

		if (IsDebuggerPresent())
		{
			while (true)
			{
				std::this_thread::sleep_for(5s);
			}
		}

		TerminateProcess(GetCurrentProcess(), 0xDEADDEAD);
	}

	void Console::Print(const char* message)
	{
		if (!Console::OutputWindow) return;

		const char* p = message;
		while (*p != '\0')
		{
			if (*p == '\n')
			{
				Console::ScrollOutput(1);
			}

			if (*p == '^')
			{
				char color;
				++p;

				color = (*p - '0');

				if (color < 9 && color > 0)
				{
					wattron(Console::OutputWindow, COLOR_PAIR(color + 2));
					++p;
					continue;
				}
			}

			waddch(Console::OutputWindow, *p);

			++p;
		}

		wattron(Console::OutputWindow, COLOR_PAIR(9));

// 		int currentTime = static_cast<int>(GetTickCount64()); // Make our compiler happy
// 
// 		if (!Console::HasConsole)
// 		{
// 			Console::RefreshOutput();
// 		}
// 		else if ((currentTime - Console::LastRefresh) > 100)
// 		{
// 			Console::RefreshOutput();
// 			Console::LastRefresh = currentTime;
// 		}

		Console::RefreshOutput();
	}

	void Console::ConsoleRunner()
	{
		Game::Sys_ShowConsole();

		MSG message;
		while (IsWindow(*reinterpret_cast<HWND*>(0x64A3288)) != FALSE && GetMessageA(&message, 0, 0, 0))
		{
			TranslateMessage(&message);
			DispatchMessageA(&message);
		}

		if (Game::Sys_Milliseconds() - Console::LastRefresh > 100 &&
			MessageBoxA(0, "The application is not responding anymore, do you want to force its termination?", "Application is not responding", MB_ICONEXCLAMATION | MB_YESNO) == IDYES)
		{
			// Force process termination
			// if the main thread is not responding
			OutputDebugStringA("Process termination forced, as the main thread is not responding!");

			// We can not force the termination in this thread
			// The destructor would be called in this thread
			// and would try to join this thread, which is impossible
			std::async([] ()
			{
				std::this_thread::sleep_for(200ms);
				ExitProcess(static_cast<uint32_t>(-1));
			});
		}
		else
		{
			// Send quit command to safely terminate the application
			Command::Execute("wait 200;quit\n", false);
		}
	}

	void Console::StdOutPrint(const char* message)
	{
		printf("%s", message);
		fflush(stdout);
	}

	void Console::StdOutError(const char* format, ...)
	{
		char buffer[0x1000] = { 0 };

		va_list ap;
		va_start(ap, format);
		_vsnprintf_s(buffer, sizeof(buffer), format, ap);
		va_end(ap);

		perror(buffer);
		fflush(stderr);

		ExitProcess(1);
	}

	__declspec(naked) void Console::DrawSolidConsoleStub()
	{
		__asm
		{
			pushad
			call Console::StoreSafeArea
			popad

			// We need esi preserved here, so we have to backup 'all' registers when storing the safearea
			call Game::Con_DrawSolidConsole

			call Console::RestoreSafeArea
			retn
		}
	}

	void Console::StoreSafeArea()
	{
		// Backup the original safe area
		Console::OriginalSafeArea = *Game::safeArea;

		// Apply new safe area and border
		float border = 6.0f;
		Game::safeArea->top = border;
		Game::safeArea->left = border;
		Game::safeArea->bottom = static_cast<float>(Renderer::Height()) - border;
		Game::safeArea->right = static_cast<float>(Renderer::Width()) - border;

		Game::safeArea->textHeight = static_cast<int>((Game::safeArea->bottom - Game::safeArea->top - (2 * Game::safeArea->fontHeight) - 24.0) / Game::safeArea->fontHeight);
		Game::safeArea->textWidth = static_cast<int>(Game::safeArea->right - Game::safeArea->left - 10.0f - 18.0);
	}

	void Console::RestoreSafeArea()
	{
		// Restore the initial safe area
		*Game::safeArea = Console::OriginalSafeArea;
	}

	Console::Console()
	{
		// Console '%s: %s> ' string
		Utils::Hook::Set<char*>(0x5A44B4, "IW4x: r" REVISION_STR "> ");

		// Internal console
		Utils::Hook(0x4F690C, Console::ToggleConsole, HOOK_CALL).Install()->Quick();
		Utils::Hook(0x4F65A5, Console::ToggleConsole, HOOK_JUMP).Install()->Quick();

		// Patch safearea for ingame-console
		Utils::Hook(0x5A50EF, Console::DrawSolidConsoleStub, HOOK_CALL).Install()->Quick();

		// Check for bad food ;)
		Utils::Hook(0x4CB9F4, Console::GetAutoCompleteFileList, HOOK_CALL).Install()->Quick();

		// Modify console style
		Utils::Hook::Set<BYTE>(0x428A8E, 0);    // Adjust logo Y pos
		Utils::Hook::Set<BYTE>(0x428A90, 0);    // Adjust logo X pos
		Utils::Hook::Set<BYTE>(0x428AF2, 67);   // Adjust output Y pos
		Utils::Hook::Set<DWORD>(0x428AC5, 397); // Adjust input Y pos
		Utils::Hook::Set<DWORD>(0x428951, 609); // Reduce window width
		Utils::Hook::Set<DWORD>(0x42895D, 423); // Reduce window height
		Utils::Hook::Set<DWORD>(0x428AC0, 597); // Reduce input width
		Utils::Hook::Set<DWORD>(0x428AED, 596); // Reduce output width

		// Don't resize the console
		Utils::Hook(0x64DC6B, 0x64DCC2, HOOK_JUMP).Install()->Quick(); 

		// Code below is not necessary when performing unit tests!
		if (Loader::PerformingUnitTests()) return;

		// External console
		if (Flags::HasFlag("stdout"))
		{
			Utils::Hook(0x4B2080, Console::StdOutPrint, HOOK_JUMP).Install()->Quick();
			Utils::Hook(0x43D570, Console::StdOutError, HOOK_JUMP).Install()->Quick();
		}
		else if (Flags::HasFlag("console") || ZoneBuilder::IsEnabled()) // ZoneBuilder uses the game's console, until the native one is adapted.
		{
			FreeConsole();
			Utils::Hook::Nop(0x60BB58, 11);

			Utils::Hook(0x60BB68, [] ()
			{
				Console::ConsoleThread = std::thread(Console::ConsoleRunner);
			}, HOOK_CALL).Install()->Quick();

			QuickPatch::OnFrame([] ()
			{
				Console::LastRefresh = Game::Sys_Milliseconds();
			});
		}
		else if (Dedicated::IsEnabled()/* || ZoneBuilder::IsEnabled()*/)
		{
			Utils::Hook::Nop(0x60BB58, 11);

			Utils::Hook(0x4305E0, Console::Create, HOOK_JUMP).Install()->Quick();
			Utils::Hook(0x4528A0, Console::Destroy, HOOK_JUMP).Install()->Quick();
			Utils::Hook(0x4B2080, Console::Print, HOOK_JUMP).Install()->Quick();
			Utils::Hook(0x43D570, Console::Error, HOOK_JUMP).Install()->Quick();
			Utils::Hook(0x4859A5, Console::Input, HOOK_CALL).Install()->Quick();
		}
		else
		{
			FreeConsole();
		}
	}

	Console::~Console()
	{
		if (Console::ConsoleThread.joinable())
		{
			Console::ConsoleThread.join();
		}
	}
}
