/*
 * Copyright (C) 2019-2025 Roger Clark, VK3KYY / G4KYF
 *                         Daniel Caujolle-Bert, F1RMB
 *
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
 *    in the documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * 4. Use of this source code or binary releases for commercial purposes is strictly forbidden. This includes, without limitation,
 *    incorporation in a commercial product or incorporation into a product or project which allows commercial use.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */
#include "user_interface/uiGlobals.h"
#include "user_interface/menuSystem.h"
#include "user_interface/uiLocalisation.h"
#include "user_interface/uiUtilities.h"

menuDataGlobal_t menuDataGlobal =
{
		.currentItemIndex 		= 0, // each menu can re-use this var to hold the position in their display list. To save wasted memory if they each had their own variable
		.startIndex 			= 0, // as above
		.numItems 				= 0, // as above
		.lightTimer 			= -1,
		.currentMenuList		= NULL,

		.controlData =
		{
				.stackPosition 	= 0,
				.stack 			=
				{
						MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY, MENU_EMPTY
				}
		},

		/*
		 * ---------------------- IMPORTANT ----------------------------
		 *
		 * The menuFunctions array and the menusData array.....
		 *
		 * MUST match the enum MENU_SCREENS in menuSystem.h
		 *
		 * ---------------------- IMPORTANT ----------------------------
		 */
		.data 					=
		{
				&menuDataMainMenu,
				&menuDataContact,
				NULL,// zone
				NULL,// RadioInfos
				NULL,// RSSI
				NULL,// LastHeard
				&menuDataOptions,// Options
				NULL,// General options
				NULL,// Radio options
				NULL,// Display options
				NULL,// Sound options
				NULL,// SatelliteScreen
#if defined(HAS_GPS)
				NULL,// GPS
#endif
				NULL,// Contact List
				NULL,// DTMF Contact List
				NULL,// Contact Quick List (SK2+#)
				NULL,// Contact List Quick Menu
				NULL,// Contact Details
				NULL,// Language
				NULL,// Calibration
#if defined(HAS_COLOURS)
				NULL,// Theme options
#endif
				NULL,// Quick menu - Channel
				NULL,// Quick menu - VFO
				NULL,// Channel Details
				NULL,// Firmwareinfo
#if !defined(PLATFORM_GD77S)
				NULL,// APRS options
#endif
				&menuDataSMS,// SMS hub
				// *** Add new menus to be accessed using quickkey (ID: 0..31) above this line ***
				NULL,// SMS compose
				NULL,// SMS inbox
				NULL,// SMS sent
				NULL,// SMS view
				NULL,// SMS quick texts
				NULL,// SMS options
				NULL,// SMS contact picker
				NULL,// SMS RX popup
				NULL,// MessageBox
				NULL,// hotspot mode
				NULL,// CPS
				NULL,// Numerical entry
				NULL,// Tx
				NULL,// splash
				NULL,// power off
				NULL,// vfo mode
				NULL,// channel mode
				NULL,// Lock screen
				NULL,// Private Call
				NULL,// New Contact
#if defined(HAS_COLOURS)
				NULL,// Theme items browser
				NULL,// Colour picker
#endif
		}
};

static menuFunctionData_t menuFunctions[] =
{
		{ menuDisplayMenuList,      NULL, NULL, 0 },// display Main menu using the menu display system
		{ menuDisplayMenuList,      NULL, NULL, 0 },// display Contact menu using the menu display system
		{ menuZoneList,             NULL, NULL, 0 },
		{ menuRadioInfos,           NULL, NULL, 0 },
		{ menuRSSIScreen,           NULL, NULL, 0 },
		{ menuLastHeard,            NULL, NULL, 0 },
		{ menuDisplayMenuList,      NULL, NULL, 0 },
		{ menuGeneralOptions,       NULL, NULL, 0 },
		{ menuRadioOptions,         NULL, NULL, 0 },
		{ menuDisplayOptions,       NULL, NULL, 0 },
		{ menuSoundOptions,         NULL, NULL, 0 },
#if defined(USING_EXTERNAL_DEBUGGER)
		{ menuSoundOptions,         NULL, NULL, 0 },// hack to remove satellite screen from the build when external debugger is selected, otherwise there is not enough space in the ROM to build the firmware
#else
		{ menuSatelliteScreen,      NULL, NULL, 0 },
#endif
#if defined(HAS_GPS)
		{ menuGPS,                  NULL, NULL, 0 },
#endif
		{ menuContactList,          NULL, NULL, 0 },
		{ menuContactList,          NULL, NULL, 0 },
		{ menuContactList,          NULL, NULL, 0 },
		{ menuContactListSubMenu,   NULL, NULL, 0 },
		{ menuContactDetails,       NULL, NULL, 0 },
		{ menuLanguage,             NULL, NULL, 0 },
		{ menuCalibration,          NULL, NULL, 0 },
#if defined(HAS_COLOURS)
		{ menuThemeOptions,         NULL, NULL, 0 },
#endif
		{ uiChannelModeQuickMenu,   NULL, NULL, 0 },
		{ uiVFOModeQuickMenu,       NULL, NULL, 0 },
		{ menuChannelDetails,       NULL, NULL, 0 },
		{ menuFirmwareInfoScreen,   NULL, NULL, 0 },
#if !defined(PLATFORM_GD77S)
		{ menuAPRSOptions,          NULL, NULL, 0 },
#endif
		{ menuDisplayMenuList,      NULL, NULL, 0 },// SMS hub
		// *** Add new menus to be accessed using quickkey (ID: 0..31) above this line ***
		{ menuSMSCompose,           NULL, NULL, 0 },
		{ menuSMSList,              NULL, NULL, 0 },// SMS inbox
		{ menuSMSList,              NULL, NULL, 0 },// SMS sent
		{ menuSMSList,              NULL, NULL, 0 },// SMS view
		{ menuSMSQuickText,         NULL, NULL, 0 },
		{ menuSMSOptions,           NULL, NULL, 0 },
		{ menuSMSContact,           NULL, NULL, 0 },
		{ uiSMSPopup,               NULL, NULL, 0 },
		{ uiMessageBox,             NULL, NULL, 0 },
		{ menuHotspotMode,          NULL, NULL, 0 },
		{ uiCPS,                    NULL, NULL, 0 },
		{ menuNumericalEntry,       NULL, NULL, 0 },
		{ menuTxScreen,             NULL, NULL, 0 },
		{ uiSplashScreen,           NULL, NULL, 0 },
		{ uiPowerOff,               NULL, NULL, 0 },
		{ uiVFOMode,                NULL, NULL, 0 },
		{ uiChannelMode,            NULL, NULL, 0 },
		{ menuLockScreen,           NULL, NULL, 0 },
		{ menuPrivateCall,          NULL, NULL, 0 },
		{ menuContactDetails,       NULL, NULL, 0 }, // Contact New
#if defined(HAS_COLOURS)
		{ menuThemeItemsBrowser,    NULL, NULL, 0 },
		{ menuColourPicker,         NULL, NULL, 0 },
#endif
};

static void menuSystemCheckForFirstEntryAudible(menuStatus_t status)
{
	if (nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_BEEP)
	{
		// If VP is currently playing, we should not set the next beep, otherwise it
		// will be played at the wrong time (e.g  entering TG/PC input window, ACK beep will be
		// played on the next beep playback event
		if ((nonVolatileSettings.audioPromptMode >= AUDIO_PROMPT_MODE_VOICE_THRESHOLD) && voicePromptsIsPlaying())
		{
			return;
		}

		if (status & MENU_STATUS_ERROR)
		{
			nextKeyBeepMelody = (int16_t *)MELODY_ERROR_BEEP;
		}
		else if (((status & MENU_STATUS_LIST_TYPE) && (menuDataGlobal.currentItemIndex == 0)) || (status & MENU_STATUS_FORCE_FIRST))
		{
			nextKeyBeepMelody = (int16_t *)MELODY_KEY_BEEP_FIRST_ITEM;
		}
		else if (status & MENU_STATUS_INPUT_TYPE)
		{
			nextKeyBeepMelody = (int16_t *)MELODY_ACK_BEEP;
		}
	}
}

static void menuSystemPushMenuFirstRun(void)
{
	uiEvent_t ev = { .buttons = 0, .keys = NO_KEYCODE, .rotary = 0, .function = 0, .events = NO_EVENT, .hasEvent = false, .time = ticksGetMillis() };
	menuStatus_t status;

	if (uiNotificationIsVisible() && uiNotificationHasTimedOut())
	{
		uiNotificationHide(false);
	}

	// Due to QuickKeys, menu list won't go through menuDisplayMenuList() first, so those
	// two members won't get always initialized. Hence, we need to tag them as uninitialized,
	// and check if they got initialized after entering a menu.
	menuDataGlobal.numItems = INT32_MIN;
	menuDataGlobal.currentMenuList = NULL;
	menuDataGlobal.currentItemIndex = menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].lastItemIndex;
	menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].exitCallback = NULL;
	menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].data = NULL;
	displayLightTrigger(false);

	// This only happens when on Cal Data or Flash init errors.
	if (menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition] == MENU_EMPTY)
	{
		status = MENU_STATUS_SUCCESS;
		displayClearBuf();
		displayPrintCentered(((DISPLAY_SIZE_Y - FONT_SIZE_3_HEIGHT) >> 1), globalFailureMessage, FONT_SIZE_3);
		displayRender();
	}
	else
	{
		status = menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].function(&ev, true);
	}

	if (menuDataGlobal.numItems == INT32_MIN)
	{
		menuDataGlobal.numItems = ((menuDataGlobal.data[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]] != NULL) ? menuDataGlobal.data[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]]->numItems : 0);
	}

	if (menuDataGlobal.currentMenuList == NULL)
	{
		menuDataGlobal.currentMenuList = ((menuDataGlobal.data[menuDataGlobal.controlData.stackPosition] != NULL) ? (menuItemNewData_t *)menuDataGlobal.data[menuDataGlobal.controlData.stackPosition]->items : NULL);
	}

	if (menuDataGlobal.currentItemIndex > menuDataGlobal.numItems)
	{
		menuDataGlobal.currentItemIndex = 0;
	}
	menuSystemCheckForFirstEntryAudible(status);
}

int menuSystemGetLastItemIndex(int stackPos)
{
	if ((stackPos >= 0) && (stackPos <= menuDataGlobal.controlData.stackPosition))
	{
		return menuFunctions[menuDataGlobal.controlData.stack[stackPos]].lastItemIndex;
	}

	return -1;
}

void menuSystemRegisterExitCallback(menuExitCallback_t f, void *data)
{
	menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].exitCallback = f;
	menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].data = data;
}

void menuSystemPushNewMenu(int menuNumber)
{
	if (menuDataGlobal.controlData.stackPosition < 15)
	{
		keyboardReset();
		menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].lastItemIndex = menuDataGlobal.currentItemIndex;
		menuDataGlobal.controlData.stackPosition++;
		menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition] = menuNumber;
#if defined(PLATFORM_MD9600)
		if ((menuNumber != UI_VFO_MODE) && (menuNumber != UI_CHANNEL_MODE))
		{
			uiDataGlobal.sk2latched = false;
		}
#endif
		menuSystemPushMenuFirstRun();
	}
}

void menuSystemCallExitCallback(void)
{
	if (menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].exitCallback != NULL)
	{
		menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].exitCallback(menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].data);
		menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].exitCallback = NULL;
		menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].data = NULL;
	}
}

void menuSystemPopPreviousMenu(void)
{
	keyboardReset();

	menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].lastItemIndex = menuDataGlobal.currentItemIndex;

	menuSystemCallExitCallback();

	// Clear stackPosition + 1 menu trace
	if (menuDataGlobal.controlData.stackPosition < 15)
	{
		menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition + 1] = MENU_EMPTY;
	}

	// Avoid crashing if something goes wrong.
	if (menuDataGlobal.controlData.stackPosition > 0)
	{
		menuDataGlobal.controlData.stackPosition -= 1;
	}
	menuSystemPushMenuFirstRun();
}

void menuSystemPopAllAndDisplayRootMenu(void)
{
	keyboardReset();

	menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].lastItemIndex = menuDataGlobal.currentItemIndex;

	while (menuDataGlobal.controlData.stackPosition >= 1)
	{
		menuSystemCallExitCallback();
		menuDataGlobal.controlData.stackPosition--;
	}

	// MENU_EMPTY is equal to -1 (0xFFFFFFFF), hence the following works, even if it's an int32_t array
	memset(&menuDataGlobal.controlData.stack[1], MENU_EMPTY, sizeof(menuDataGlobal.controlData.stack) - sizeof(int));
	menuDataGlobal.controlData.stackPosition = 0;
	menuSystemPushMenuFirstRun();
}

void menuSystemPopAllAndDisplaySpecificRootMenu(int newRootMenu, bool resetKeyboard)
{
	if (resetKeyboard)
	{
		keyboardReset();
	}

	menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].lastItemIndex = menuDataGlobal.currentItemIndex;

	while (menuDataGlobal.controlData.stackPosition >= 1)
	{
		menuSystemCallExitCallback();
		menuDataGlobal.controlData.stackPosition--;
	}

	// MENU_EMPTY is equal to -1 (0xFFFFFFFF), hence the following works, even if it's an int32_t array
	memset(&menuDataGlobal.controlData.stack[1], MENU_EMPTY, sizeof(menuDataGlobal.controlData.stack) - sizeof(int));
	menuDataGlobal.controlData.stack[0] = newRootMenu;
	menuDataGlobal.controlData.stackPosition = 0;
	menuSystemPushMenuFirstRun();
}

void menuSystemSetCurrentMenu(int menuNumber)
{
	keyboardReset();
	menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition] = menuNumber;
	menuSystemPushMenuFirstRun();
}

int menuSystemGetCurrentMenuNumber(void)
{
	return menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition];
}

int menuSystemGetPreviousMenuNumber(void)
{
	if (menuDataGlobal.controlData.stackPosition >= 1)
	{
		return menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition - 1];
	}

	return MENU_ANY;
}

int menuSystemGetPreviouslyPushedMenuNumber(bool keepIt)
{
	if (menuDataGlobal.controlData.stackPosition < 15)
	{
		int m = menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition + 1];

		if (keepIt == false)
		{
			menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition + 1] = MENU_EMPTY;
		}
		return m;
	}

	return MENU_EMPTY;
}

int menuSystemGetRootMenuNumber(void)
{
	return menuDataGlobal.controlData.stack[0];
}

static void menuSystemPreProcessEvent(uiEvent_t *ev)
{
#if defined(PLATFORM_GD77) || defined(PLATFORM_GD77S) || defined(PLATFORM_DM1801) || defined(PLATFORM_DM1801A) || defined(PLATFORM_RD5R)
	if (ev->hasEvent || ((uiDataGlobal.displayQSOState == QSO_DISPLAY_CALLER_DATA) && (nonVolatileSettings.backlightMode != BACKLIGHT_MODE_BUTTONS)))
	{
		if ((ev->events & SYNTHETIC_EVENT) == 0)
		{
			displayLightTrigger(true);
		}
	}
#else // ! MK22
	if (ev->hasEvent)
	{
		int currentMenu = menuSystemGetCurrentMenuNumber();
#if defined(PLATFORM_MD9600)
		bool isEnteringDTMF = ((trxIsTransmitting || (currentMenu == MENU_NUMERICAL_ENTRY)) && (trxGetMode() == RADIO_MODE_ANALOG));
#endif

		if ((currentMenu != UI_CHANNEL_MODE) && (currentMenu != UI_VFO_MODE))
		{
			switch(ev->keys.key)
			{
#if defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_MD9600)
				case KEY_ROTARY_INCREMENT:
					ev->keys.key = KEY_RIGHT;
					break;
				case KEY_ROTARY_DECREMENT:
					ev->keys.key = KEY_LEFT;
					break;
#endif
#if defined(PLATFORM_MD9600)
				case KEY_B:
					if (isEnteringDTMF == false)
					{
						ev->keys.key = KEY_RIGHT;
					}
					break;
				case KEY_C:
					if (isEnteringDTMF == false)
					{
						ev->keys.key = KEY_LEFT;
					}
					break;
				case KEY_A:
					if ((trxIsTransmitting == false) &&
							((isEnteringDTMF == false) ||
									(isEnteringDTMF && ((ev->keys.event & KEY_MOD_LONG) == KEY_MOD_LONG))))
					{
						ev->keys.key = KEY_RED;

						if (isEnteringDTMF)
						{
							ev->keys.event = KEY_MOD_UP;
						}
					}
					break;
#endif
				case KEY_FRONT_UP:
					ev->keys.key = KEY_UP;
					break;
				case KEY_FRONT_DOWN:
					ev->keys.key = KEY_DOWN;
					break;
			}
		}
		else
		{
#if defined(PLATFORM_MD9600) || !defined(PLATFORM_RT84_DM1701) // This block is for MD9600, MDUV380 or MD380
			switch(ev->keys.key)
			{
#if ! (defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017) || defined(PLATFORM_MD9600))
				case KEY_UP:
					ev->keys.key = KEY_LEFT;
					break;
				case KEY_DOWN:
					ev->keys.key = KEY_RIGHT;
					break;
#elif defined(PLATFORM_MD2017)
					// The two following events are possibly issued from the Trackball, check that and convert if needed
				case KEY_UP:
					// In Channel and VFO, KEY_UP has to be remapped to KEY_FONT_UP
					if (ev->keys.event == (KEY_MOD_UP | KEY_MOD_PRESS))
					{
						ev->keys.key = KEY_FRONT_UP;
					}
					break;
				case KEY_DOWN:
					// In Channel and VFO, KEY_DOWN has to be remapped to KEY_FONT_DOWN
					if (ev->keys.event == (KEY_MOD_UP | KEY_MOD_PRESS))
					{
						ev->keys.key = KEY_FRONT_DOWN;
					}
					break;
#endif
#if defined(PLATFORM_MD9600)
				case KEY_B:
					ev->keys.key = KEY_RIGHT;
					break;
				case KEY_C:
					ev->keys.key = KEY_LEFT;
					break;
				case KEY_A:
					ev->keys.key = KEY_RED;
					break;
				case KEY_ROTARY_INCREMENT:
					ev->keys.key = KEY_UP;
					break;
				case KEY_ROTARY_DECREMENT:
					ev->keys.key = KEY_DOWN;
					break;
				case KEY_FRONT_UP:
					if ((ev->keys.event & (KEY_MOD_DOWN | KEY_MOD_LONG)) == (KEY_MOD_DOWN | KEY_MOD_LONG))
					{
						ev->keys.key = KEY_UP;
					}
					else
					{
						ev->keys.key = KEY_RIGHT;
					}
					break;
				case KEY_FRONT_DOWN:
					if ((ev->keys.event & (KEY_MOD_DOWN | KEY_MOD_LONG)) == (KEY_MOD_DOWN | KEY_MOD_LONG))
					{
						ev->keys.key = KEY_DOWN;
					}
					else
					{
						ev->keys.key = KEY_LEFT;
					}
					break;
#endif
#if defined(PLATFORM_MD9600) || !defined(PLATFORM_RT84_DM1701)
			}
#endif
#endif
		}

		if ((ev->events & SYNTHETIC_EVENT) == 0)
		{
			displayLightTrigger(true);
		}
	}
	else
	{
		if (((uiDataGlobal.displayQSOState == QSO_DISPLAY_CALLER_DATA) && (nonVolatileSettings.backlightMode != BACKLIGHT_MODE_BUTTONS)) )
		{
			displayLightTrigger(true);
		}
	}
#endif // ! MK22
}

static void menuSystemPostProcessEvent(uiEvent_t *ev)
{
	if (uiDataGlobal.displayQSOState == QSO_DISPLAY_CALLER_DATA)
	{
		uiDataGlobal.displayQSOState = QSO_DISPLAY_IDLE;
	}
}

void menuSystemCallCurrentMenuTick(uiEvent_t *ev)
{
	menuStatus_t status;

	menuSystemPreProcessEvent(ev);

	if (menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition] == MENU_EMPTY)
	{
		status = MENU_STATUS_SUCCESS;
		displayClearBuf();
		displayPrintCentered(((DISPLAY_SIZE_Y - FONT_SIZE_3_HEIGHT) >> 1), globalFailureMessage, FONT_SIZE_3);
		displayRender();
	}
	else
	{
		status = menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].function(ev, false);
	}

	menuSystemPostProcessEvent(ev);

	if (ev->hasEvent)
	{
		menuSystemCheckForFirstEntryAudible(status);
	}
}

void displayLightTrigger(bool fromKeyEvent)
{
	// BACKLIGHT_MODE_MANUAL is handled in main.c
	if ((menuSystemGetCurrentMenuNumber() != UI_TX_SCREEN) &&
			(((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_AUTO) || (nonVolatileSettings.backlightMode == BACKLIGHT_MODE_SQUELCH))
					|| ((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_BUTTONS) && fromKeyEvent)))
	{
		menuDataGlobal.lightTimer = nonVolatileSettings.backLightTimeout * 1000;

		displayEnableBacklight(true, nonVolatileSettings.displayBacklightPercentageOff);
	}
}

// use -1 to force LED on all the time
void displayLightOverrideTimeout(int timeout)
{
	int prevTimer = menuDataGlobal.lightTimer;

	menuDataGlobal.lightTimer = timeout;

	if ((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_AUTO) || (nonVolatileSettings.backlightMode == BACKLIGHT_MODE_SQUELCH))
	{
		// Backlight is OFF, or timeout override (-1) as just been set
		if ((displayIsBacklightLit() == false) || ((timeout == -1) && (prevTimer != -1)))
		{
			displayEnableBacklight(true,nonVolatileSettings.displayBacklightPercentageOff);
		}
	}
}

void menuSystemInit(time_t_custom UTCdateTimeInSecs)
{
	uiEvent_t ev = { .buttons = 0, .keys = NO_KEYCODE, .rotary = 0, .function = 0, .events = NO_EVENT, .hasEvent = false, .time = ticksGetMillis() };

	uiSetUTCDateTimeInSecs(UTCdateTimeInSecs);

	menuDataGlobal.lightTimer = -1;
	menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition] = UI_SPLASH_SCREEN;// set the very first screen as the splash screen
	menuDataGlobal.currentItemIndex = 0;

	if ((nonVolatileSettings.backlightMode == BACKLIGHT_MODE_MANUAL) ||
			(nonVolatileSettings.backlightMode == BACKLIGHT_MODE_BUTTONS) ||
			(nonVolatileSettings.backlightMode == BACKLIGHT_MODE_SQUELCH))
	{
		if (nonVolatileSettings.displayBacklightPercentageOff > 0)
		{
			displayEnableBacklight(false, nonVolatileSettings.displayBacklightPercentageOff);
		}
	}
	else
	{
		displayLightTrigger(false);
	}

	menuFunctions[menuDataGlobal.controlData.stack[menuDataGlobal.controlData.stackPosition]].function(&ev, true);// Init and display this screen
}

void menuSystemLanguageHasChanged(void)
{
	// Force full update of menuChannelMode() on next call (if isFirstRun arg. is true)
	if (menuSystemGetRootMenuNumber() == UI_CHANNEL_MODE)
	{
		uiChannelModeColdStart();
	}
}

const menuItemNewData_t mainMenuItems[] =
{
	{   3, MENU_ZONE_LIST       },
	{   6, MENU_CONTACTS_MENU   },
	{ 282, MENU_SMS             },// messages
	{  12, MENU_CHANNEL_DETAILS },
	{   4, MENU_RSSI_SCREEN     },
	{   8, MENU_FIRMWARE_INFO   },
	{   9, MENU_OPTIONS         },
	{   7, MENU_LAST_HEARD      },
	{ 150, MENU_RADIO_INFOS     },
	{ 173, MENU_SATELLITE       },
#if defined(HAS_GPS)
	{ 195, MENU_GPS		        },
#endif
};

const menuItemsList_t menuDataMainMenu =
{
	.numItems = (sizeof(mainMenuItems) / sizeof(mainMenuItems[0])),
	.items = mainMenuItems
};

static const menuItemNewData_t smsMenuItems[] =
{
	{ 283, MENU_SMS_COMPOSE   },// new message
	{ 284, MENU_SMS_INBOX     },// inbox
	{ 285, MENU_SMS_SENT      },// sent
	{ 286, MENU_SMS_QUICKTEXT },// quick text
	{   9, MENU_SMS_OPTIONS   },// options
};

const menuItemsList_t menuDataSMS =
{
	.numItems = (sizeof(smsMenuItems) / sizeof(smsMenuItems[0])),
	.items = smsMenuItems
};

static const menuItemNewData_t contactMenuItems[] =
{
	{ 15,  MENU_CONTACT_LIST      },
	{ 139, MENU_DTMF_CONTACT_LIST },
	{ 14,  MENU_CONTACT_NEW       },
};

const menuItemsList_t menuDataContact =
{
	.numItems = (sizeof(contactMenuItems) / sizeof(contactMenuItems[0])),
	.items = contactMenuItems
};

static const menuItemNewData_t optionsMenuItems[] =
{
	{ 190, MENU_GENERAL         },
	{ 191, MENU_RADIO           },
	{  10, MENU_DISPLAY         },
	{  11, MENU_SOUND           },
	{  13, MENU_LANGUAGE        },
	{ 206, MENU_CALIBRATION     },
#if defined(HAS_COLOURS)
	{ 218, MENU_THEME           },
#endif
#if !defined(PLATFORM_GD77S)
	{ 257, MENU_APRS            },
#endif
};

const menuItemsList_t menuDataOptions =
{
	.numItems = (sizeof(optionsMenuItems) / sizeof(optionsMenuItems[0])),
	.items = optionsMenuItems
};

void menuDisplayTitle(const char *title)
{
	themeItem_t fgItem, bgItem;

	displayThemeGetForegroundAndBackgroundItems(&fgItem, &bgItem);

#if defined(HAS_COLOURS)
	displayThemeApply(THEME_ITEM_BG_MENU_NAME, THEME_ITEM_COLOUR_NONE);
	displayFillRect(0, 0, DISPLAY_SIZE_X, MENU_HEADER_HEIGHT, false);
#endif

	displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG);
	displayDrawFastHLine(0, (MENU_HEADER_HEIGHT - 1), DISPLAY_SIZE_X, true);

	displayThemeApply(THEME_ITEM_FG_MENU_NAME, THEME_ITEM_BG);
	displayPrintCore(0, 3, title, FONT_SIZE_2, TEXT_ALIGN_CENTER, false);

	displayThemeApply(fgItem, bgItem);
}

// optStart:
//  - if less than 0, entryText will use fgOptItem colour
//  - if equal to 0, no coloured option will be handled

void menuDisplayEntry(int loopOffset, int focusedItem, const char *entryText, int32_t optStart, themeItem_t fgItem, themeItem_t fgOptItem, themeItem_t bgItem)
{
	bool focused = (focusedItem == menuDataGlobal.currentItemIndex);
	int16_t textXPos = DISPLAY_X_POS_MENU_TEXT_OFFSET;

	if (focused)
	{
		displayThemeApply(THEME_ITEM_BG_MENU_ITEM_SELECTED, bgItem);
		displayFillRoundRect(DISPLAY_X_POS_MENU_OFFSET, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT
#if defined(PLATFORM_RD5R)
				- 1 // Small V offset due to small font usage
#endif
				+ (loopOffset * MENU_ENTRY_HEIGHT), DISPLAY_SIZE_X - (DISPLAY_X_POS_MENU_OFFSET * 2), MENU_ENTRY_HEIGHT, 2, true);
	}

	displayThemeApply(fgItem, bgItem);

	if ((focused == false) && ((optStart < 0) || (optStart > 0)))
	{
		char buffer[SCREEN_LINE_BUFFER_SIZE];

		snprintf(buffer, (optStart > 0) ? (optStart + 1) : (strlen(entryText) + 1), "%s", entryText);

		if (optStart < 0)
		{
			displayThemeApply(fgOptItem, bgItem);
		}

		displayPrintCore(textXPos, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT + (loopOffset * MENU_ENTRY_HEIGHT), buffer, FONT_SIZE_3, TEXT_ALIGN_LEFT, focused);

		if (optStart > 0)
		{
			displayThemeApply(fgOptItem, bgItem);
			displayPrintCore(textXPos + (optStart * 8), DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT + (loopOffset * MENU_ENTRY_HEIGHT), (entryText + optStart), FONT_SIZE_3, TEXT_ALIGN_LEFT, focused);
		}
	}
	else
	{
		displayPrintCore(textXPos, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT + (loopOffset * MENU_ENTRY_HEIGHT), entryText, FONT_SIZE_3, TEXT_ALIGN_LEFT, focused);
	}

	displayThemeResetToDefault();
}

// Returns menu offset, -1 if the line is before the first menu item, -2 if the line is after the last menu item
int menuGetMenuOffset(int maxMenuItems, int loopOffset)
{
	int offset = menuDataGlobal.currentItemIndex + loopOffset;
	int startOffset = 0;
	int iter = (loopOffset + (MENU_MAX_DISPLAYED_ENTRIES / 2) + ((MENU_MAX_DISPLAYED_ENTRIES & 0x01) ? 1 : 0));

	if (maxMenuItems < MENU_MAX_DISPLAYED_ENTRIES)
	{
		startOffset = (((MENU_MAX_DISPLAYED_ENTRIES - ((MENU_MAX_DISPLAYED_ENTRIES & 0x01) ? 0 : 1)) - maxMenuItems) / 2);

		if (iter <= startOffset)
		{
			return MENU_OFFSET_BEFORE_FIRST_ENTRY;
		}
		else if (iter > (startOffset + maxMenuItems))
		{
			return MENU_OFFSET_AFTER_LAST_ENTRY;
		}
	}

	if (offset < 0)
	{
		if ((maxMenuItems == 1) && (maxMenuItems < MENU_MAX_DISPLAYED_ENTRIES))
		{
			offset = MENU_MAX_DISPLAYED_ENTRIES - 1;
		}
		else
		{
			offset = maxMenuItems + offset;
		}
	}

	if (offset >= maxMenuItems)
	{
		offset = offset - maxMenuItems;
	}

	return offset;
}

/*
 * Returns 99 if key is unknown, or not numerical when digitsOnly is true
 */
#if defined(PLATFORM_MD9600)
#define NON_NUMERICAL_KEYS 7
#else
#define NON_NUMERICAL_KEYS 6
#endif
int menuGetKeypadKeyValue(uiEvent_t *ev, bool digitsOnly)
{
#if !defined(PLATFORM_GD77S)
	uint32_t keypadKeys[] =
	{
#if defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380)
			KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9,
			KEY_UP, 0, KEY_DOWN, 0, KEY_STAR, KEY_HASH
#elif defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
			KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9,
			KEY_LEFT, KEY_UP, KEY_DOWN, KEY_RIGHT, KEY_STAR, KEY_HASH
#elif defined(PLATFORM_MD9600)
			KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9,
			KEY_A, KEY_B, KEY_C, KEY_D, KEY_STAR, KEY_HASH, KEY_GREEN
#else // MK22, minus GD77S
			KEY_0, KEY_1, KEY_2, KEY_3, KEY_4, KEY_5, KEY_6, KEY_7, KEY_8, KEY_9,
			KEY_LEFT, KEY_UP, KEY_DOWN, KEY_RIGHT, KEY_STAR, KEY_HASH
#endif
	};

	for (int i = 0; i < ((sizeof(keypadKeys) / sizeof(keypadKeys[0])) - (digitsOnly ? NON_NUMERICAL_KEYS : 0 )); i++)
	{
		if (
#if defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380)
				(keypadKeys[i] != 0) &&
#endif
				KEYCHECK_PRESS(ev->keys, keypadKeys[i]))
		{
#if defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380)
			if (BUTTONCHECK_DOWN(ev, BUTTON_SK1))
			{
				if (i == 10) // Simulate 'B'
				{
					return 11;
				}
				else if (i == 12) // Simulate 'D'
				{
					return 13;
				}

			}
#endif
			return i;
		}
	}
#endif // GD77S

	return 99;
}

void menuUpdateCursor(int pos, bool moved, bool render)
{
#if defined(PLATFORM_RD5R)
	const int MENU_CURSOR_Y = 35;
#elif defined(PLATFORM_MDUV380) || defined(PLATFORM_MD380) || defined(PLATFORM_RT84_DM1701) || defined(PLATFORM_MD2017)
#if defined(PLATFORM_VARIANT_DM1701)
	const int MENU_CURSOR_Y = 66;
#else
	const int MENU_CURSOR_Y = 78;
#endif
#else
	const int MENU_CURSOR_Y = 46;
#endif

	static uint32_t lastBlink = 0;
	static bool     blink = false;
	uint32_t        m = ticksGetMillis();

	if (moved)
	{
		blink = true;
	}

	if (moved || (m - lastBlink) > 500)
	{
		displayDrawFastHLine(((pos * 8) + DISPLAY_X_POS_MENU_TEXT_OFFSET), MENU_CURSOR_Y, 8, blink);

		blink = !blink;
		lastBlink = m;

		if (render)
		{
			displayRenderRows(MENU_CURSOR_Y / 8, MENU_CURSOR_Y / 8 + 1);
		}
	}
}

void moveCursorLeftInString(char *str, int *pos, bool delete)
{
	int nLen = strlen(str);

	if (*pos > 0)
	{
		if (nLen == 16)
		{
			if (*pos != 15)
			{
				*pos -=1;
			}
		}
		else
		{
			*pos -=1;
		}

		announceChar(str[*pos]); // speak the new char or the char about to be backspaced out.

		if (delete)
		{
			for (int i = *pos; i <= nLen; i++)
			{
				str[i] = str[i + 1];
			}
		}
	}
}

void moveCursorRightInString(char *str, int *pos, int max, bool insert)
{
	int nLen = strlen(str);

	if (*pos < strlen(str))
	{
		if (insert)
		{
			if (nLen < max)
			{
				for (int i = nLen; i > *pos; i--)
				{
					str[i] = str[i - 1];
				}
				str[*pos] = ' ';
			}
		}

		if (*pos < max-1)
		{
			*pos += 1;
			announceChar(str[*pos]); // speak the new char or the char about to be backspaced out.
		}
	}
}

void menuSystemMenuIncrement(int32_t *currentItem, int32_t numItems)
{
	*currentItem = (*currentItem + 1) % numItems;
}

void menuSystemMenuDecrement(int32_t *currentItem, int32_t numItems)
{
	*currentItem = (*currentItem + numItems - 1) % numItems;
}

// For QuickKeys
void menuDisplaySettingOption(const char *entryText, const char *valueText)
{
	displayThemeApply(THEME_ITEM_FG_DECORATION, THEME_ITEM_BG);

#if defined(PLATFORM_RD5R)
	displayDrawRoundRect(2, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT - MENU_ENTRY_HEIGHT - 6, DISPLAY_SIZE_X - 4, (MENU_ENTRY_HEIGHT * 2) + 8, 2, true);
	displayFillRoundRect(2, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT - MENU_ENTRY_HEIGHT - 6, DISPLAY_SIZE_X - 4, MENU_ENTRY_HEIGHT + 3, 2, true);

	displayThemeApply(THEME_ITEM_FG_NOTIFICATION, THEME_ITEM_BG);
	if (entryText)
	{
		displayPrintCore(0, DISPLAY_Y_POS_MENU_START - MENU_ENTRY_HEIGHT - 4, entryText, FONT_SIZE_2, TEXT_ALIGN_CENTER, true);
	}
#else
	displayDrawRoundRect(2, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT - MENU_ENTRY_HEIGHT - 2, DISPLAY_SIZE_X - 4, (MENU_ENTRY_HEIGHT * 2) + 4, 2, true);
	displayFillRoundRect(2, DISPLAY_Y_POS_MENU_ENTRY_HIGHLIGHT - MENU_ENTRY_HEIGHT - 2, DISPLAY_SIZE_X - 4, MENU_ENTRY_HEIGHT, 2, true);

	displayThemeApply(THEME_ITEM_FG_NOTIFICATION, THEME_ITEM_BG);
	if (entryText)
	{
		displayPrintCore(0, DISPLAY_Y_POS_MENU_START - MENU_ENTRY_HEIGHT + DISPLAY_V_OFFSET + 2, entryText, FONT_SIZE_2, TEXT_ALIGN_CENTER, true);
	}
#endif

	displayPrintCore(0, DISPLAY_Y_POS_MENU_START + DISPLAY_V_OFFSET, valueText, FONT_SIZE_3, TEXT_ALIGN_CENTER, false);

	displayThemeResetToDefault();
}
