/*
** menudef.cpp
** MENUDEF parser amd menu generation code
**
**---------------------------------------------------------------------------
** Copyright 2010 Christoph Oelckers
** All rights reserved.
**
** 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. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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 <float.h>

#include "menu.h"
#include "c_dispatch.h"
#include "filesystem.h"
#include "sc_man.h"
#include "v_font.h"
#include "c_bind.h"
#include "d_event.h"
#include "d_gui.h"
#include "printf.h"
#include "gamecontrol.h"
#include "cmdlib.h"
#include "c_cvars.h"
#include "optionmenuitems.h"
#include "i_soundfont.h"
#include "zstring.h"
#include "texturemanager.h"
#include "v_video.h"
#include <zmusic.h>

// Menu-relevant content that gets filled in by scripts. This will get processed after the game has loaded.
FString gSkillNames[MAXSKILLS];
FString gVolumeNames[MAXVOLUMES];
FString gVolumeSubtitles[MAXVOLUMES];
int32_t gVolumeFlags[MAXVOLUMES];
int gDefaultVolume = 0, gDefaultSkill = 1;
MenuGameplayStemEntry g_MenuGameplayEntries[MAXMENUGAMEPLAYENTRIES];

MenuDescriptorList MenuDescriptors;
static FListMenuDescriptor DefaultListMenuSettings;	// contains common settings for all list menus
static FOptionMenuDescriptor DefaultOptionMenuSettings;	// contains common settings for all Option menus
FOptionMenuSettings OptionSettings;
FOptionMap OptionValues;

void I_BuildMIDIMenuList(FOptionValues* opt);
void I_BuildALDeviceList(FOptionValues *opt);
void I_BuildALResamplersList(FOptionValues* opt);

bool IsOpenALPresent();

void M_DeinitMenus()
{
	{
		MenuDescriptorList::Iterator it(MenuDescriptors);

		MenuDescriptorList::Pair *pair;

		while (it.NextPair(pair))
		{
			delete pair->Value;
			pair->Value = NULL;
		}
	}

	{
		FOptionMap::Iterator it(OptionValues);

		FOptionMap::Pair *pair;

		while (it.NextPair(pair))
		{
			delete pair->Value;
			pair->Value = NULL;
		}
	}
	MenuDescriptors.Clear();
	OptionValues.Clear();

	DMenu::CurrentMenu = NULL;
	DefaultListMenuSettings.mItems.Clear();
}

static FGameTexture* GetMenuTexture(const char* const name)
{
	auto texid = TexMan.CheckForTexture(name, ETextureType::Any);
	if (!texid.isValid())
	{
		Printf("Missing menu texture: \"%s\"\n", name);
	}

	return TexMan.GetGameTexture(texid);
}

//=============================================================================
//
//
//
//=============================================================================

static void SkipSubBlock(FScanner &sc)
{
	sc.MustGetStringName("{");
	int depth = 1;
	while (depth > 0)
	{
		sc.MustGetString();
		if (sc.Compare("{")) depth++;
		if (sc.Compare("}")) depth--;
	}
}

//=============================================================================
//
//
//
//=============================================================================

struct gamefilter
{
	const char* gamename;
	int gameflag;
};

static const gamefilter games[] = {
	{ "Duke", GAMEFLAG_DUKE},
	{ "Nam", GAMEFLAG_NAM|GAMEFLAG_NAPALM},
	{ "NamOnly", GAMEFLAG_NAM},	// for cases where the difference matters.
	{ "Napalm", GAMEFLAG_NAPALM},
	{ "WW2GI", GAMEFLAG_WW2GI},
	{ "Fury", GAMEFLAG_FURY},
	{ "Redneck", GAMEFLAG_RR},
	{ "RedneckRides", GAMEFLAG_RRRA},
	{ "Deer", GAMEFLAG_DEER},
	{ "Blood", GAMEFLAG_BLOOD},
	{ "ShadowWarrior", GAMEFLAG_SW},
	{ "Exhumed", GAMEFLAG_POWERSLAVE|GAMEFLAG_EXHUMED},
};

// for other parts that need to filter by game name.
bool validFilter(const char *str)
{
	for (auto &gf : games)
	{
		if (g_gameType & gf.gameflag)
		{
			if (!stricmp(str, gf.gamename)) return true;
		}
	}
	return false;
}


static bool CheckSkipGameBlock(FScanner &sc)
{
	int filter = 0;
	sc.MustGetStringName("(");
	do
	{
		sc.MustGetString();
		int gi = sc.MustMatchString(&games[0].gamename, sizeof(games[0]));

		filter |= games[gi].gameflag;
	}
	while (sc.CheckString(","));
	sc.MustGetStringName(")");
	if (!(filter & g_gameType))
	{
		SkipSubBlock(sc);
		return !sc.CheckString("else");
	}
	return false;
}

//=============================================================================
//
//
//
//=============================================================================

static bool CheckSkipOptionBlock(FScanner &sc)
{
	bool filter = false;
	sc.MustGetStringName("(");
	do
	{
		sc.MustGetString();
		if (sc.Compare("Windows"))
		{
			#ifdef _WIN32
				filter = true;
			#endif
		}
		else if (sc.Compare("unix"))
		{
			#ifdef __unix__
				filter = true;
			#endif
		}
		else if (sc.Compare("Mac"))
		{
			#ifdef __APPLE__
				filter = true;
			#endif
		}

		if (sc.Compare("openal"))
		{
			if (IsOpenALPresent())
				filter = true;
		}
	}
	while (sc.CheckString(","));
	sc.MustGetStringName(")");
	if (!filter)
	{
		SkipSubBlock(sc);
		return !sc.CheckString("else");
	}
	return false;
}

//=============================================================================
//
//
//
//=============================================================================

static bool CheckSkipNoSwBlock(FScanner& sc)
{
	sc.MustGetStringName("(");
	sc.MustGetString();
	bool res = sc.Compare("true");
	sc.MustGetStringName(")");
	if ((!(g_gameType & GAMEFLAG_SHAREWARE)) == res)
	{
		SkipSubBlock(sc);
		return !sc.CheckString("else");
	}
	return false;
}

//=============================================================================
//
//
//
//=============================================================================

static void ParseListMenuBody(FScanner &sc, FListMenuDescriptor *desc)
{
	sc.MustGetStringName("{");
	while (!sc.CheckString("}"))
	{
		sc.MustGetString();
		if (sc.Compare("else"))
		{
			SkipSubBlock(sc);
		}
		else if (sc.Compare("ifgame"))
		{
			if (!CheckSkipGameBlock(sc))
			{
				// recursively parse sub-block
				ParseListMenuBody(sc, desc);
			}
		}
		else if (sc.Compare("ifoption"))
		{
			if (!CheckSkipOptionBlock(sc))
			{
				// recursively parse sub-block
				ParseListMenuBody(sc, desc);
			}
		}
		else if (sc.Compare("ifshareware"))
		{
			if (!CheckSkipNoSwBlock(sc))
			{
				// recursively parse sub-block
				ParseListMenuBody(sc, desc);
			}
		}
		else if (sc.Compare("Class"))
		{
			sc.MustGetString();
			FString s = sc.String;
			s.Substitute("$", gi->Name());
			desc->mClass = s;
		}
		else if (sc.Compare("Selector"))
		{
			sc.MustGetString();
			desc->mSelector = GetMenuTexture(sc.String);
			sc.MustGetStringName(",");
			sc.MustGetNumber();
			desc->mSelectOfsX = sc.Number;
			sc.MustGetStringName(",");
			sc.MustGetNumber();
			desc->mSelectOfsY = sc.Number;
		}
		else if (sc.Compare("Linespacing"))
		{
			sc.MustGetNumber();
			desc->mLinespacing = sc.Number;
		}
		else if (sc.Compare("Position"))
		{
			sc.MustGetNumber();
			desc->mXpos = sc.Number;
			sc.MustGetStringName(",");
			sc.MustGetNumber();
			desc->mYpos = sc.Number;
			if (sc.CheckString(","))
			{
				sc.MustGetNumber();
				desc->mYbotton = sc.Number;
			}
		}
		else if (sc.Compare("Centermenu"))
		{
			desc->mFlags |= LMF_Centered;
		}
		else if (sc.Compare("animatedtransition"))
		{
			desc->mFlags |= LMF_Animate;
		}
		else if (sc.Compare("Fixedspacing"))
		{
			sc.MustGetNumber();
			desc->mSpacing = sc.Number;
		}
		else if (sc.Compare("MouseWindow"))
		{
			sc.MustGetNumber();
			desc->mWLeft = sc.Number;
			sc.MustGetStringName(",");
			sc.MustGetNumber();
			desc->mWRight = sc.Number;
		}
		else if (sc.Compare("StaticPatch") || sc.Compare("StaticPatchCentered"))
		{
			bool centered = sc.Compare("StaticPatchCentered");
			sc.MustGetNumber();
			int x = sc.Number;
			sc.MustGetStringName(",");
			sc.MustGetNumber();
			int y = sc.Number;
			sc.MustGetStringName(",");
			sc.MustGetString();
			auto tex = GetMenuTexture(sc.String);

			FListMenuItem *it = new FListMenuItemStaticPatch(x, y, tex, centered);
			desc->mItems.Push(it);
		}
		else if (sc.Compare("ScriptId"))
		{
			sc.MustGetNumber();
			desc->mScriptId = sc.Number;
		}
		else if (sc.Compare("Caption"))
		{
			sc.MustGetString();
			desc->mCaption = sc.String;
		}
		else if (sc.Compare("StaticText") || sc.Compare("StaticTextCentered"))
		{
			bool centered = sc.Compare("StaticTextCentered");
			sc.MustGetNumber();
			int x = sc.Number;
			sc.MustGetStringName(",");
			sc.MustGetNumber();
			int y = sc.Number;
			sc.MustGetStringName(",");
			sc.MustGetString();
			FListMenuItem *it = new FListMenuItemStaticText(x, y, sc.String, desc->mFont, desc->mFontColor, centered);
			desc->mItems.Push(it);
		}
		else if (sc.Compare("PatchItem"))
		{
			sc.MustGetString();
			auto tex = GetMenuTexture(sc.String);
			sc.MustGetStringName(",");
			sc.MustGetString();
			int hotkey = sc.String[0];
			sc.MustGetStringName(",");
			sc.MustGetString();
			FName action = sc.String;
			int param = 0;
			if (sc.CheckString(","))
			{
				sc.MustGetNumber();
				param = sc.Number;
			}

			FListMenuItem *it = new FListMenuItemPatch(desc->mXpos, desc->mYpos, desc->mLinespacing, hotkey, tex, action, param);
			desc->mItems.Push(it);
			desc->mYpos += desc->mLinespacing;
			if (desc->mSelectedItem == -1) desc->mSelectedItem = desc->mItems.Size()-1;
		}
		else if (sc.Compare("TextItem"))
		{
			sc.MustGetString();
			FString text = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetString();
			int hotkey = sc.String[0];
			sc.MustGetStringName(",");
			sc.MustGetString();
			FName action = sc.String;
			int param = 0;
			if (sc.CheckString(","))
			{
				sc.MustGetNumber();
				param = sc.Number;
			}

			FListMenuItem *it = new FListMenuItemText(desc->mXpos, desc->mYpos, desc->mLinespacing, hotkey, text, desc->mFont, desc->mFontColor, desc->mFontColor2, action, param);
			desc->mItems.Push(it);
			desc->mYpos += desc->mLinespacing;
			if (desc->mSelectedItem == -1) desc->mSelectedItem = desc->mItems.Size()-1;

		}
		else if (sc.Compare("NativeTextItem"))
		{
		sc.MustGetString();
		FString text = sc.String;
		sc.MustGetStringName(",");
		sc.MustGetString();
		int hotkey = sc.String[0];
		sc.MustGetStringName(",");
		sc.MustGetString();
		FName action = sc.String;
		int param = 0;
		if (sc.CheckString(","))
		{
			sc.MustGetNumber();
			param = sc.Number;
		}

		auto it = new FListMenuItemNativeText(desc->mXpos, desc->mYpos, desc->mLinespacing, hotkey, text, desc->mNativeFontNum, desc->mNativePalNum, desc->mNativeFontScale, action, param);
		desc->mItems.Push(it);
		desc->mYpos += desc->mLinespacing;
		if (desc->mSelectedItem == -1) desc->mSelectedItem = desc->mItems.Size() - 1;

		}
		else if (sc.Compare("NativeFont"))
		{
			desc->mNativePalNum = NIT_ActiveColor;
			desc->mNativeFontScale = 1.f;
			sc.MustGetString();
			if (sc.Compare("Big")) desc->mNativeFontNum = NIT_BigFont;
			else if (sc.Compare("Small")) desc->mNativeFontNum = NIT_SmallFont;
			else if (sc.Compare("Tiny")) desc->mNativeFontNum = NIT_TinyFont;
			else sc.ScriptError("Unknown native font type");
			if (sc.CheckString(","))
			{
				sc.MustGetString();
				if (sc.Compare("Active")) desc->mNativePalNum = NIT_ActiveColor;
				else if (sc.Compare("Inactive")) desc->mNativePalNum = NIT_InactiveColor;
				else if (sc.Compare("Selected")) desc->mNativePalNum = NIT_SelectedColor;
				else
				{
					char* ep;
					int v = (int)strtoll(sc.String, &ep, 0);
					if (*ep != 0) sc.ScriptError("Unknown native palette");
					desc->mNativePalNum = v;
				}
				if (sc.CheckString(","))
				{
					sc.MustGetFloat();
					desc->mNativeFontScale = sc.Float;
				}			
			}
		}
		else if (sc.Compare("Font"))
		{
		sc.MustGetString();
		FFont* newfont = V_GetFont(sc.String);
		if (newfont != NULL) desc->mFont = newfont;
		if (sc.CheckString(","))
		{
			sc.MustGetString();
			desc->mFontColor2 = desc->mFontColor = V_FindFontColor((FName)sc.String);
			if (sc.CheckString(","))
			{
				sc.MustGetString();
				desc->mFontColor2 = V_FindFontColor((FName)sc.String);
				}
			}
		else
		{
			desc->mFontColor = OptionSettings.mFontColor;
			desc->mFontColor2 = OptionSettings.mFontColorValue;
		}
		}
		else if (sc.Compare("NetgameMessage"))
		{
			sc.MustGetString();
			desc->mNetgameMessage = sc.String;
		}
		else
		{
			sc.ScriptError("Unknown keyword '%s'", sc.String);
		}
	}
}

//=============================================================================
//
//
//
//=============================================================================

static bool CheckCompatible(FMenuDescriptor *newd, FMenuDescriptor *oldd)
{
	/*if (oldd->mClass == NULL)*/ return true;
	//return oldd->mClass == newd->mClass;
}

static bool ReplaceMenu(FScanner &sc, FMenuDescriptor *desc)
{
	FMenuDescriptor **pOld = MenuDescriptors.CheckKey(desc->mMenuName);
	if (pOld != NULL && *pOld != NULL) 
	{
		if (CheckCompatible(desc, *pOld))
		{
			delete *pOld;
		}
		else
		{
			sc.ScriptMessage("Tried to replace menu '%s' with a menu of different type", desc->mMenuName.GetChars());
			return true;
		}
	}
	MenuDescriptors[desc->mMenuName] = desc;
	return false;
}

//=============================================================================
//
//
//
//=============================================================================

static void ParseListMenu(FScanner &sc)
{
	sc.MustGetString();

	FListMenuDescriptor *desc = new FListMenuDescriptor;
	desc->mType = MDESC_ListMenu;
	desc->mMenuName = sc.String;
	desc->mSelectedItem = -1;
	desc->mAutoselect = -1;
	desc->mSelectOfsX = DefaultListMenuSettings.mSelectOfsX;
	desc->mSelectOfsY = DefaultListMenuSettings.mSelectOfsY;
	desc->mSelector = DefaultListMenuSettings.mSelector;
	desc->mDisplayTop = DefaultListMenuSettings.mDisplayTop;
	desc->mXpos = DefaultListMenuSettings.mXpos;
	desc->mYpos = DefaultListMenuSettings.mYpos;
	desc->mLinespacing = DefaultListMenuSettings.mLinespacing;
	desc->mNetgameMessage = DefaultListMenuSettings.mNetgameMessage;
	desc->mFont = DefaultListMenuSettings.mFont;
	desc->mFontColor = DefaultListMenuSettings.mFontColor;
	desc->mFontColor2 = DefaultListMenuSettings.mFontColor2;
	desc->mClass = NULL;
	desc->mRedirect = NULL;
	desc->mWLeft = 0;
	desc->mWRight = 0;

	ParseListMenuBody(sc, desc);
	bool scratch = ReplaceMenu(sc, desc);
	if (scratch) delete desc;
}

//=============================================================================
//
//
//
//=============================================================================

static void ParseImageScrollerBody(FScanner &sc, FImageScrollerDescriptor *desc)
{
	sc.MustGetStringName("{");
	while (!sc.CheckString("}"))
	{
		sc.MustGetString();
		if (sc.Compare("else"))
		{
			SkipSubBlock(sc);
		}
		else if (sc.Compare("ifgame"))
		{
			if (!CheckSkipGameBlock(sc))
			{
				// recursively parse sub-block
				ParseImageScrollerBody(sc, desc);
			}
		}
		else if (sc.Compare("ifshareware"))
		{
			if (!CheckSkipNoSwBlock(sc))
			{
				// recursively parse sub-block
				ParseImageScrollerBody(sc, desc);
			}
		}
		else if (sc.Compare("ifoption"))
		{
			if (!CheckSkipOptionBlock(sc))
			{
				// recursively parse sub-block
				ParseImageScrollerBody(sc, desc);
			}
		}
		else if (sc.Compare("Class"))
		{
			sc.MustGetString();
			FString s = sc.String;
			s.Substitute("$", gi->Name());
			desc->mClass = s;
		}
		else if (sc.Compare("TextItem") || sc.Compare("ImageItem"))
		{
			FImageScrollerDescriptor::ScrollerItem item;
			int type = sc.Compare("TextItem");
			sc.MustGetString();
			item.text = strbin1(sc.String);
			if (type)
			{
				sc.MustGetStringName(",");
				sc.MustGetNumber();
				item.type = sc.Number; // y-coordinate
			}
			else item.type = 0;
			item.scriptID = INT_MAX;
			if (sc.CheckString(","))
			{
				sc.MustGetNumber();
				item.scriptID = sc.Number;
			}
			desc->mItems.Push(item);
		}
		else if (sc.Compare("qavanimationitem"))
		{
			if (!(g_gameType & GAMEFLAG_BLOOD))
			{
				I_Error("QAV animations not available!"); // these (currently) only exist in Blood.
			}
			FImageScrollerDescriptor::ScrollerItem item;
			sc.GetString();
			item.text = sc.String;
			item.type = -1;
			item.scriptID = INT_MAX;
			desc->mItems.Push(item);
		}
		else if (sc.Compare("animatedtransition"))
		{
			desc->mFlags |= LMF_Animate;
		}
		else
		{
			sc.ScriptError("Unknown keyword '%s'", sc.String);
		}
	}
}


//=============================================================================
//
//
//
//=============================================================================

static void ParseImageScroller(FScanner &sc)
{
	sc.MustGetString();

	FImageScrollerDescriptor *desc = new FImageScrollerDescriptor;
	desc->mType = MDESC_ImageScroller;
	desc->mMenuName = sc.String;
	desc->mClass = NAME_None;

	ParseImageScrollerBody(sc, desc);
	bool scratch = ReplaceMenu(sc, desc);
	if (scratch) delete desc;
}

//=============================================================================
//
//
//
//=============================================================================

static void ParseOptionValue(FScanner &sc)
{
	FName optname;

	FOptionValues *val = new FOptionValues;
	sc.MustGetString();
	optname = sc.String;
	sc.MustGetStringName("{");
	while (!sc.CheckString("}"))
	{
		FOptionValues::Pair &pair = val->mValues[val->mValues.Reserve(1)];
		sc.MustGetFloat();
		pair.Value = sc.Float;
		sc.MustGetStringName(",");
		sc.MustGetString();
		pair.Text = strbin1(sc.String);
	}
	FOptionValues **pOld = OptionValues.CheckKey(optname);
	if (pOld != NULL && *pOld != NULL) 
	{
		delete *pOld;
	}
	OptionValues[optname] = val;
}


//=============================================================================
//
//
//
//=============================================================================

static void ParseOptionString(FScanner &sc)
{
	FName optname;

	FOptionValues *val = new FOptionValues;
	sc.MustGetString();
	optname = sc.String;
	sc.MustGetStringName("{");
	while (!sc.CheckString("}"))
	{
		FOptionValues::Pair &pair = val->mValues[val->mValues.Reserve(1)];
		sc.MustGetString();
		pair.Value = DBL_MAX;
		pair.TextValue = sc.String;
		sc.MustGetStringName(",");
		sc.MustGetString();
		pair.Text = strbin1(sc.String);
	}
	FOptionValues **pOld = OptionValues.CheckKey(optname);
	if (pOld != NULL && *pOld != NULL) 
	{
		delete *pOld;
	}
	OptionValues[optname] = val;
}


//=============================================================================
//
//
//
//=============================================================================

static void ParseOptionSettings(FScanner &sc)
{
	sc.MustGetStringName("{");
	while (!sc.CheckString("}"))
	{
		sc.MustGetString();
		if (sc.Compare("else"))
		{
			SkipSubBlock(sc);
		}
		else if (sc.Compare("ifgame"))
		{
			if (!CheckSkipGameBlock(sc))
			{
				// recursively parse sub-block
				ParseOptionSettings(sc);
			}
		}
		else if (sc.Compare("Linespacing"))
		{
			sc.MustGetNumber();
			OptionSettings.mLinespacing = sc.Number;
		}
		else if (sc.Compare("LabelOffset"))
		{
			sc.MustGetNumber();
			// ignored
		}
		else
		{
			sc.ScriptError("Unknown keyword '%s'", sc.String);
		}
	}
}

//=============================================================================
//
//
//
//=============================================================================

static void ParseOptionMenuBody(FScanner &sc, FOptionMenuDescriptor *desc)
{
	sc.MustGetStringName("{");
	while (!sc.CheckString("}"))
	{
		sc.MustGetString();
		if (sc.Compare("else"))
		{
			SkipSubBlock(sc);
		}
		else if (sc.Compare("ifgame"))
		{
			if (!CheckSkipGameBlock(sc))
			{
				// recursively parse sub-block
				ParseOptionMenuBody(sc, desc);
			}
		}
		else if (sc.Compare("ifshareware"))
		{
			if (!CheckSkipNoSwBlock(sc))
			{
				// recursively parse sub-block
				ParseOptionMenuBody(sc, desc);
			}
		}
		else if (sc.Compare("ifoption"))
		{
			if (!CheckSkipOptionBlock(sc))
			{
				// recursively parse sub-block
				ParseOptionMenuBody(sc, desc);
			}
		}
		else if (sc.Compare("Class"))
		{
			sc.MustGetString();
			FString s = sc.String;
			s.Substitute("$", gi->Name());
			desc->mClass = s;
		}
		else if (sc.Compare("Title") || sc.Compare("Caption"))
		{
			sc.MustGetString();
			desc->mTitle = sc.String;
		}
		else if (sc.Compare("Position"))
		{
			sc.MustGetNumber();
			desc->mPosition = sc.Number;
		}
		else if (sc.Compare("DefaultSelection"))
		{
			sc.MustGetNumber();
			desc->mSelectedItem = sc.Number;
		}
		else if (sc.Compare("ScrollTop"))
		{
			sc.MustGetNumber();
			desc->mScrollTop = sc.Number;
		}
		else if (sc.Compare("Indent"))
		{
			sc.MustGetNumber();
			desc->mIndent = sc.Number;
		}
		else if (sc.Compare("Submenu"))
		{
			sc.MustGetString();
			FString label = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetString();
			FOptionMenuItem *it = new FOptionMenuItemSubmenu(label, sc.String);
			desc->mItems.Push(it);
		}
		else if (sc.Compare("LabeledSubmenu"))
		{
			sc.MustGetString();
			FString label = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetString();
			auto cvar = FindCVar(sc.String, nullptr);
			sc.MustGetStringName(",");
			sc.MustGetString();
			FOptionMenuItem* it = new FOptionMenuItemLabeledSubmenu(label, cvar, sc.String);
			desc->mItems.Push(it);
		}
		else if (sc.Compare("Option"))
		{
			sc.MustGetString();
			FString label = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetString();
			FString cvar = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetString();
			FString values = sc.String;
			FString check;
			int center = 0;
			if (sc.CheckString(","))
			{
				sc.MustGetString();
				if (*sc.String != 0) check = sc.String;
				if (sc.CheckString(","))
				{
					sc.MustGetNumber();
					center = sc.Number;
				}
			}
			FOptionMenuItem *it = new FOptionMenuItemOption(label, cvar, values, check, center);
			desc->mItems.Push(it);
		}
		else if (sc.Compare("Command"))
		{
			sc.MustGetString();
			FString label = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetString();
			FOptionMenuItem *it = new FOptionMenuItemCommand(label, sc.String);
			desc->mItems.Push(it);
		}
		else if (sc.Compare("SafeCommand"))
		{
			sc.MustGetString();
			FString label = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetString();
			FOptionMenuItem *it = new FOptionMenuItemSafeCommand(label, sc.String);
			desc->mItems.Push(it);
		}
		else if (sc.Compare("Control") || sc.Compare("MapControl"))
		{
			bool map = sc.Compare("MapControl");
			sc.MustGetString();
			FString label = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetString();
			FOptionMenuItem *it = new FOptionMenuItemControl(label, sc.String, map? &AutomapBindings : &Bindings);
			desc->mItems.Push(it);
		}
		else if (sc.Compare("StaticText"))
		{
			sc.MustGetString();
			FString label = sc.String;
			EColorRange cr = OptionSettings.mFontColorHeader;
			if (sc.CheckString(","))
			{
				sc.MustGetNumber();
				cr = sc.Number? OptionSettings.mFontColorHeader : OptionSettings.mFontColor;	// fixme!
			}
			FOptionMenuItem *it = new FOptionMenuItemStaticText(label, cr);
			desc->mItems.Push(it);
		}
		else if (sc.Compare("StaticTextSwitchable"))
		{
			sc.MustGetString();
			FString label = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetString();
			FString label2 = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetString();
			FName action = sc.String;
			EColorRange cr = OptionSettings.mFontColorHeader;
			if (sc.CheckString(","))
			{
				sc.MustGetNumber();
				cr = sc.Number ? OptionSettings.mFontColorHeader : OptionSettings.mFontColor;	// fixme!
			}
			FOptionMenuItem *it = new FOptionMenuItemStaticTextSwitchable(label, label2, action, cr);
			desc->mItems.Push(it);
		}
		else if (sc.Compare("Slider"))
		{
			sc.MustGetString();
			FString text = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetString();
			FString action = sc.String;
			sc.MustGetStringName(",");
			sc.MustGetFloat();
			double min = sc.Float;
			sc.MustGetStringName(",");
			sc.MustGetFloat();
			double max = sc.Float;
			sc.MustGetStringName(",");
			sc.MustGetFloat();
			double step = sc.Float;
			int showvalue = 1;
			if (sc.CheckString(","))
			{
				sc.MustGetNumber();
				showvalue = sc.Number;
			}
			FOptionMenuItem *it = new FOptionMenuSliderCVar(text, action, min, max, step, showvalue);
			desc->mItems.Push(it);
		}
		// [TP] -- Text input widget
		else if ( sc.Compare( "TextField" ))
		{
			sc.MustGetString();
			FString label = sc.String;
			sc.MustGetStringName( "," );
			sc.MustGetString();
			FString cvar = sc.String;
			FString check;
			
			if ( sc.CheckString( "," ))
			{
				sc.MustGetString();
				check = sc.String;
			}

			FOptionMenuItem* it = new FOptionMenuTextField( label, cvar, check );
			desc->mItems.Push( it );
		}
		// [TP] -- Number input widget
		else if ( sc.Compare( "NumberField" ))
		{
			sc.MustGetString();
			FString label = sc.String;
			sc.MustGetStringName( "," );
			sc.MustGetString();
			FString cvar = sc.String;
			float minimum = 0.0f;
			float maximum = 100.0f;
			float step = 1.0f;
			FString check;

			if ( sc.CheckString( "," ))
			{
				sc.MustGetFloat();
				minimum = (float) sc.Float;
				sc.MustGetStringName( "," );
				sc.MustGetFloat();
				maximum = (float) sc.Float;

				if ( sc.CheckString( "," ))
				{
					sc.MustGetFloat();
					step = (float) sc.Float;

					if ( sc.CheckString( "," ))
					{
						sc.MustGetString();
						check = sc.String;
					}
				}
			}

			FOptionMenuItem* it = new FOptionMenuNumberField( label, cvar,
				minimum, maximum, step, check );
			desc->mItems.Push( it );
		}
		else
		{
			sc.ScriptError("Unknown keyword '%s'", sc.String);
		}
	}
}

//=============================================================================
//
//
//
//=============================================================================

static void ParseOptionMenu(FScanner &sc)
{
	sc.MustGetString();

	FOptionMenuDescriptor *desc = new FOptionMenuDescriptor;
	desc->mType = MDESC_OptionsMenu;
	desc->mMenuName = sc.String;
	desc->mSelectedItem = -1;
	desc->mScrollPos = 0;
	desc->mClass = NULL;
	desc->mPosition = DefaultOptionMenuSettings.mPosition;
	desc->mScrollTop = DefaultOptionMenuSettings.mScrollTop;
	desc->mIndent =  DefaultOptionMenuSettings.mIndent;
	desc->mDontDim =  DefaultOptionMenuSettings.mDontDim;

	ParseOptionMenuBody(sc, desc);
	bool scratch = ReplaceMenu(sc, desc);
	if (scratch) delete desc;
}


//=============================================================================
//
//
//
//=============================================================================

void M_ParseMenuDefs()
{
	int lump, lastlump = 0;

	//OptionSettings.mTitleColor = CR_RED;// V_FindFontColor(gameinfo.mTitleColor);
	OptionSettings.mFontColor = CR_RED;
	OptionSettings.mFontColorValue = CR_GRAY;
	OptionSettings.mFontColorMore = CR_GRAY;
	OptionSettings.mFontColorHeader = CR_GOLD;
	OptionSettings.mFontColorHighlight = CR_YELLOW;
	OptionSettings.mFontColorSelection = CR_BRICK;

	if (g_gameType & (GAMEFLAG_NAM | GAMEFLAG_NAPALM | GAMEFLAG_WW2GI))
	{
		OptionSettings.mFontColor = CR_DARKGREEN;
		OptionSettings.mFontColorHeader = CR_DARKGRAY;
		OptionSettings.mFontColorHighlight = CR_WHITE;
		OptionSettings.mFontColorSelection = CR_DARKGREEN;
	}
	else if (g_gameType & GAMEFLAG_BLOOD)
	{
		OptionSettings.mFontColorHeader = CR_DARKGRAY;
		OptionSettings.mFontColorHighlight = CR_WHITE;
		OptionSettings.mFontColorSelection = CR_DARKRED;
	}
	else if (g_gameType & GAMEFLAG_FURY)
	{
		OptionSettings.mFontColor = CR_TEAL;
		OptionSettings.mFontColorHeader = CR_LIGHTBLUE;
		OptionSettings.mFontColorHighlight = CR_ORANGE;
		OptionSettings.mFontColorSelection = CR_GOLD;
	}
	else if (g_gameType & GAMEFLAG_RRALL)
	{
		OptionSettings.mFontColor = CR_BROWN;
		OptionSettings.mFontColorHeader = CR_DARKBROWN;
		OptionSettings.mFontColorHighlight = CR_ORANGE;
		OptionSettings.mFontColorSelection = CR_TAN;
	}
	else if (g_gameType & GAMEFLAG_SW)
	{
		OptionSettings.mFontColorHeader = CR_DARKRED;
		OptionSettings.mFontColorHighlight = CR_WHITE;
	}

	DefaultListMenuSettings.Reset();
	DefaultOptionMenuSettings.Reset();

	M_DeinitMenus();
	while ((lump = fileSystem.FindLumpFullName("engine/menudef.txt", &lastlump)) != -1)
	{
		FScanner sc(lump);

		sc.SetCMode(true);
		while (sc.GetString())
		{
			if (sc.Compare("LISTMENU"))
			{
				ParseListMenu(sc);
			}
			else if (sc.Compare("ImageScroller"))
			{
				ParseImageScroller(sc);
			}
			else if (sc.Compare("DEFAULTLISTMENU"))
			{
				ParseListMenuBody(sc, &DefaultListMenuSettings);
				if (DefaultListMenuSettings.mItems.Size() > 0)
				{
					I_Error("You cannot add menu items to the menu default settings.");
				}
			}
			else if (sc.Compare("OPTIONVALUE"))
			{
				ParseOptionValue(sc);
			}
			else if (sc.Compare("OPTIONSTRING"))
			{
				ParseOptionString(sc);
			}
			else if (sc.Compare("OPTIONMENUSETTINGS"))
			{
				ParseOptionSettings(sc);
			}
			else if (sc.Compare("OPTIONMENU"))
			{
				ParseOptionMenu(sc);
			}
			else if (sc.Compare("DEFAULTOPTIONMENU"))
			{
				ParseOptionMenuBody(sc, &DefaultOptionMenuSettings);
				if (DefaultOptionMenuSettings.mItems.Size() > 0)
				{
					I_Error("You cannot add menu items to the menu default settings.");
				}
			}
			else
			{
				sc.ScriptError("Unknown keyword '%s'", sc.String);
			}
		}
	}
}


//=============================================================================
//
// Unlocks a custom menu from a script
//
//=============================================================================

void M_UnhideCustomMenu(int menu, int iSet)
{
	FName menuname = FName(ENamedName(NAME_CustomSubMenu1 + menu));
	auto desc = MenuDescriptors.CheckKey(menuname);
	if (desc != NULL && (*desc)->mType == MDESC_ListMenu)
	{
		FListMenuDescriptor* ld = static_cast<FListMenuDescriptor*>(*desc);
		for (unsigned int b = 0; b < MAXMENUGAMEPLAYENTRIES; ++b)
		{
			if (b >= ld->mItems.Size()) return;
			if (iSet & (1u << b))
			{
				ld->mItems[b]->mHidden = false;
				ld->mItems[b]->mEnabled = true;
			}
		}
	}
}

//=============================================================================
//
// Creates the episode menu
//
//=============================================================================

static void BuildEpisodeMenu()
{
	// Build episode menu
	int addedVolumes = 0;
	FMenuDescriptor **desc = MenuDescriptors.CheckKey(NAME_Episodemenu);
	if (desc != NULL && (*desc)->mType == MDESC_ListMenu)
	{
		FListMenuDescriptor *ld = static_cast<FListMenuDescriptor*>(*desc);
		ld->mSelectedItem = gDefaultVolume;
		int y = ld->mYpos;

		for (int i = 0; i < MAXVOLUMES; i++)
		{
			if (gVolumeNames[i].IsNotEmpty() && !(gVolumeFlags[i] & EF_HIDEFROMSP))

			{
				auto it = new FListMenuItemNativeText(ld->mXpos, y, ld->mLinespacing, gVolumeNames[i][0], gVolumeNames[i], NIT_BigFont, NIT_ActiveState, 1, NAME_Skillmenu, i);
				if ((g_gameType & GAMEFLAG_DUKE) && (g_gameType & GAMEFLAG_SHAREWARE) && i > 0)
				{
					it->mEnabled = false;
				}
				y += ld->mLinespacing;
				ld->mItems.Push(it);
				addedVolumes++;
				if (gVolumeSubtitles[i].IsNotEmpty())
				{
					auto it = new FListMenuItemNativeStaticText(ld->mXpos, y, gVolumeSubtitles[i], NIT_SmallFont, NIT_ActiveState, false);
					y += ld->mLinespacing * 6 / 10;
					ld->mItems.Push(it);
				}
			}
		}
#if 0	// this needs to be backed by a working selection menu, until that gets done it must be disabled.
		if (!(g_gameType & GAMEFLAG_SHAREWARE))
		{
			//auto it = new FListMenuItemNativeStaticText(ld->mXpos, "", NIT_SmallFont);	// empty entry as spacer.
			//ld->mItems.Push(it);

			y += ld->mLinespacing / 3;
			auto it = new FListMenuItemNativeText(ld->mXpos, y, ld->mLineSpacing, 0, "$MNU_USERMAP", NIT_BigFont, NIT_ActiveState, 1, NAME_UsermapMenu);
			ld->mItems.Push(it);
			addedVolumes++;
		}
#endif
		if (addedVolumes == 1)
		{
			ld->mAutoselect = 0;
		}
	}

	// Build skill menu
	int addedSkills = 0;
	desc = MenuDescriptors.CheckKey(NAME_Skillmenu);
	if (desc != NULL && (*desc)->mType == MDESC_ListMenu)
	{
		FListMenuDescriptor* ld = static_cast<FListMenuDescriptor*>(*desc);
		ld->mSelectedItem = gDefaultSkill;
		int y = ld->mYpos;

		for (int i = 0; i < MAXSKILLS; i++)
		{
			if (gSkillNames[i].IsNotEmpty())
			{
				auto it = new FListMenuItemNativeText(ld->mXpos, y, ld->mLinespacing, gSkillNames[i][0], gSkillNames[i], NIT_BigFont, NIT_ActiveState, 1, NAME_Startgame, i);
				y += ld->mLinespacing;
				ld->mItems.Push(it);
				addedSkills++;
			}
		}
		if (addedSkills == 0)
		{
			// Need to add one item with the default skill so that the menu does not break.
			auto it = new FListMenuItemNativeText(ld->mXpos, 0, ld->mLinespacing, 0, "", NIT_BigFont, NIT_ActiveState, 1, NAME_Startgame, gDefaultSkill);
			ld->mItems.Push(it);
		}
		if (addedSkills == 1)
		{
			ld->mAutoselect = 0;
		}
	}

	if (g_MenuGameplayEntries[0].entry.isValid())
	{
		int e = 0;
		FMenuDescriptor** desc = MenuDescriptors.CheckKey("CustomGameMenu");
		if (desc != NULL && (*desc)->mType == MDESC_ListMenu)
		{
			FListMenuDescriptor* ldo = static_cast<FListMenuDescriptor*>(*desc);

			for (MenuGameplayStemEntry const& stem : g_MenuGameplayEntries)
			{
				MenuGameplayEntry const& entry = stem.entry;
				if (!entry.isValid())
					break;

				int s = 0;
				FMenuDescriptor** sdesc = MenuDescriptors.CheckKey(FName(FStringf("CustomSubMenu%d", e+1)));
				if (sdesc != NULL && (*sdesc)->mType == MDESC_ListMenu)
				{
					FListMenuDescriptor* ld = static_cast<FListMenuDescriptor*>(*sdesc);
					ld->mCaption = entry.name;

					for (MenuGameplayEntry const& subentry : stem.subentries)
					{
						if (!subentry.isValid())
							break;

						auto li = new FListMenuItemNativeText(ld->mXpos, 0, ld->mLinespacing, 0, subentry.name, NIT_BigFont, NIT_ActiveColor, 1.f, subentry.flags & MGE_UserContent ? NAME_UsermapMenu : NAME_Skillmenu);

						if (subentry.flags & MGE_Locked) li->mEnabled = false;
						if (subentry.flags & MGE_Hidden) li->mHidden = true;
						ld->mItems.Push(li);
						++s;
					}
				}
				FName link = entry.flags & MGE_UserContent ? NAME_UsermapMenu : s == 0 ? NAME_Skillmenu : NAME_CustomSubMenu1;

				auto li = new FListMenuItemNativeText(ldo->mXpos, 0, ldo->mLinespacing, 0, entry.name, NIT_BigFont, NIT_ActiveColor, 1.f, link, e);
				if (entry.flags & MGE_Locked) li->mEnabled = false;
				if (entry.flags & MGE_Hidden) li->mHidden = true;
				ldo->mItems.Push(li);
				e++;
			}
		}
		if (e > 0)
		{
			for (auto name : { NAME_Mainmenu, NAME_IngameMenu })
			{
				FMenuDescriptor** desc = MenuDescriptors.CheckKey(name);
				if (desc != NULL && (*desc)->mType == MDESC_ListMenu)
				{
					FListMenuDescriptor* ld = static_cast<FListMenuDescriptor*>(*desc);
					auto li = ld->mItems[0];
					if (li->GetAction(nullptr) == NAME_Episodemenu)
					{
						li->SetAction(NAME_CustomGameMenu);
					}
				}
			}
		}
	}
}

//=============================================================================
//
// Reads any XHAIRS lumps for the names of crosshairs and
// adds them to the display options menu.
//
//=============================================================================

static void InitCrosshairsList()
{
#if 0
	int lastlump, lump;

	lastlump = 0;

	FOptionValues **opt = OptionValues.CheckKey(NAME_Crosshairs);
	if (opt == NULL) 
	{
		return;	// no crosshair value list present. No need to go on.
	}

	FOptionValues::Pair *pair = &(*opt)->mValues[(*opt)->mValues.Reserve(1)];
	pair->Value = 0;
	pair->Text = "None";

	while ((lump = Wads.FindLump("XHAIRS", &lastlump)) != -1)
	{
		FScanner sc(lump);
		while (sc.GetNumber())
		{
			FOptionValues::Pair value;
			value.Value = sc.Number;
			sc.MustGetString();
			value.Text = sc.String;
			if (value.Value != 0)
			{ // Check if it already exists. If not, add it.
				unsigned int i;

				for (i = 1; i < (*opt)->mValues.Size(); ++i)
				{
					if ((*opt)->mValues[i].Value == value.Value)
					{
						break;
					}
				}
				if (i < (*opt)->mValues.Size())
				{
					(*opt)->mValues[i].Text = value.Text;
				}
				else
				{
					(*opt)->mValues.Push(value);
				}
			}
		}
	}
#endif
}

//=============================================================================
//
// Initialize the music configuration submenus
//
//=============================================================================
extern "C"
{
	extern int adl_getBanksCount();
	extern const char* const* adl_getBankNames();
}

static void InitMusicMenus()
{
	FMenuDescriptor** advmenu = MenuDescriptors.CheckKey(NAME_AdvSoundOptions);
	auto soundfonts = sfmanager.GetList();
	std::tuple<const char*, int, const char*> sfmenus[] = { 
																std::make_tuple("FluidPatchsetMenu", SF_SF2, "fluid_patchset") };

	for (auto& p : sfmenus)
	{
		FMenuDescriptor** menu = MenuDescriptors.CheckKey(std::get<0>(p));

		if (menu != nullptr)
		{
			if (soundfonts.Size() > 0)
			{
				for (auto& entry : soundfonts)
				{
					if (entry.type & std::get<1>(p))
					{
						FString display = entry.mName;
						display.ReplaceChars("_", ' ');
						auto it = new FOptionMenuItemCommand (display, FStringf("%s \"%s\"", std::get<2>(p), entry.mName.GetChars())/*, true*/);
						static_cast<FOptionMenuDescriptor*>(*menu)->mItems.Push(it);
					}
				}
			}
			else if (advmenu != nullptr)
			{
				// Remove the item for this submenu
				auto d = static_cast<FOptionMenuDescriptor*>(*advmenu);
				auto it = d->GetItem(std::get<0>(p));
				if (it != nullptr) d->mItems.Delete(d->mItems.Find(it));
			}
		}
	}
}

//=============================================================================
//
// Special menus will be created once all engine data is loaded
//
//=============================================================================

void M_CreateMenus()
{
	BuildEpisodeMenu();
	InitCrosshairsList();
	InitMusicMenus();

	FOptionValues** opt = OptionValues.CheckKey(NAME_Mididevices);
	if (opt != nullptr)
	{
		I_BuildMIDIMenuList(*opt);
	}
#ifndef NO_OPENAL
	opt = OptionValues.CheckKey(NAME_Aldevices);
	if (opt != nullptr)
	{
		I_BuildALDeviceList(*opt);
	}
	opt = OptionValues.CheckKey(NAME_Alresamplers);
	if (opt != nullptr)
	{
		I_BuildALResamplersList(*opt);
	}
#endif // !NO_OPENAL
}

//=============================================================================
//
// Returns the default skill level.
//
//=============================================================================

int M_GetDefaultSkill()
{
	return gDefaultSkill;
}

void FListMenuDescriptor::Reset()
{
	// Reset the default settings (ignore all other values in the struct)
	mSelectOfsX = 0;
	mSelectOfsY = 0;
	mSelector = nullptr;
	mDisplayTop = 0;
	mXpos = 0;
	mYpos = 0;
	mLinespacing = 0;
	mNetgameMessage = "";
	mFont = NULL;
	mFontColor = CR_UNTRANSLATED;
	mFontColor2 = CR_UNTRANSLATED;
	mScriptId = -1;
	mSecondaryId = 0;
	mNativeFontNum = NIT_BigFont;
	mNativePalNum = NIT_ActiveColor;
	mNativeFontScale = 1.f;
	mFlags = 0;
	mSpacing = 0;
}