//===========================================================================//
// L4KEYLIGHT.cpp - Windows Dynamic Lighting keyboard mirror.
//
// COMPILED APART FROM THE ENGINE: /std:c++17, DEFAULT struct packing,
// conformance mode (see the per-file settings in Munga_L4.vcxproj).
// No engine headers may be included here - the engine compiles /Zp1
// and its types would take a different layout in this translation
// unit. The l4keylight.h interface is scalars only for that reason.
//
// Everything WinRT runs on a private worker thread: device watcher,
// keyboard claiming, and a 100 ms paint loop that mirrors the lamp
// bytes (same flash formula as the on-screen cockpit buttons in
// L4MFDVIEW, so the board and the screen blink in step).
//===========================================================================//

#include <windows.h>

#include <atomic>
#include <mutex>
#include <thread>
#include <vector>
#include <cstring>
#include <cstdio>

#include <winrt/base.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Devices.Enumeration.h>
#include <winrt/Windows.Devices.Lights.h>
#include <winrt/Windows.System.h>
#include <winrt/Windows.UI.h>

#pragma comment(lib, "windowsapp.lib")

#include "l4keylight.h"

namespace
{
	using namespace winrt;
	using namespace winrt::Windows::Devices::Enumeration;
	using namespace winrt::Windows::Devices::Lights;
	using winrt::Windows::UI::Color;
	using winrt::Windows::System::VirtualKey;

	struct KeyEntry
	{
		int		virtualKey;
		int		address;
		bool	yellow;
	};

	struct ClaimedArray
	{
		hstring		id;
		LampArray	array{ nullptr };
		bool		perKey = false;
		bool		baseCoated = false;
	};

	std::mutex				gLock;
	std::vector<KeyEntry>	gMap;
	unsigned char			gLamps[64] = {};
	void					(*gLogger)(const char *) = nullptr;

	std::atomic<bool>		gRunning{ false };
	std::thread				gWorker;

	void Log(const char *line)
	{
		void (*logger)(const char *);
		{
			std::lock_guard<std::mutex> hold(gLock);
			logger = gLogger;
		}
		if (logger != nullptr)
		{
			logger(line);
		}
	}

	void Logf(const char *format, ...)
	{
		char line[256];
		va_list args;
		va_start(args, format);
		_vsnprintf_s(line, sizeof(line), _TRUNCATE, format, args);
		va_end(args);
		Log(line);
	}

	//---------------------------------------------------------------
	// Lamp byte -> brightness level 0-3, animating the flash modes.
	// Identical to LampLevel in L4MFDVIEW.cpp so the keyboard and the
	// on-screen buttons blink together.
	//---------------------------------------------------------------
	int LampLevel(int lamp_state)
	{
		int mode = lamp_state & 0x03;
		int level1 = (lamp_state >> 2) & 0x03;
		int level2 = (lamp_state >> 4) & 0x03;

		if (mode == 0)
		{
			return level1;
		}

		static const int half_period[4] = { 0, 500, 250, 125 };
		return ((GetTickCount() / half_period[mode]) & 1) ? level2 : level1;
	}

	//---------------------------------------------------------------
	// The panel palette (vRIO's KeyboardLampMirror shades): red for
	// the banks, yellow for Secondary/Screen; the off shade keeps the
	// bound keys faintly visible so the board reads as a button field.
	//---------------------------------------------------------------
	Color Shade(int level, bool yellow)
	{
		Color color;
		color.A = 255;
		if (yellow)
		{
			if (level >= 3)			{ color.R = 245; color.G = 210; color.B = 60; }
			else if (level >= 1)	{ color.R = 140; color.G = 118; color.B = 38; }
			else					{ color.R = 70;  color.G = 60;  color.B = 24; }
		}
		else
		{
			if (level >= 3)			{ color.R = 230; color.G = 70;  color.B = 70; }
			else if (level >= 1)	{ color.R = 120; color.G = 50;  color.B = 50; }
			else					{ color.R = 64;  color.G = 40;  color.B = 40; }
		}
		return color;
	}

	bool SameColor(const Color &a, const Color &b)
	{
		return a.A == b.A && a.R == b.R && a.G == b.G && a.B == b.B;
	}

	//---------------------------------------------------------------
	// The worker: watcher + claim + paint loop
	//---------------------------------------------------------------
	void Worker()
	{
		try
		{
			init_apartment();
		}
		catch (...)
		{
			// apartment already set on this thread; carry on
		}

		std::mutex claimedLock;
		std::vector<ClaimedArray> claimed;
		bool anySeen = false;

		DeviceWatcher watcher{ nullptr };
		try
		{
			watcher = DeviceInformation::CreateWatcher(LampArray::GetDeviceSelector());

			watcher.Added([&](DeviceWatcher const &, DeviceInformation const &info)
			{
				try
				{
					LampArray array = LampArray::FromIdAsync(info.Id()).get();
					if (array == nullptr ||
						array.LampArrayKind() != LampArrayKind::Keyboard)
					{
						return;		// mice / strips / cases stay untouched
					}
					anySeen = true;
					ClaimedArray entry;
					entry.id = info.Id();
					entry.array = array;
					entry.perKey = array.SupportsVirtualKeys();
					{
						std::lock_guard<std::mutex> hold(claimedLock);
						claimed.push_back(entry);
					}
					Logf(entry.perKey
						? "KeyLight: + %ls (%d LEDs, per-key)"
						: "KeyLight: + %ls (%d zones - board-wide mirror)",
						info.Name().c_str(), (int) array.LampCount());
				}
				catch (...)
				{
					Log("KeyLight: could not open a lamp array");
				}
			});
			watcher.Removed([&](DeviceWatcher const &, DeviceInformationUpdate const &update)
			{
				std::lock_guard<std::mutex> hold(claimedLock);
				for (size_t i = 0; i < claimed.size(); ++i)
				{
					if (claimed[i].id == update.Id())
					{
						claimed.erase(claimed.begin() + i);
						Log("KeyLight: keyboard disconnected");
						break;
					}
				}
			});
			watcher.Updated([](DeviceWatcher const &, DeviceInformationUpdate const &)
			{
				// required for the watcher to progress
			});
			watcher.Start();
		}
		catch (...)
		{
			Log("KeyLight: Dynamic Lighting unavailable on this system");
			return;
		}

		//
		// Paint loop: 100 ms cadence, repaint only on change
		//
		std::vector<Color> lastColors;
		int waited = 0;
		while (gRunning.load())
		{
			Sleep(50);
			waited += 50;
			if (waited < 100)
			{
				continue;
			}
			waited = 0;

			std::vector<KeyEntry> map;
			unsigned char lamps[64];
			{
				std::lock_guard<std::mutex> hold(gLock);
				map = gMap;
				memcpy(lamps, gLamps, sizeof(lamps));
			}

			std::vector<Color> colors(map.size());
			std::vector<VirtualKey> keys(map.size());
			int bestLevel = 0;
			bool bestYellow = false;
			bool changed = (lastColors.size() != map.size());
			for (size_t i = 0; i < map.size(); ++i)
			{
				int address = map[i].address;
				int level = (address >= 0 && address < 64)
					? LampLevel(lamps[address]) : 0;
				if (level > bestLevel)
				{
					bestLevel = level;
					bestYellow = map[i].yellow;
				}
				colors[i] = Shade(level, map[i].yellow);
				keys[i] = (VirtualKey) map[i].virtualKey;
				if (!changed && !SameColor(colors[i], lastColors[i]))
				{
					changed = true;
				}
			}

			std::lock_guard<std::mutex> hold(claimedLock);
			bool freshClaim = false;
			for (ClaimedArray &entry : claimed)
			{
				if (!entry.baseCoated)
				{
					freshClaim = true;
				}
			}
			if (!changed && !freshClaim)
			{
				continue;
			}

			Color aggregate = Shade(bestLevel, bestYellow);
			for (ClaimedArray &entry : claimed)
			{
				try
				{
					if (entry.perKey)
					{
						if (!entry.baseCoated)
						{
							Color black;
							black.A = 255; black.R = 0; black.G = 0; black.B = 0;
							entry.array.SetColor(black);
							entry.baseCoated = true;
						}
						if (!map.empty())
						{
							entry.array.SetColorsForKeys(
								array_view<Color const>(colors.data(), colors.data() + colors.size()),
								array_view<VirtualKey const>(keys.data(), keys.data() + keys.size()));
						}
					}
					else
					{
						entry.baseCoated = true;
						entry.array.SetColor(aggregate);
					}
				}
				catch (...)
				{
					// device wobble; the watcher handles removal
				}
			}
			lastColors = colors;
		}

		//
		// Releasing the arrays hands the LEDs back to Windows
		//
		try
		{
			watcher.Stop();
		}
		catch (...)
		{
		}
		{
			std::lock_guard<std::mutex> hold(claimedLock);
			claimed.clear();
		}
		if (!anySeen)
		{
			Log("KeyLight: no Dynamic Lighting keyboard was found this session");
		}
	}
}

//########################################################################
// The scalar interface (safe across the packing boundary)
//########################################################################

void
	KeyLight_SetLogger(void (*logger)(const char *line))
{
	std::lock_guard<std::mutex> hold(gLock);
	gLogger = logger;
}

void
	KeyLight_SetMap(
		const int *virtual_keys,
		const int *addresses,
		const unsigned char *yellow,
		int count
	)
{
	std::lock_guard<std::mutex> hold(gLock);
	gMap.clear();
	gMap.reserve(count);
	for (int i = 0; i < count; ++i)
	{
		KeyEntry entry;
		entry.virtualKey = virtual_keys[i];
		entry.address = addresses[i];
		entry.yellow = (yellow[i] != 0);
		gMap.push_back(entry);
	}
}

void
	KeyLight_UpdateLamps(const unsigned char *lamp_state, int count)
{
	if (count > 64)
	{
		count = 64;
	}
	std::lock_guard<std::mutex> hold(gLock);
	memcpy(gLamps, lamp_state, count);
}

void
	KeyLight_Start()
{
	if (gRunning.exchange(true))
	{
		return;
	}
	gWorker = std::thread(Worker);
}

void
	KeyLight_Stop()
{
	if (!gRunning.exchange(false))
	{
		return;
	}
	if (gWorker.joinable())
	{
		gWorker.join();
	}
}
