/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2023, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#include "gui-precomp.h"  // Precompiled headers
//
#include <mrpt/config.h>
#include <mrpt/gui/CDisplayWindow.h>
#include <mrpt/gui/CDisplayWindow3D.h>
#include <mrpt/gui/CDisplayWindowPlots.h>
#include <mrpt/gui/WxSubsystem.h>
#include <mrpt/gui/WxUtils.h>
#include <mrpt/system/os.h>
#include <mrpt/system/thread_name.h>

//#define  WXSUBSYSTEM_VERBOSE

// ------------------------------------------------------------------------
// Defined: Try to wait for all windows & the thread to exit cleanly.
// Undefined: Just to a std::this_thread::sleep_for(ms) and quit crossing our
// fingers.
//
//  Problem with the "clean way" is: As of feb/2011, I get this error
//   at the end:
//  ** (MRPT:11711): CRITICAL **: giop_thread_request_push: assertion `tdata !=
//  NULL' failed
// ------------------------------------------------------------------------
//#define WXSHUTDOWN_DO_IT_CLEAN

#if MRPT_HAS_WXWIDGETS

using namespace mrpt;
using namespace mrpt::gui;
using namespace std;

class WxSubSystemGlobalData
{
   public:
	static WxSubSystemGlobalData& Instance()
	{
		static WxSubSystemGlobalData d;
		return d;
	}

	int windowCount = 0;
	std::mutex cs_windowCount;

	std::queue<WxSubsystem::TRequestToWxMainThread*> listPendingWxRequests;
	std::mutex cs_listPendingWxRequests;

   private:
	WxSubSystemGlobalData() = default;
};

volatile WxSubsystem::CWXMainFrame* WxSubsystem::CWXMainFrame::oneInstance =
	nullptr;
bool isConsoleApp_value = true;
bool WxSubsystem::isConsoleApp() { return isConsoleApp_value; }
WxSubsystem::CAuxWxSubsystemShutdowner WxSubsystem::global_wxsubsystem_shutdown;

// Auxiliary class implementation:
WxSubsystem::CAuxWxSubsystemShutdowner::CAuxWxSubsystemShutdowner() = default;
WxSubsystem::CAuxWxSubsystemShutdowner::~CAuxWxSubsystemShutdowner()
{
	if (WxSubsystem::isConsoleApp())
	{
#ifdef WXSUBSYSTEM_VERBOSE
		printf("[~CAuxWxSubsystemShutdowner] Sending 999...\n");
#endif
		// Shut down:
		try
		{
			auto* REQ = new WxSubsystem::TRequestToWxMainThread[1];
			REQ->OPCODE = 999;
			WxSubsystem::pushPendingWxRequest(REQ);

			// std::this_thread::sleep_for(100ms); // JL: I found no better way
			// of doing this, sorry :-(  See
			// WxSubsystem::waitWxShutdownsIfNoWindows()
			WxSubsystem::waitWxShutdownsIfNoWindows();
		}
		catch (...)
		{
		}  // Just in case we got an out-of-mem error.
	}  // is console app.

#ifdef WXSUBSYSTEM_VERBOSE
	printf("[~CAuxWxSubsystemShutdowner] Deleting static objects.\n");
#endif
}

// ---------------------------------------------------------------------------------------
// Auxiliary dialog class for the "ask user to open a camera":
// ---------------------------------------------------------------------------------------
class CDialogAskUserForCamera : public wxDialog
{
   public:
	mrpt::gui::CPanelCameraSelection* panel;

	static const long ID_BTN_OK;
	static const long ID_BTN_CANCEL;

	CDialogAskUserForCamera()
		: wxDialog(
			  nullptr, wxID_ANY, wxT("Select image source"), wxDefaultPosition,
			  wxDefaultSize, wxDEFAULT_DIALOG_STYLE, wxDialogNameStr)
	{
		auto* f1 = new wxFlexGridSizer(2, 1, 0, 0);
		panel = new mrpt::gui::CPanelCameraSelection(this, wxID_ANY);
		f1->Add(
			panel, 1, wxALL | wxALIGN_BOTTOM | wxALIGN_CENTER_HORIZONTAL, 5);

		auto* f2 = new wxFlexGridSizer(1, 2, 0, 0);
		wxButton* btnOk = new wxButton(
			this, ID_BTN_OK, wxT("Ok"), wxDefaultPosition, wxDefaultSize);
		wxButton* btnCancel = new wxButton(
			this, ID_BTN_CANCEL, wxT("Cancel"), wxDefaultPosition,
			wxDefaultSize);
		f1->Add(f2, 1, wxALL | wxALIGN_BOTTOM | wxALIGN_CENTER_HORIZONTAL, 5);

		f2->Add(
			btnOk, 1, wxALL | wxALIGN_BOTTOM | wxALIGN_CENTER_HORIZONTAL, 5);
		f2->Add(
			btnCancel, 1, wxALL | wxALIGN_BOTTOM | wxALIGN_CENTER_HORIZONTAL,
			5);

		Bind(wxEVT_BUTTON, &CDialogAskUserForCamera::OnBtnOk, this, ID_BTN_OK);
		Bind(
			wxEVT_BUTTON, &CDialogAskUserForCamera::OnBtnCancel, this,
			ID_BTN_CANCEL);

		SetSizer(f1);
		Fit();

		btnOk->SetFocus();	// So the default params can be accepted by just
		// pressing ENTER.
	}

	~CDialogAskUserForCamera() override = default;
	void OnBtnOk(wxCommandEvent& event) { EndModal(wxID_OK); }
	void OnBtnCancel(wxCommandEvent& event) { EndModal(wxID_CANCEL); }
};

const long CDialogAskUserForCamera::ID_BTN_OK = wxNewId();
const long CDialogAskUserForCamera::ID_BTN_CANCEL = wxNewId();

// ---------------------------------------------------------------------------------------
// The wx dummy frame:
// ---------------------------------------------------------------------------------------
BEGIN_EVENT_TABLE(WxSubsystem::CWXMainFrame, wxFrame)

END_EVENT_TABLE()

const long ID_TIMER_WX_PROCESS_REQUESTS = wxNewId();

WxSubsystem::CWXMainFrame::CWXMainFrame(wxWindow* parent, wxWindowID id)
{
	Create(
		parent, id, _("MRPT-dummy frame window"), wxDefaultPosition,
		wxSize(1, 1),
		0,	// wxDEFAULT_FRAME_STYLE,
		_T("id"));

	if (oneInstance)
	{
		cerr << "[CWXMainFrame] More than one instance running!" << endl;
	}
	oneInstance = this;

	// ------------------------------------------------------------------------------------------
	// Create a timer so requests from the main application thread can be
	// processed regularly:
	// ------------------------------------------------------------------------------------------
	Bind(
		wxEVT_TIMER, &CWXMainFrame::OnTimerProcessRequests, this,
		ID_TIMER_WX_PROCESS_REQUESTS);
	m_theTimer = new wxTimer(this, ID_TIMER_WX_PROCESS_REQUESTS);

	m_theTimer->Start(10, true);  // One-shot
}

WxSubsystem::CWXMainFrame::~CWXMainFrame()
{
#ifdef WXSUBSYSTEM_VERBOSE
	cout << "[CWXMainFrame] Destructor." << endl;
#endif
	delete m_theTimer;
	oneInstance = nullptr;

	// Purge all pending requests:
	TRequestToWxMainThread* msg;
	while (nullptr != (msg = popPendingWxRequest()))
		delete[] msg;
}

int WxSubsystem::CWXMainFrame::notifyWindowCreation()
{
	auto& wxd = WxSubSystemGlobalData::Instance();

	std::lock_guard<std::mutex> lock(wxd.cs_windowCount);
	return ++wxd.windowCount;
}

int WxSubsystem::CWXMainFrame::notifyWindowDestruction()
{
	auto& wxd = WxSubSystemGlobalData::Instance();

	int ret;
	{
		std::lock_guard<std::mutex> lock(wxd.cs_windowCount);
		ret = --wxd.windowCount;
	}

	if (ret == 0)
	{
		// That was the last window... we should close the wx subsystem:
		if (oneInstance)
		{
#ifdef WXSHUTDOWN_DO_IT_CLEAN
			CWXMainFrame* me =
				(CWXMainFrame*)(oneInstance);  // cast away the "volatile".
			me->Close();
#endif

#ifdef WXSUBSYSTEM_VERBOSE
			cout << "[CWXMainFrame::notifyWindowDestruction] numWindows=0. "
					"me->Close() called."
				 << endl;
#endif
		}
	}

	return ret;
}

/** Thread-safe method to return the next pending request, or nullptr if there
 * is none (After usage, FREE the memory!)
 */
WxSubsystem::TRequestToWxMainThread* WxSubsystem::popPendingWxRequest()
{
	auto& wxd = WxSubSystemGlobalData::Instance();

	std::lock_guard<std::mutex> locker(wxd.cs_listPendingWxRequests);

	// Is empty?
	if (wxd.listPendingWxRequests.empty()) return nullptr;

	TRequestToWxMainThread* ret = wxd.listPendingWxRequests.front();
	wxd.listPendingWxRequests.pop();  // Remove from the queue

	return ret;
}

/** Thread-safe method to insert a new pending request (The memory must be
 * dinamically allocated with "new T[1]", will be freed by receiver.)
 */
void WxSubsystem::pushPendingWxRequest(
	WxSubsystem::TRequestToWxMainThread* data)
{
	if (!WxSubsystem::CWXMainFrame::oneInstance)
	{
#ifdef WXSUBSYSTEM_VERBOSE
		cout << "[WxSubsystem::pushPendingWxRequest] IGNORING request since "
				"app seems already closed.\n";
#endif
		delete[] data;
		return;	 // wx subsystem already closed, ignore.
	}

	auto& wxd = WxSubSystemGlobalData::Instance();

	std::lock_guard<std::mutex> locker(wxd.cs_listPendingWxRequests);
	wxd.listPendingWxRequests.push(data);
}

/** This method processes the pending requests from the main MRPT application
 * thread.
 *  The requests may be to create a new window, close another one, change
 * title, etc...
 */
void WxSubsystem::CWXMainFrame::OnTimerProcessRequests(wxTimerEvent& event)
{
	bool app_closed = false;
	try
	{
		TRequestToWxMainThread* msg;

#ifdef WXSUBSYSTEM_VERBOSE
		cout << "[OnTimerProcessRequests] Entering" << endl;
#endif

		// For each pending request:
		while (nullptr != (msg = popPendingWxRequest()))
		{
			// Process it:
			switch (msg->OPCODE)
			{
				// CREATE NEW WINDOW
				case 200:
					if (msg->source2D)
					{
						auto* wnd = new CWindowDialog(
							msg->source2D, this, (wxWindowID)-1, msg->str,
							wxSize(msg->x, msg->y));

						// Set the "m_hwnd" member of the window:
						*((void**)msg->voidPtr) = (void*)wnd;

						// Signal to the constructor (still waiting) that the
						// window is now ready so it can continue:
						msg->source2D->notifySemThreadReady();

						wnd->Show();
					}
					break;
				// UPDATE IMAGE
				case 201:
					if (msg->source2D)
					{
						auto* wnd =
							(CWindowDialog*)
								msg->voidPtr;  // msg->source2D->getWxObject();
						if (!wnd) break;
						auto* img = (wxImage*)msg->voidPtr2;
						if (!img) break;

						wnd->m_image->AssignImage(new wxBitmap(
							*img));	 // Memory will be freed by the object.

						if (wnd->m_image->GetSize().GetX() != img->GetWidth() &&
							wnd->m_image->GetSize().GetY() != img->GetHeight())
						{
							wnd->m_image->SetSize(
								img->GetWidth(), img->GetHeight());
							wnd->m_image->SetMinSize(
								wxSize(img->GetWidth(), img->GetHeight()));
							wnd->m_image->SetMaxSize(
								wxSize(img->GetWidth(), img->GetHeight()));
							wnd->Fit();
							// wnd->SetClientSize(img->GetWidth(),
							// img->GetHeight());
						}
						delete img;
						// false: Do NOT erase background: avoid flickering
						wnd->m_image->Refresh(false);
						wnd->m_image->Update();
					}
					break;
				// Set position
				case 202:
					if (msg->source2D)
					{
						auto* wnd =
							(CWindowDialog*)msg->source2D->getWxObject();
						if (wnd)
							wnd->SetSize(
								msg->x, msg->y, wxDefaultCoord, wxDefaultCoord);
					}
					break;
				// Set size
				case 203:
					if (msg->source2D)
					{
						auto* wnd =
							(CWindowDialog*)msg->source2D->getWxObject();
						if (wnd) wnd->SetClientSize(msg->x, msg->y);
					}
					break;
				// Set window's title:
				case 204:
					if (msg->source2D)
					{
						auto* wnd =
							(CWindowDialog*)msg->source2D->getWxObject();
						if (wnd) wnd->SetTitle(msg->str.c_str());
					}
					break;
				// DESTROY EXISTING WINDOW:
				case 299:
					if (msg->source2D)
					{
						auto* wnd =
							(CWindowDialog*)msg->source2D->getWxObject();
						if (wnd)
						{
							// delete wnd;
							wnd->Close();
						}
					}
					break;

				// CREATE NEW WINDOW
				case 300:
					if (msg->source3D)
					{
						auto* wnd = new C3DWindowDialog(
							msg->source3D, this, (wxWindowID)-1, msg->str,
							wxSize(msg->x, msg->y));

						// Set the "m_hwnd" member of the window:
						*((void**)msg->voidPtr) = (void*)wnd;

						// Signal to the constructor (still waiting) that the
						// window is now ready so it can continue:
						msg->source3D->notifySemThreadReady();

						wnd->Show();
					}
					break;
				// Set position
				case 302:
					if (msg->source3D)
					{
						auto* wnd =
							(C3DWindowDialog*)msg->source3D->getWxObject();
						if (wnd)
							wnd->SetSize(
								msg->x, msg->y, wxDefaultCoord, wxDefaultCoord);
					}
					break;
				// Set size
				case 303:
					if (msg->source3D)
					{
						auto* wnd =
							(C3DWindowDialog*)msg->source3D->getWxObject();
						if (wnd) wnd->SetClientSize(msg->x, msg->y);
					}
					break;
				// Set window's title:
				case 304:
					if (msg->source3D)
					{
						auto* wnd =
							(C3DWindowDialog*)msg->source3D->getWxObject();
						if (wnd) wnd->SetTitle(msg->str.c_str());
					}
					break;
				// FORCE REPAINT
				case 350:
					if (msg->source3D)
					{
						auto* wnd =
							(C3DWindowDialog*)msg->source3D->getWxObject();
						if (wnd) { wnd->Refresh(false); }
					}
					break;

				// DESTROY EXISTING WINDOW:
				case 399:
					if (msg->source3D)
					{
						auto* wnd =
							(C3DWindowDialog*)msg->source3D->getWxObject();
						if (wnd)
						{
							// delete wnd;
							wnd->Close();
						}
					}
					break;

				// CREATE NEW WINDOW
				case 400:
					if (msg->sourcePlots)
					{
						auto* wnd = new CWindowDialogPlots(
							msg->sourcePlots, this, (wxWindowID)-1, msg->str,
							wxSize(msg->x, msg->y));

						// Set the "m_hwnd" member of the window:
						*((void**)msg->voidPtr) = (void*)wnd;

						// Signal to the constructor (still waiting) that the
						// window is now ready so it can continue:
						msg->sourcePlots->notifySemThreadReady();

						wnd->Show();
					}
					break;
				// Set position
				case 402:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd)
							wnd->SetSize(
								msg->x, msg->y, wxDefaultCoord, wxDefaultCoord);
					}
					break;
				// Set size
				case 403:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd) wnd->SetClientSize(msg->x, msg->y);
					}
					break;
				// Set window's title:
				case 404:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd) wnd->SetTitle(msg->str.c_str());
					}
					break;
				// Mouse pan
				case 410:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd) wnd->m_plot->EnableMousePanZoom(msg->boolVal);
					}
					break;
				// Aspect ratio
				case 411:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd) wnd->m_plot->LockAspect(msg->boolVal);
					}
					break;

				// Zoom over a rectangle vectorx[0-1] & vectory[0-1]
				case 412:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd)
						{
							if (msg->vector_x.size() == 2 &&
								msg->vector_y.size() == 2)
							{
								wnd->m_plot->Fit(
									msg->vector_x[0], msg->vector_x[1],
									msg->vector_y[0], msg->vector_y[1]);
								wnd->m_plot->LockAspect(msg->boolVal);
							}
						}
					}
					break;
				// Axis fit, with aspect ratio fix to boolVal.
				case 413:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd)
						{
							wnd->m_plot->LockAspect(msg->boolVal);
							wnd->m_plot->Fit();
						}
					}
					break;
				// Clear all objects:
				case 414:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd)
						{
							wnd->m_plot->DelAllLayers(true, true);
							wnd->m_plot->AddLayer(new mpScaleX());
							wnd->m_plot->AddLayer(new mpScaleY());
						}
					}
					break;

				// Create/modify 2D plot
				case 420:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd)
							wnd->plot(
								msg->vector_x, msg->vector_y, msg->str,
								msg->plotName);
					}
					break;

				// Create/modify 2D ellipse
				case 421:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd)
							wnd->plotEllipse(
								msg->vector_x, msg->vector_y, msg->str,
								msg->plotName, msg->boolVal);
					}
					break;

				// Create/modify bitmap image
				case 422:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd)
							wnd->image(
								msg->voidPtr2, msg->vector_x[0],
								msg->vector_x[1], msg->vector_x[2],
								msg->vector_x[3], msg->plotName);
					}
					break;

				// 440: Insert submenu in the popup menu. name=menu label, x=ID
				case 440:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd)
						{
							const long MENUITEM_ID = wxNewId();
							// Remember the association between this ID and the
							// user ID:
							wnd->m_ID2ID[MENUITEM_ID] = msg->x;

							wxMenu* popupMnu = wnd->m_plot->GetPopupMenu();
							if (wnd->m_firstSubmenu)
							{
								wnd->m_firstSubmenu = false;
								popupMnu->InsertSeparator(0);
							}
							wxMenuItem* mnuTarget = new wxMenuItem(
								popupMnu, MENUITEM_ID, msg->plotName.c_str(),
								wxEmptyString, wxITEM_NORMAL);
							popupMnu->Insert(0, mnuTarget);

							wnd->Bind(
								wxEVT_MENU, &CWindowDialogPlots::OnMenuSelected,
								wnd, MENUITEM_ID);
						}
					}
					break;

				// DESTROY EXISTING WINDOW:
				case 499:
					if (msg->sourcePlots)
					{
						auto* wnd = (CWindowDialogPlots*)
										msg->sourcePlots->getWxObject();
						if (wnd)
						{
							// delete wnd;
							wnd->Close();
						}
					}
					break;

				// CREATE NEW WINDOW
				case 700:
					if (msg->sourceCameraSelectDialog)
					{
						auto* sem =
							reinterpret_cast<std::promise<void>*>(msg->voidPtr);

						auto dlg = std::make_unique<CDialogAskUserForCamera>();

						// Signal that the window is ready:
						sem->set_value();

						// Show
						const bool wasOk = (dlg->ShowModal() == wxID_OK);

						// send selection to caller:
						auto* promise = reinterpret_cast<std::promise<
							mrpt::gui::detail::TReturnAskUserOpenCamera>*>(
							msg->voidPtr2);
						mrpt::gui::detail::TReturnAskUserOpenCamera ret;

						// Parse selection as a config text block:
						mrpt::config::CConfigFileMemory c;
						dlg->panel->writeConfigFromVideoSourcePanel(
							"CONFIG", &c);
						c.getContent(ret.selectedConfig);
						ret.accepted_by_user = wasOk;

						promise->set_value(std::move(ret));
						dlg->Close();
					}
					break;

				case 800:
				{
					try
					{
						msg->userFunction();
					}
					catch (const std::exception& e)
					{
						std::cerr
							<< "[WxSubsystem] Exception with userFunction():\n"
							<< e.what();
					}
				}
				break;

				// wxSubsystem shutdown:
				case 999:
				{
#ifdef WXSUBSYSTEM_VERBOSE
					cout << "[WxSubsystem:999] Shutdown" << endl;
#endif
					app_closed = true;	// Do NOT launch a timer again
					if (WxSubsystem::CWXMainFrame::oneInstance)
						((WxSubsystem::CWXMainFrame*)(WxSubsystem::
														  CWXMainFrame::
															  oneInstance))
							->Close();
#ifdef WXSUBSYSTEM_VERBOSE
					cout << "[WxSubsystem:999] Shutdown done" << endl;
#endif
				}
				break;

			}  // end switch OPCODE

			// Free the memory:
			delete[] msg;
		}  // end while
	}
	catch (...)
	{
	}

	if (!app_closed) m_theTimer->Start(10, true);  // One-shot
}

// ---------------------------------------------------------------------------------------
// MRPT Icons
// ---------------------------------------------------------------------------------------
const char* mrpt_default_icon_xpm[] = {
	"32 32 2 1",
	" 	c None",
	".	c #000000",
	"                                ",
	"                                ",
	"                                ",
	" .....     ..... .........      ",
	"  ....     ....   ...   ....    ",
	"  .....    ....   ...    ...    ",
	"  . ...   . ...   ...    ...    ",
	"  . ...   . ...   ...    ...    ",
	"  .  ... .  ...   ...   ...     ",
	"  .  ... .  ...   ........      ",
	"  .  .....  ...   ... ....      ",
	"  .   ...   ...   ...  ....     ",
	"  .   ...   ...   ...   ....    ",
	"  .   ..    ...   ...    ....   ",
	" ...   .   ..... .....    ..... ",
	"                                ",
	"                                ",
	"    ........     ...........    ",
	"     ...  ....   ..  ...  ..    ",
	"     ...   ...   .   ...   .    ",
	"     ...   ...       ...        ",
	"     ...   ...       ...        ",
	"     ...  ...        ...        ",
	"     .......         ...        ",
	"     ...             ...        ",
	"     ...             ...        ",
	"     ...             ...        ",
	"     ...             ...        ",
	"    .....           .....       ",
	"                                ",
	"                                ",
	"                                "};

wxBitmap WxSubsystem::getMRPTDefaultIcon()
{
// To avoid an error in wx, always resize the icon to the expected size:
#ifdef _WIN32
	const wxSize iconsSize(
		::GetSystemMetrics(SM_CXICON), ::GetSystemMetrics(SM_CYICON));
	return wxBitmap(wxBitmap(mrpt_default_icon_xpm)
						.ConvertToImage()
						.Scale(iconsSize.x, iconsSize.y));
#else
	return wxBitmap(mrpt_default_icon_xpm);
#endif
}

// ---------------------------------------------------------------------------------------
// The wx app:
// ---------------------------------------------------------------------------------------
class CDisplayWindow_WXAPP : public wxApp
{
   public:
	bool OnInit() override;
	int OnExit() override;
};

bool CDisplayWindow_WXAPP::OnInit()
{
	// Starting in wxWidgets 2.9.0, we must reset numerics locale to "C",
	//  if we want numbers to use "." in all countries. The App::OnInit() is a
	//  perfect place to undo
	//  the default wxWidgets settings. (JL @ Sep-2009)
	wxSetlocale(LC_NUMERIC, wxString(wxT("C")));

	wxInitAllImageHandlers();

	// cout << "[wxApp::OnInit] wxApplication OnInit called." << endl;

	// Create a dummy frame:
	auto* Frame = new WxSubsystem::CWXMainFrame(nullptr);
	Frame->Hide();

	// We are ready!!
	// cout << "[wxMainThread] Signaling semaphore." << endl;
	WxSubsystem::GetWxMainThreadInstance().m_semWxMainThreadReady.set_value();

	return true;
}

// This will be called when all the windows / frames are closed.
int CDisplayWindow_WXAPP::OnExit()
{
#ifdef WXSUBSYSTEM_VERBOSE
	cout << "[wxApp::OnExit] wxApplication OnExit called." << endl;
#endif

	std::lock_guard<std::mutex> lock(
		WxSubsystem::GetWxMainThreadInstance().m_csWxMainThreadId);

	wxApp::OnExit();
	CleanUp();
	return 0;
}

/** This method must be called in the destructor of the user class FROM THE MAIN
 * THREAD, in order to wait for the shutdown of the wx thread if this was the
 * last open window.
 */
void WxSubsystem::waitWxShutdownsIfNoWindows()
{
#ifndef WXSHUTDOWN_DO_IT_CLEAN

#ifdef WXSUBSYSTEM_VERBOSE
	cout << "[WxSubsystem::waitWxShutdownsIfNoWindows] Doing a quick "
			"std::this_thread::sleep_for(ms) and returning.\n";
#endif
	std::this_thread::sleep_for(100ms);
	return;
#else
	// Just let know a global object that, at its destruction, it must  ....
	// Any open windows?
	int nOpenWnds;
	{
		std::lock_guard<std::mutex> lock(CWXMainFrame::cs_windowCount);
		nOpenWnds = CWXMainFrame::m_windowCount;
	}

	if (!nOpenWnds && WxSubsystem::isConsoleApp())
	{
#ifdef WXSUBSYSTEM_VERBOSE
		cout << "[WxSubsystem::waitWxShutdownsIfNoWindows] Waiting for "
				"WxWidgets thread to shutdown...\n";
#endif

		// Then we must be shutting down in the wx thread (we are in the main
		// MRPT application thread)...
		// Wait until wx is safely shut down:
		bool done = false;
		int maxTimeout =
#ifdef _DEBUG
			30000;
#else
			5000;
#endif
		if (m_done.wait_for(std::chrono::milliseconds(maxTimeout)) ==
			std::future_status::timeout)
		{
			cerr << "[WxSubsystem::waitWxShutdownsIfNoWindows] Timeout waiting "
					"for WxWidgets thread to shutdown!"
				 << endl;
		}
	}
#endif
}

wxAppConsole* mrpt_wxCreateApp()
{
	wxAppConsole::CheckBuildOptions(WX_BUILD_OPTIONS_SIGNATURE, "your program");
	return new CDisplayWindow_WXAPP;
}

// DECLARE_APP(CDisplayWindow_WXAPP)
extern CDisplayWindow_WXAPP& wxGetApp();

// Aux. funcs used in WxSubsystem::wxMainThread
// --------------------------------------------------
int mrpt_wxEntryReal()
{
	// library initialization
	if (!wxInitialize())
	{
#if wxUSE_LOG
		// flush any log messages explaining why we failed
		delete wxLog::SetActiveTarget(nullptr);
#endif
		return -1;
	}

	// if wxEntryStart succeeded, we must call wxEntryCleanup even if the code
	// below returns or throws
	try
	{
		// app initialization
		if (!wxTheApp->CallOnInit())
			return -1;	// don't call OnExit() if OnInit() failed

		// app execution
		int ret = wxTheApp->OnRun();

		{
			wxLogNull logNo;  // Skip any warning in this scope.

			wxTheApp->OnExit();	 // This replaces the above callOnExit class
			wxEntryCleanup();
		}

		return ret;
	}
	catch (...)
	{
		wxTheApp->OnUnhandledException();
		wxEntryCleanup();
		return -1;
	}
}

/*---------------------------------------------------------------
					wxMainThread
 This will be the "MAIN" of wxWidgets: It starts an application
   object and does not end until all the windows are closed.
 Only for console apps, not for user GUI apps already with wx.
 ---------------------------------------------------------------*/
void WxSubsystem::wxMainThread()
{
	MRPT_START
	// Prepare wxWidgets:
#ifdef WXSUBSYSTEM_VERBOSE
	cout << "[wxMainThread] Starting..." << endl;
#endif

	// Are we in a console or wxGUI application????
	wxAppConsole* app_gui = wxApp::GetInstance();
	if (!app_gui)
	{
// We are NOT in a wx application (it's a console program)
// ---------------------------------------------------------
#ifdef WXSUBSYSTEM_VERBOSE
		cout << "[wxMainThread] I am in a console app" << endl;
#endif
		//  Start a new wx application object:

		// JLBC OCT2008: wxWidgets little hack to enable console/gui mixed
		// applications:
		wxApp::SetInitializerFunction(
			(wxAppInitializerFunction)mrpt_wxCreateApp);
		mrpt_wxEntryReal();

#ifdef WXSUBSYSTEM_VERBOSE
		cout << "[wxMainThread] Finished" << endl;
#endif

		// Now this thread is ready. The main thread is free to end now:
		WxSubsystem::GetWxMainThreadInstance().m_done.set_value();
	}
	else
	{
// We are ALREADY in a wx application:
// ---------------------------------------------------------
#ifdef WXSUBSYSTEM_VERBOSE
		cout << "[wxMainThread] I am in a GUI app" << endl;
#endif
		wxWindow* topWin = static_cast<wxApp*>(app_gui)->GetTopWindow();

		auto* Frame = new WxSubsystem::CWXMainFrame(topWin);
		Frame->Hide();

// We are ready!!
#ifdef WXSUBSYSTEM_VERBOSE
		cout << "[wxMainThread] Signaling semaphore." << endl;
#endif
		WxSubsystem::GetWxMainThreadInstance()
			.m_semWxMainThreadReady.set_value();
	}

	MRPT_END
}

WxSubsystem::TWxMainThreadData& WxSubsystem::GetWxMainThreadInstance()
{
	// static TWxMainThreadData dat;
	// Create as dynamic memory, since it'll be deleted in
	// CAuxWxSubsystemShutdowner:
	static TWxMainThreadData* dat = nullptr;
	static bool first_creat = true;
	if (!dat && first_creat)
	{
		first_creat = false;
		dat = new TWxMainThreadData;
	}
	return *dat;
}

/*---------------------------------------------------------------
					createOneInstanceMainThread
 ---------------------------------------------------------------*/
bool WxSubsystem::createOneInstanceMainThread()
{
	WxSubsystem::TWxMainThreadData& wxmtd =
		WxSubsystem::GetWxMainThreadInstance();
	std::lock_guard<std::mutex> lock(wxmtd.m_csWxMainThreadId);

	wxAppConsole* app_con = wxApp::GetInstance();
	if (app_con && wxmtd.m_wxMainThreadId.get_id() == std::thread::id())
	{
		// We are NOT in a console application: There is already a wxApp
		// instance running and it's not us.
		isConsoleApp_value = false;
		// cout << "[createOneInstanceMainThread] Mode: User GUI." << endl;
		if (!WxSubsystem::CWXMainFrame::oneInstance)
		{
			// Create our main hidden frame:
			wxWindow* topWin = static_cast<wxApp*>(app_con)->GetTopWindow();

			auto* Frame = new WxSubsystem::CWXMainFrame(topWin);
			// Frame->Show();
			// SetTopWindow(Frame);
			Frame->Hide();
		}
	}
	else
	{
		// cout << "[createOneInstanceMainThread] Mode: Console." << endl;
		isConsoleApp_value = true;
		if (wxmtd.m_wxMainThreadId.get_id() == std::thread::id())
		{
#ifdef WXSUBSYSTEM_VERBOSE
			printf(
				"[WxSubsystem::createOneInstanceMainThread] Launching "
				"wxMainThread() thread...\n");
#endif
			// Create a thread for message processing there:
			wxmtd.m_wxMainThreadId = std::thread(wxMainThread);

			mrpt::system::thread_name("wxMainThread", wxmtd.m_wxMainThreadId);

			int maxTimeout =
#ifdef _DEBUG
				30000;
#else
				5000;
#endif

			// If we have an "MRPT_WXSUBSYS_TIMEOUT_MS" environment variable,
			// use that timeout instead:
			const char* envVal = getenv("MRPT_WXSUBSYS_TIMEOUT_MS");
			if (envVal) maxTimeout = atoi(envVal);

			// A few secs should be enough...
			if (wxmtd.m_semWxMainThreadReady.get_future().wait_for(
					std::chrono::milliseconds(maxTimeout)) ==
				std::future_status::timeout)
			{
				cerr << "[WxSubsystem::createOneInstanceMainThread] Timeout "
						"waiting wxApplication to start up!"
					 << endl;
				return false;
			}
		}
	}

	return true;  // OK
}

#endif	// MRPT_HAS_WXWIDGETS
