#include <RumPi/RumPi.h>
#include <RumPi/Data/SelectedSensorReadings.h>
#include <RumPi/AlertManager.h>
#include <wx/filename.h>
#include <wx/utils.h>
#include "../../Headers/json.hpp"
#include "../../Headers/GUI/MainWindow.h"
#include "../../Headers/RumPiManagerThread.h"
#include "../../Headers/ClientUtilities.h"

wxDEFINE_EVENT(wxEVT_RUMPI_UPDATE, wxThreadEvent);

/**
* Builds the main window: sets up the menu/status bar and notebook, creates the alert forwarder, and starts the
* background poll thread (surfacing a failure to start through the alert channel rather than a modal popup).
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	MainWindow* window = new MainWindow(nullptr, wxID_ANY, "MainWindow");
*	window->Show(true);
* @endcode
*
* @param [in] parent The parent window (nullptr for a top-level frame).
* @param [in] id     The window identifier.
* @param [in] title  The window title.
* @param [in] pos    The initial position.
* @param [in] size   The initial size.
* @param [in] style  The frame style flags.
*/
MainWindow::MainWindow(wxWindow* parent, wxWindowID id, const wxString& title, const wxPoint& pos, const wxSize& size, long style) : wxFrame(parent, id, title, pos, size, style)
{
	myRumPiManagerThread = nullptr;
	myLoggerEventForwarder = nullptr;
	myLoggerListDataModel = nullptr;

	this->SetSizeHints(wxDefaultSize, wxDefaultSize);

	ConstructMenuAndStatusBar();

	//Forward every alert into the Logger tab (in addition to the library's file/console logging). The forwarder
	//owns the AlertManager subscription and marshals each event to us on the main thread. Created BEFORE
	//ConstructNotebook so alerts fired during panel construction (e.g. a HomeScenePanel scene-load failure) are
	//captured too - OnLoggerUpdate is already bound above and events are queued, so they arrive after the logger
	//model is built. Torn down in TeardownBackgroundWork(), after the worker is stopped, so no alert can fire into
	//it as it unsubscribes.
	myLoggerEventForwarder = new LoggerEventForwarder(this);

	ConstructNotebook();

	//Start the background worker that polls RumPi. Keep the pointer so OnClose can stop and free it before the
	//window is torn down - it holds a pointer to us and posts events to us, so it must not outlive us.
	myRumPiManagerThread = new RumPiManagerThread(this);
	if (myRumPiManagerThread->Run() != wxTHREAD_NO_ERROR)
	{
		//The poll thread feeds every tab, so surface the failure through the same alert channel the rest of
		//the app uses (Logger tab + log file) rather than a modal popup that could trap an unattended kiosk.
		RumPi::AlertManager::GetInstance().Report(RumPi::EAlertLevel::Alert, "Client MainWindow: failed to start the RumPiManagerThread; live data will be unavailable.");

		//Run() never started the thread, so there is nothing to stop; free the object and clear the pointer.
		delete myRumPiManagerThread;
		myRumPiManagerThread = nullptr;
	}
}

/**
* Destroys the window, tearing down the background work as a safety net for paths that bypass OnClose (null-safe
* and idempotent).
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Runs when the window is destroyed.
* @endcode
*/
MainWindow::~MainWindow()
{
	//Safety net for teardown paths that bypass OnClose. TeardownBackgroundWork() is null-safe and idempotent,
	//so if OnClose already ran this is a no-op.
	TeardownBackgroundWork();
}

/**
* Builds the menu bar and status bar and binds the window's menu, update, logger, and close events.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Called by the constructor.
* @endcode
*/
void MainWindow::ConstructMenuAndStatusBar()
{
	wxMenu* menuFile = new wxMenu();
	menuFile->Append(wxID_EXIT);

	wxMenu* menuHelp = new wxMenu();
	menuHelp->Append(ID_SHOW_LOGS, "&Show Log Files...");
	menuHelp->AppendSeparator();
	menuHelp->Append(wxID_ABOUT);

	wxMenuBar* menuBar = new wxMenuBar;
	menuBar->Append(menuFile, "&File");
	menuBar->Append(menuHelp, "&Help");

	SetMenuBar(menuBar);

	CreateStatusBar();
	SetStatusText("Welcome to RumPi!");

	//Bind events for the top bar menus (File, Exit, etc).
	Bind(wxEVT_MENU, &MainWindow::OnAbout, this, wxID_ABOUT);
	Bind(wxEVT_MENU, &MainWindow::OnExit, this, wxID_EXIT);
	Bind(wxEVT_MENU, &MainWindow::OnShowLogs, this, ID_SHOW_LOGS);

	//Bind event for getting RumPi data from it's update thread.
	Bind(wxEVT_RUMPI_UPDATE, &MainWindow::OnRumPiUpdate, this);

	//Bind event for appending log events to the Logger tab (posted by the AlertManager subscriber).
	Bind(wxEVT_RUMPI_LOGGER_UPDATE, &MainWindow::OnLoggerUpdate, this);

	//Stop the background worker before the frame is destroyed - it posts events to us.
	Bind(wxEVT_CLOSE_WINDOW, &MainWindow::OnClose, this);
}

/**
* Builds the notebook and its five tabs (Home, Summary, Devices, Logger, Settings) and adds it to the window layout.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Called by the constructor.
* @endcode
*/
void MainWindow::ConstructNotebook()
{
	wxBoxSizer* layoutBoxSizer;
	layoutBoxSizer = new wxBoxSizer(wxVERTICAL);

	//Create the notebook that contains the tabs and the panels.
	myNotebook = new wxNotebook(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);

	//Create the home panel (the stylized backdrop) and add it to the notebook.
	ConstructHomePanel();

	//Create the summary panel (the at-a-glance sensor tiles) and add it to the notebook.
	ConstructSummaryPanel();

	//Create the devices panel and add it to the notebook.
	ConstructDevicesPanel();

	//Create the logger panel and add it to the notebook.
	ConstructLoggerPanel();

	//Create the settings panel and add it to the notebook.
	ConstructSettingsPanel();

	//Add the notebook to the root layout.
	layoutBoxSizer->Add(myNotebook, 1, wxEXPAND | wxALL, 5);

	this->SetSizer(layoutBoxSizer);
	this->Layout();

	//Center this window on the parent.
	this->Centre(wxBOTH);
}

/**
* Builds the Home tab's condition-reactive scene panel and adds it to the notebook.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Called by ConstructNotebook().
* @endcode
*/
void MainWindow::ConstructHomePanel()
{
	//The Home tab is the stylized, condition-reactive backdrop. HomeScenePanel owns the drawing; the
	//at-a-glance sensor tiles live on the Summary tab (see ConstructSummaryPanel). The scene crossfades the
	//day/night base art by local time and paints the reading-driven overlays (clouds, haze, temperature tint,
	//mist, wind) on top.
	myHomeScenePanel = new HomeScenePanel(myNotebook);
	myNotebook->AddPage(myHomeScenePanel, _("Home"), false);
}

/**
* Builds the Summary tab: a three-per-row grid of at-a-glance sensor tiles, stashing each tile's value label for
* live updates.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Called by ConstructNotebook().
* @endcode
*/
void MainWindow::ConstructSummaryPanel()
{
	//The Summary tab holds the at-a-glance sensor tiles (this was previously the Home tab's content).
	mySummaryPanel = new wxPanel(myNotebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
	myNotebook->AddPage(mySummaryPanel, _("Summary"), false);

	//A grid of at-a-glance tiles, three per row, that grows to fill the panel. Each CreateSummaryTile call
	//builds a titled tile and hands back its value label so UpdateSummaryPanel can refresh it live.
	wxGridSizer* tileGridSizer = new wxGridSizer(0, 3, 10, 10);

	myTemperatureValue    = CreateSummaryTile(mySummaryPanel, tileGridSizer, _("Temperature"));
	myHumidityValue       = CreateSummaryTile(mySummaryPanel, tileGridSizer, _("Humidity"));
	myAirQualityValue     = CreateSummaryTile(mySummaryPanel, tileGridSizer, _("Air Quality"));
	myPmTwoPointFiveValue = CreateSummaryTile(mySummaryPanel, tileGridSizer, _("PM2.5"));
	myTVOCValue           = CreateSummaryTile(mySummaryPanel, tileGridSizer, _("TVOC"));
	myFormaldehydeValue   = CreateSummaryTile(mySummaryPanel, tileGridSizer, _("Formaldehyde"));
	myCO2Value            = CreateSummaryTile(mySummaryPanel, tileGridSizer, _("CO2"));
	myMotionValue         = CreateSummaryTile(mySummaryPanel, tileGridSizer, _("Motion"));
	myWindSpeedValue      = CreateSummaryTile(mySummaryPanel, tileGridSizer, _("Wind Speed"));
	myLightValue          = CreateSummaryTile(mySummaryPanel, tileGridSizer, _("Light"));

	//Wrap the grid so it sits with a margin inside the panel.
	wxBoxSizer* outerSizer = new wxBoxSizer(wxVERTICAL);
	outerSizer->Add(tileGridSizer, 1, wxEXPAND | wxALL, 10);
	mySummaryPanel->SetSizer(outerSizer);
	mySummaryPanel->Layout();
}

/**
* Builds one titled summary tile with a large centered value label and adds it to the grid.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	wxStaticText* label = CreateSummaryTile(mySummaryPanel, tileGridSizer, "Temperature");
* @endcode
*
* @param [in] parent        The parent window for the tile.
* @param [in] tileGridSizer The grid sizer to add the tile to.
* @param [in] title         The tile's title.
*
* @return The tile's value label, for later live updates.
*/
wxStaticText* MainWindow::CreateSummaryTile(wxWindow* parent, wxSizer* tileGridSizer, const wxString& title)
{
	//Each tile is a titled box with a large value label centered inside it.
	wxStaticBoxSizer* tileSizer = new wxStaticBoxSizer(wxVERTICAL, parent, title);

	//The value label starts as "N/A" until the first snapshot arrives.
	wxStaticText* valueLabel = new wxStaticText(tileSizer->GetStaticBox(), wxID_ANY, "N/A", wxDefaultPosition, wxDefaultSize, wxALIGN_CENTRE_HORIZONTAL);

	//Make the reading stand out over the smaller box title.
	wxFont valueFont = valueLabel->GetFont();
	valueFont.SetPointSize(valueFont.GetPointSize() + 6);
	valueFont.SetWeight(wxFONTWEIGHT_BOLD);
	valueLabel->SetFont(valueFont);

	//Center the value vertically within the tile.
	tileSizer->AddStretchSpacer();
	tileSizer->Add(valueLabel, 0, wxALIGN_CENTER_HORIZONTAL | wxALL, 5);
	tileSizer->AddStretchSpacer();

	tileGridSizer->Add(tileSizer, 1, wxEXPAND | wxALL, 5);

	return valueLabel;
}

/**
* Refreshes every summary tile's value label from a sensor snapshot (temperature, humidity, gases, particulates,
* air quality, motion, wind, and light) and re-flows the tiles.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	UpdateSummaryPanel(readings);
* @endcode
*
* @param [in] readings The sensor snapshot to display.
*/
void MainWindow::UpdateSummaryPanel(const RumPi::SelectedSensorReadings& readings)
{
	//DHT11 (smoothed) temperature, and PTQS1005 humidity + gases.
	myTemperatureValue->SetLabel(ClientUtilities::FormatReading(readings.GetTemperatureCelsius(), "°C"));
	myHumidityValue->SetLabel(ClientUtilities::FormatReading(readings.GetHumidityPercent(), "%", 0));
	myTVOCValue->SetLabel(ClientUtilities::FormatReading(readings.GetTVOC(), "mg/m³", 3));
	myFormaldehydeValue->SetLabel(ClientUtilities::FormatReading(readings.GetFormaldehyde(), "mg/m³", 3));
	myCO2Value->SetLabel(ClientUtilities::FormatReading(readings.GetCO2(), "ppm", 0));

	//PMS5003 particulates, plus its air-quality category (a string, not a number).
	myPmTwoPointFiveValue->SetLabel(ClientUtilities::FormatReading(readings.GetPmTwoPointFive(), "µg/m³", 0));
	std::string airQuality = readings.GetAirQuality();
	myAirQualityValue->SetLabel(airQuality.empty() ? wxString("N/A") : wxString::FromUTF8(airQuality.c_str()));

	//HCSR501 motion (yes/no) and QSFS01 wind speed.
	myMotionValue->SetLabel(readings.GetIsMotionDetected() ? _("Detected") : _("Clear"));
	myWindSpeedValue->SetLabel(ClientUtilities::FormatReading(readings.GetWindSpeed(), "m/s", 1));

	//TSL2591 light - the category is the meaningful bit here; raw lux stays on the Devices tab.
	std::string lightCategory = readings.GetLightCategory();
	myLightValue->SetLabel(lightCategory.empty() ? wxString("N/A") : wxString::FromUTF8(lightCategory.c_str()));

	//Labels changed width, so re-flow the tiles to keep them centered.
	mySummaryPanel->Layout();
}

/**
* Builds the Devices tab: a scrollable, three-column grid that will hold one tile per component instance.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Called by ConstructNotebook().
* @endcode
*/
void MainWindow::ConstructDevicesPanel()
{
	wxBoxSizer* outerBoxSizer;

	//Create the devices page panel for the notebook
	myDevicesPanel = new wxPanel(myNotebook, wxID_ANY);
	myNotebook->AddPage(myDevicesPanel, _("Devices"), false);

	//Create the ScrolledWindow as a child of myDevicesPanel
	myDevicesScrollWindow = new wxScrolledWindow(myDevicesPanel, wxID_ANY);
	myDevicesScrollWindow->SetScrollRate(5, 5);

	//Create the GridSizer
	myDataViewGridSizer = new wxGridSizer(0, 3, 10, 10);

	//Assign the GridSizer to the ScrolledWindow
	myDevicesScrollWindow->SetSizer(myDataViewGridSizer);

	//Use a simple BoxSizer to make the ScrolledWindow fill myDevicesPanel
	outerBoxSizer = new wxBoxSizer(wxVERTICAL);
	outerBoxSizer->Add(myDevicesScrollWindow, 1, wxEXPAND);
	myDevicesPanel->SetSizer(outerBoxSizer);

	//Update the layout.
	myDevicesPanel->Layout();
}

/**
* Builds the Logger tab: a data-view control with icon/time/level/description columns, backed by the log model.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Called by ConstructNotebook().
* @endcode
*/
void MainWindow::ConstructLoggerPanel()
{
	myLoggerPanel = new wxPanel(myNotebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
	myNotebook->AddPage(myLoggerPanel, _("Logger"), false);

	//The data view control that shows the running list of log events.
	myLoggerDataViewCtrl = new wxDataViewCtrl(myLoggerPanel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxDV_ROW_LINES | wxDV_HORIZ_RULES);

	//Hand the model to the control. AssociateModel takes a reference of its own, so we release ours
	//(see DecRef) and let the control own the model for the rest of its life.
	myLoggerListDataModel = new LoggerListData();
	myLoggerDataViewCtrl->AssociateModel(myLoggerListDataModel);
	myLoggerListDataModel->DecRef();

	//Column 0: the severity icon.
	myLoggerDataViewCtrl->AppendBitmapColumn("", LoggerColumnID::E_COL_ALERTICON, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE);

	//Column 1: the timestamp.
	myLoggerDataViewCtrl->AppendTextColumn("Time", LoggerColumnID::E_COL_LOGTIME, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE);

	//Column 2: the alert level as text.
	myLoggerDataViewCtrl->AppendTextColumn("Alert Level", LoggerColumnID::E_COL_ALERTLEVEL, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE);

	//Column 3: the event description.
	myLoggerDataViewCtrl->AppendTextColumn("Description", LoggerColumnID::E_COL_EVENTDESCRIPTION, wxDATAVIEW_CELL_INERT, wxCOL_WIDTH_AUTOSIZE, wxALIGN_LEFT, wxDATAVIEW_COL_RESIZABLE);

	//Make the data view fill the panel.
	wxBoxSizer* loggerSizer = new wxBoxSizer(wxVERTICAL);
	loggerSizer->Add(myLoggerDataViewCtrl, 1, wxEXPAND | wxALL, 5);
	myLoggerPanel->SetSizer(loggerSizer);
}

/**
* Builds the (currently empty) Settings tab and adds it to the notebook.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Called by ConstructNotebook().
* @endcode
*/
void MainWindow::ConstructSettingsPanel()
{
	mySettingsPanel = new wxPanel(myNotebook, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
	myNotebook->AddPage(mySettingsPanel, _("Settings"), false);
}

/**
* Stops the background poll thread and then tears down the alert forwarder, in that order (stop the producer
* before the consumer). Null-safe and idempotent, so both OnClose and the destructor can call it.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	TeardownBackgroundWork();
* @endcode
*/
void MainWindow::TeardownBackgroundWork()
{
	//Order matters: stop the alert producer before the consumer.
	//
	//The worker is joinable, so Delete() signals it to leave its TestDestroy() loop and blocks until Entry()
	//returns; joinable threads don't self-delete, so we free the object ourselves. Once this returns no
	//component thread is running, so no alert can fire into the forwarder while we destroy it below.
	if (myRumPiManagerThread != nullptr)
	{
		myRumPiManagerThread->Delete();
		delete myRumPiManagerThread;
		myRumPiManagerThread = nullptr;
	}

	//The forwarder's destructor unsubscribes from the AlertManager. Deleting it only after the worker is
	//stopped guarantees no snapshotted callback is still mid-flight on another thread when it goes away.
	if (myLoggerEventForwarder != nullptr)
	{
		delete myLoggerEventForwarder;
		myLoggerEventForwarder = nullptr;
	}
}

/**
* Close handler: tears down the background work before the window is destroyed (the worker and forwarder both post
* events to us), then lets the default handler proceed.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Invoked by wxWidgets on a close request.
* @endcode
*
* @param [in] event The close event.
*/
void MainWindow::OnClose(wxCloseEvent& event)
{
	//Stop background work before the window is torn down: the worker and the forwarder both post events to us,
	//so a running producer racing a half-destroyed window is a crash. Then let the default handler destroy it.
	TeardownBackgroundWork();

	event.Skip();
}

/**
* Exit handler: closes the window.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Invoked by the File > Exit menu item.
* @endcode
*
* @param [in] event The command event.
*/
void MainWindow::OnExit(wxCommandEvent& event)
{
	Close(true);
}

/**
* About handler: shows the application's about dialog.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Invoked by the Help > About menu item.
* @endcode
*
* @param [in] event The command event.
*/
void MainWindow::OnAbout(wxCommandEvent& event)
{
	wxMessageBox("RumPi - a Raspberry Pi environment monitor.\n\nShows live readings from the Pi's sensors on a condition-reactive home scene, with at-a-glance tiles, a per-device view, and an event log.",
				 "About RumPi", wxOK | wxICON_INFORMATION);
}

/**
* "Show Log Files" handler: opens the log folder in the platform's file manager, falling back to a message box
* with the path if that isn't possible (e.g. a headless kiosk) or if no log path is available yet.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Invoked by the Help > Show Log Files menu item.
* @endcode
*
* @param [in] event The command event.
*/
void MainWindow::OnShowLogs(wxCommandEvent& event)
{
	wxString logDirectory;

	//Ask the library where it writes the log, so the path lives in one place (the library) rather than
	//being hardcoded here as well.
	wxString logFilePath = RumPi::GetLogFilePath();

	if (logFilePath.IsEmpty() == false)
	{
		//The folder that contains the log file.
		logDirectory = wxFileName(logFilePath).GetPath();

		//wxLaunchDefaultApplication opens the folder in the platform's default file manager (via xdg-open on Linux).
		//It returns false if that isn't possible - e.g. a headless/kiosk Pi with no file manager,
		//so we fall back to simply telling the user where the file lives.
		if (wxLaunchDefaultApplication(logDirectory) == false)
		{
			wxMessageBox("Logs are saved to:\n\n" + logFilePath, "Show Log Files", wxOK | wxICON_INFORMATION);
		}
	}
	else
	{
		wxMessageBox("The log file location is not available yet.", "Show Log Files", wxOK | wxICON_WARNING);
	}
}

/**
* RumPi-update handler (main thread): refreshes the Summary tiles and Home scene from the event's snapshot, then
* defensively parses the component JSON payload and creates/refreshes one device tile per named component instance.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Invoked on the main thread when the worker posts a wxEVT_RUMPI_UPDATE.
* @endcode
*
* @param [in] event The thread event carrying the snapshot and component JSON.
*/
void MainWindow::OnRumPiUpdate(wxThreadEvent& event)
{
	//The worker piggybacks the curated snapshot on this same event; feed it to both the Summary tiles and the Home scene backdrop.
	const RumPi::SelectedSensorReadings readings = event.GetPayload<RumPi::SelectedSensorReadings>();
	UpdateSummaryPanel(readings);
	myHomeScenePanel->Update(readings);

	//Guard the per-component refresh: it parses the payload, news ComponentPanels, and relayouts, so a bad_alloc
	//(or any other throw) must not escape this wx event handler and terminate the app. The typed Summary/Home
	//update above already happened, so on failure we just skip this tick's device tiles.
	try
	{
		RefreshDeviceTiles(event.GetString().ToStdString());
	}
	catch (...)
	{
	}
}

void MainWindow::RefreshDeviceTiles(const std::string& componentJson)
{
	RumPi::EComponentType componentType;
	ComponentPanel* theComponentPanel;
	std::string componentName;
	std::string componentDataStr;
	nlohmann::json componentDataJson;

	//Parse the component payload defensively - a malformed message would otherwise throw on the UI thread. The
	//Summary tiles and Home scene use the typed payload and are unaffected, so on a bad parse we just skip the
	//per-component refresh for this tick.
	try
	{
		componentDataJson = nlohmann::json::parse(componentJson);
	}
	catch (const nlohmann::json::exception& parseException)
	{
		RumPi::AlertManager::GetInstance().Report(RumPi::EAlertLevel::Warning, std::string("Client: failed to parse the component update - ") + parseException.what() + ".");
		return;
	}

	//Each entry is keyed by the component's unique name (so multiple of one type stay distinct) and carries its
	//type - for the icon - plus its serialized data. Walk the entries directly: one panel per named instance.
	//Panels are created on first sight and kept; components are static after Initialize, so none disappear here.
	//The payload is a JSON object of {name: {type, data}}. If it is ever not that shape, skip rather than letting
	//.key()/.value() throw out of this UI event handler (which would terminate the app).
	if (componentDataJson.is_object() == true)
	{
		for (nlohmann::json::const_iterator entryIterator = componentDataJson.begin(); entryIterator != componentDataJson.end(); ++entryIterator)
		{
			if (entryIterator.value().is_object() == true)
			{
				componentName = entryIterator.key();

				//Read type/data defensively: only when present AND a string. The is_object guards above cover the shape, but
				//a present-but-wrong-typed field (e.g. "type": 3) would still make .value() throw type_error.302 out of this
				//UI event handler and terminate the app.
				const nlohmann::json& entryValue = entryIterator.value();
				componentType = RumPi::BaseComponent::StringToComponentType((entryValue.contains("type") && entryValue["type"].is_string()) ? entryValue["type"].get<std::string>() : std::string(""));
				componentDataStr = (entryValue.contains("data") && entryValue["data"].is_string()) ? entryValue["data"].get<std::string>() : std::string("");

				//Add the panel the first time we see this name; afterwards just refresh its data.
				if (myComponentDataCollection.find(componentName) == myComponentDataCollection.end())
				{
					theComponentPanel = new ComponentPanel(myDevicesScrollWindow, ComponentData(componentType, componentName, componentDataStr, true));
					myComponentDataCollection.insert(std::make_pair(componentName, theComponentPanel));
					myDataViewGridSizer->Add(theComponentPanel, 1, wxEXPAND | wxALL, 5);
					myDevicesPanel->Layout();
				}
				else //Component exists so update its data.
				{
					myComponentDataCollection[componentName]->GetComponentData().SetData(componentDataStr);
				}

				//Update panel UI components (sizes the data field to its real content).
				myComponentDataCollection[componentName]->UpdateUI();
			}
		}

		//Re-fit the scroll window's virtual area once, AFTER every tile's UpdateUI has set its real height, so tiles
		//past the initial viewport (and any whose data grew) can be scrolled to. Doing this per-component inside the
		//loop above would relayout N times per tick for no benefit.
		myDevicesScrollWindow->Layout();
		myDevicesScrollWindow->FitInside();
	}
}

/**
* Logger-update handler (main thread): appends the event's log line to the logger model.
*
* @author Eddie O'Hagan
* @date 8/13/2026
*
* @code
*	//Invoked on the main thread when the forwarder posts a wxEVT_RUMPI_LOGGER_UPDATE.
* @endcode
*
* @param [in] event The thread event carrying the log line.
*/
void MainWindow::OnLoggerUpdate(wxThreadEvent& event)
{
	//Runs on the main thread, so it is safe to touch the model/control here.
	myLoggerListDataModel->AddToList(event.GetPayload<LoggerLineData>());
}
