// Win32 console Platform implementation.
#pragma comment(lib, "user32.lib")

// Since we need std::min() and std::max()...
#define NOMINMAX

#include <numeric>
#include <sstream>
#include <vector>
#include <windows.h>
#include "Platform_Win32Console.hpp"

void fatal(std::wstring_view message)
{
	MessageBoxW(nullptr, message.data(), L"Error", MB_OK | MB_ICONERROR);
	ExitProcess(1);
}

void fatal_from_lasterror(
	std::string_view file, unsigned int line, DWORD lasterror
)
{
	auto fail = [lasterror]() {
		std::wstring message = L"Error retrieving error string for error " +
			std::to_wstring(lasterror) +
			L"?!";
		fatal(message);
	};

	LPWSTR formatted = nullptr;
	DWORD flags = (
		FORMAT_MESSAGE_ALLOCATE_BUFFER |
		FORMAT_MESSAGE_FROM_SYSTEM |
		FORMAT_MESSAGE_IGNORE_INSERTS
	);
	auto codepoints = FormatMessageW(
		flags,
		nullptr,
		lasterror,
		0,
		reinterpret_cast<LPWSTR>(&formatted),
		0,
		nullptr
	);
	if(codepoints == 0) {
		fail();
	}

	// std::wstring_convert is deprecated... So, MultiByteToWideChar it is.
	int file_len = static_cast<int>(file.size());
	int file_utf16_len = MultiByteToWideChar(
		CP_UTF8, 0, file.data(), file_len, nullptr, 0
	);
	if(file_utf16_len == 0) {
		fail();
	}

	// The terminating '\0' is already included here.
	std::wstring file_utf16(file_utf16_len, '\0');
	if(!MultiByteToWideChar(
		CP_UTF8, 0, file.data(), file_len, file_utf16.data(), file_utf16_len
	)) {
		fail();
	}

	std::wstringstream message;
	message << file_utf16 << ":" << std::to_wstring(line) << ": " << formatted;
	fatal(message.str());
}

Platform_Win32Console::Platform_Win32Console(void *handle_out, void *handle_in)
	: handle_out(handle_out), handle_in(handle_in) {
}

std::unordered_set<char> Platform_Win32Console::get_input(void)
{
	std::unordered_set<char> ret;

	DWORD events_available;
	if(!GetNumberOfConsoleInputEvents(handle_in, &events_available)) {
		fatal_from_lasterror(__FILE__, __LINE__, GetLastError());
	}

	// ReadConsoleInput blocks if we have no events. So, don't even call it
	// in this case.
	if(events_available == 0) {
		return ret;
	}

	std::vector<INPUT_RECORD> input_records(events_available);

	DWORD events_read;
	if(!ReadConsoleInput(
		handle_in, input_records.data(), events_available, &events_read
	)) {
		fatal_from_lasterror(__FILE__, __LINE__, GetLastError());
	}

	for(auto const &record : input_records) {
		if(record.EventType == KEY_EVENT) {
			auto const &key_event = record.Event.KeyEvent;
			if(key_event.bKeyDown) {
				ret.insert(record.Event.KeyEvent.uChar.AsciiChar);
			}
		}
	}
	return ret;
}

void Platform_Win32Console::render(PixelBuffer const &buffer)
{
	if(buffer.h() & 0x1) {
		fatal(L"Rendered PixelBuffers must have a height divisible by 2!");
	}

	COORD con_area = {
		static_cast<short>(buffer.w()), static_cast<short>(buffer.h() / 2)
	};
	auto con_buffer_size = (con_area.X * con_area.Y);

	// Additonally keep the buffer from the previous frame, so that we're able
	// to only render the differences.
	static std::array<std::vector<CHAR_INFO>, 2> chars;
	static bool page = true;

	page = !page;
	auto &chars_cur = chars[page];
	auto &chars_prev = chars[!page];

	bool rerender_everything = false;
	if(chars_cur.size() != con_buffer_size) {
		// U+2580 UPPER HALF BLOCK
		chars_cur.resize(con_buffer_size, { { 0x2580 }, 0 });

		// Will be resized next frame
		rerender_everything = (chars_prev.size() != con_buffer_size);

		// Resize console window and buffer
		CONSOLE_SCREEN_BUFFER_INFOEX csbi = { sizeof(csbi) };
		GetConsoleScreenBufferInfoEx(handle_out, &csbi);
		COORD buffer_size = {
			static_cast<SHORT>(con_area.X + 1),
			static_cast<SHORT>(con_area.Y + 1)
		};
		SMALL_RECT region = { 0, 0, con_area.X, con_area.Y };
		SetConsoleWindowInfo(handle_out, true, &region);
		SetConsoleScreenBufferSize(handle_out, buffer_size);
	}

	for(auto const &it : buffer) {
		auto color_value = it.peek().to_value().value_or(0xFF);
		auto char_i = (((it.pos().y / 2) * buffer.w()) + it.pos().x);
		auto is_upper = ((it.pos().y & 0x1) == 0);

		if(is_upper) {
			chars_cur[char_i].Attributes =
				((color_value & 0x1) ? FOREGROUND_INTENSITY : 0) |
				((color_value & 0x2) ? FOREGROUND_RED : 0) |
				((color_value & 0x4) ? FOREGROUND_GREEN : 0) |
				((color_value & 0x8) ? FOREGROUND_BLUE : 0);
		} else {
			chars_cur[char_i].Attributes |=
				((color_value & 0x1) ? BACKGROUND_INTENSITY : 0) |
				((color_value & 0x2) ? BACKGROUND_RED : 0) |
				((color_value & 0x4) ? BACKGROUND_GREEN : 0) |
				((color_value & 0x8) ? BACKGROUND_BLUE : 0);
		}
	}

	SMALL_RECT region = { 0, 0, con_area.X, con_area.Y };
	if(!rerender_everything) {
		region.Left = con_area.X;
		region.Top = con_area.Y;
		region.Right = 0;
		region.Bottom = 0;
		for(SHORT y = 0; y < con_area.Y; y++) {
			for(SHORT x = 0; x < con_area.X; x++) {
				auto i = ((y * buffer.w()) + x);
				if(chars_cur[i].Attributes != chars_prev[i].Attributes) {
					region.Left = std::min(region.Left, x);
					region.Top = std::min(region.Top, y);
					region.Right = std::max(region.Right, x);
					region.Bottom = std::max(region.Bottom, y);
				}
			}
		}
	}
	COORD buffer_coord = { region.Left, region.Top };
	WriteConsoleOutputW(
		handle_out, chars_cur.data(), con_area, buffer_coord, &region
	);
}

void Platform_Win32Console::set_title(std::string const &title)
{
	SetConsoleTitleA(title.c_str());
}

Platform_Win32Console Platform_Win32Console::init()
{
	// Make sure to create a dedicated window, even if we launch from a shell
	FreeConsole(); // We don't care if this fails
	if(!AllocConsole()) {
		fatal_from_lasterror(__FILE__, __LINE__, GetLastError());
	}

	auto handle_out = GetStdHandle(STD_OUTPUT_HANDLE);
	if(handle_out == INVALID_HANDLE_VALUE) {
		fatal_from_lasterror(__FILE__, __LINE__, GetLastError());
	}

	auto handle_in = GetStdHandle(STD_INPUT_HANDLE);
	if(handle_in == INVALID_HANDLE_VALUE) {
		fatal_from_lasterror(__FILE__, __LINE__, GetLastError());
	}

	CONSOLE_FONT_INFOEX cfi = { sizeof(CONSOLE_FONT_INFOEX) };
	if(!GetCurrentConsoleFontEx(handle_out, false, &cfi)) {
		fatal_from_lasterror(__FILE__, __LINE__, GetLastError());
	}

	// Make sure to enforce a default Windows font that actually has U+2580
	wcscpy_s(cfi.FaceName, L"Lucida Console");

	cfi.dwFontSize.X = 4;
	cfi.dwFontSize.Y = 4;
	if(!SetCurrentConsoleFontEx(handle_out, false, &cfi)) {
		fatal_from_lasterror(__FILE__, __LINE__, GetLastError());
	}

	CONSOLE_CURSOR_INFO cursor_info = { 1, false };
	SetConsoleCursorInfo(handle_out, &cursor_info);

	return { handle_out, handle_in };
}