// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// Copyright (C) 2006-2026 by The Odamex Team.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// DESCRIPTION:
//   Functions regarding reading and interpreting UMAPINFO lumps.
//
//-----------------------------------------------------------------------------


#include "odamex.h"

#include "g_episode.h"
#include "oscanner.h"
#include "w_wad.h"
#include "infomap.h"
#include "g_mapinfo.h" // G_MapNameToLevelNum

bool ValidateMapName(const OLumpName& mapname, int* pEpi = NULL, int* pMap = NULL)
{
	// Check if the given map name can be expressed as a gameepisode/gamemap pair and be
	// reconstructed from it.
	OLumpName lumpname;
	int epi = -1, map = -1;

	if (gamemode != commercial)
	{
		if (sscanf(mapname.c_str(), "E%dM%d", &epi, &map) != 2)
			return false;
		lumpname = fmt::format("E{}M{}", epi, map);
	}
	else
	{
		if (sscanf(mapname.c_str(), "MAP%d", &map) != 1)
			return false;
		lumpname = fmt::format("MAP{:02d}", map);
		epi = 1;
	}

	if (pEpi)
		*pEpi = epi;
	if (pMap)
		*pMap = map;
	return mapname == lumpname;
}

// used for munching the strings in UMAPINFO
std::string ParseMultiString(OScanner& os)
{
	os.scan();

	if (!os.isQuotedString())
	{
		if (os.compareTokenNoCase("clear"))
			return "-"; // this was explicitly deleted to override the default.

		os.error("Either 'clear' or quoted string expected");
	}
	os.unScan();

	std::string build;

	do
	{
		os.mustScan();

		build += os.getToken(); // Concatenate the new line onto the existing text
		build += '\n';          // Add newline

		os.scan();
	} while (os.compareToken(","));
	os.unScan();

	return build;
}

void ParseOLumpName(OScanner& os, OLumpName& buffer)
{
	os.mustScan(8);
	buffer = os.getToken();
}

void MustGetIdentifier(OScanner& os)
{
	os.mustScan();
	if (!os.isIdentifier())
	{
		os.error("Expected identifier (unexpected end of file).");
	}
}

enum struct player_action_t
{
	DISALLOW,
	ALLOW,
	REQUIRE,
};

player_action_t ParsePlayerAction(OScanner& os)
{
	MustGetIdentifier(os);
	if (os.compareTokenNoCase("disallow"))
		return player_action_t::DISALLOW;

	if (os.compareTokenNoCase("allow"))
		return player_action_t::ALLOW;

	if (os.compareTokenNoCase("require"))
		return player_action_t::REQUIRE;

	os.error("Expected 'disable', 'allow' or 'require', got '{}'.", os.getToken());
}

bool pnamemodified;

bool ParseStandardUmapInfoProperty(OScanner& os, level_pwad_info_t* mape)
{
	// find the next line with content.
	// this line is no property.

	if (!os.isIdentifier())
	{
		os.error("Expected identifier, got \"{}\".", os.getToken());
	}
	std::string pname = os.getToken();
	os.mustScan();
	os.assertTokenNoCaseIs("=");

	if (!stricmp(pname.c_str(), "levelname"))
	{
		os.mustScan();
		mape->level_name = os.getToken();
		if (!pnamemodified) // only want to clear pname if its *not* from the umapinfo
			mape->pname.clear();
	}
	else if (!stricmp(pname.c_str(), "label"))
	{
		os.mustScan();
		if (!os.isQuotedString() && os.compareTokenNoCase("clear"))
		{
			mape->clearlabel = true;
		}
		else
		{
			mape->label = os.getToken();
		}
	}
	else if (!stricmp(pname.c_str(), "author"))
	{
		os.mustScan();
		mape->author = os.getToken();
	}
	else if (!stricmp(pname.c_str(), "next"))
	{
		ParseOLumpName(os, mape->nextmap);
		if (!ValidateMapName(mape->nextmap))
		{
			os.error("Invalid map name {}", mape->nextmap);
			return false;
		}
	}
	else if (!stricmp(pname.c_str(), "nextsecret"))
	{
		ParseOLumpName(os, mape->secretmap);
		if (!ValidateMapName(mape->secretmap))
		{
			os.error("Invalid map name {}", mape->nextmap);
			return false;
		}
	}
	else if (!stricmp(pname.c_str(), "levelpic"))
	{
		ParseOLumpName(os, mape->pname);
		pnamemodified = true;
	}
	else if (!stricmp(pname.c_str(), "skytexture"))
	{
		ParseOLumpName(os, mape->skypic);
	}
	else if (!stricmp(pname.c_str(), "music"))
	{
		os.mustScan(8);
		const std::string musicname = os.getToken();
		if (W_CheckNumForName(musicname.c_str()) != -1)
		{
			mape->music = musicname;
		}
	}
	else if (!stricmp(pname.c_str(), "endpic"))
	{
		ParseOLumpName(os, mape->endpic);
		mape->nextmap = "EndGame1";
	}
	else if (!stricmp(pname.c_str(), "endcast"))
	{
		os.mustScanBool();
		if (os.getTokenBool())
			mape->nextmap = "EndGameC";
		else
			mape->endpic.clear();
	}
	else if (!stricmp(pname.c_str(), "endbunny"))
	{
		os.mustScanBool();
		if (os.getTokenBool())
			mape->nextmap = "EndGame3";
		else
			mape->endpic.clear();
	}
	else if (!stricmp(pname.c_str(), "endgame"))
	{
		os.mustScanBool();
		if (os.getTokenBool())
		{
			mape->endpic = "!";
		}
		else
		{
			mape->endpic.clear();
		}
	}
	else if (!stricmp(pname.c_str(), "exitpic"))
	{
		ParseOLumpName(os, mape->exitpic);
	}
	else if (!stricmp(pname.c_str(), "enterpic"))
	{
		ParseOLumpName(os, mape->enterpic);
	}
	else if (!stricmp(pname.c_str(), "exitanim"))
	{
		ParseOLumpName(os, mape->exitanim);
	}
	else if (!stricmp(pname.c_str(), "enteranim"))
	{
		ParseOLumpName(os, mape->enteranim);
	}
	else if (!stricmp(pname.c_str(), "nointermission"))
	{
		os.mustScanBool();
		if (os.getTokenBool())
		{
			mape->flags |= LEVEL_NOINTERMISSION;
		}
	}
	else if (!stricmp(pname.c_str(), "partime"))
	{
		os.mustScanInt();
		mape->partime = os.getTokenInt();
	}
	else if (!stricmp(pname.c_str(), "intertext"))
	{
		const std::string lname = ParseMultiString(os);
		if (lname.empty())
			return false;
		mape->intertext = lname;
	}
	else if (!stricmp(pname.c_str(), "intertextsecret"))
	{
		const std::string lname = ParseMultiString(os);
		if (lname.empty())
			return false;
		mape->intertextsecret = lname;
	}
	else if (!stricmp(pname.c_str(), "interbackdrop"))
	{
		ParseOLumpName(os, mape->interbackdrop);
	}
	else if (!stricmp(pname.c_str(), "intermusic"))
	{
		os.mustScan(8);
		const std::string musicname = os.getToken();

		if (W_CheckNumForName(musicname.c_str()) != -1)
			mape->intermusic = musicname;
	}
	else if (!stricmp(pname.c_str(), "episode"))
	{
		if (!episodes_modified && gamemode == commercial)
		{
			episodenum = 0;
			episodes_modified = true;
		}

		const std::string lname = ParseMultiString(os);
		if (lname.empty())
			return false;

		if (lname == "-") // means "clear"
		{
			episodenum = 0;
		}
		else
		{
			const StringTokens tokens = TokenizeString(lname, "\n");

			if (episodenum >= MAX_EPISODES)
			{
				os.error("Maximum episode definitions ({}) exceeded.", MAX_EPISODES);
				return false;
			}

			EpisodeMaps[episodenum] = mape->mapname;
			EpisodeInfos[episodenum].pic_name = tokens[0];
			EpisodeInfos[episodenum].menu_name = tokens[1];
			EpisodeInfos[episodenum].fulltext = false;
			EpisodeInfos[episodenum].noskillmenu = false;
			EpisodeInfos[episodenum].key = (tokens.size() > 2) ? tokens[2][0] : 0;
			++episodenum;
		}
	}
	else if (!stricmp(pname.c_str(), "bossaction"))
	{
		MustGetIdentifier(os);

		if (!stricmp(os.getToken().c_str(), "clear"))
		{
			// mark level free of boss actions
			mape->bossactions.clear();
		}
		else
		{
			const std::string actor_name = os.getToken();
			const mobjtype_t i = P_INameToMobj(actor_name);
			if (i == MT_NULL)
			{
				os.error("Unknown thing type {}", os.getToken());
				return false;
			}

			// skip comma token
			os.mustScan();
			os.assertTokenNoCaseIs(",");
			os.mustScanInt();
			const int special = os.getTokenInt();
			os.mustScan();
			os.assertTokenNoCaseIs(",");
			os.mustScanInt();
			const int tag = os.getTokenInt();
			// allow no 0-tag specials here, unless a level exit.
			if (tag != 0 || special == 11 || special == 51 || special == 52 ||
			    special == 124)
			{
				bossaction_t new_bossaction;

				new_bossaction.special = static_cast<short>(special);
				new_bossaction.tag = static_cast<short>(tag);

				new_bossaction.type = i;

				mape->bossactions.push_back(new_bossaction);
			}
		}
	}
	else if (!stricmp(pname.c_str(), "bossactionednum"))
	{
		os.mustScan();

		if (os.compareTokenNoCase("clear"))
		{
			// mark level free of boss actions
			mape->bossactions.clear();
		}
		else
		{
			const int actor_ednum = os.getTokenInt();
			const auto it = spawn_map.find(actor_ednum);
			int32_t type;
			if (it == spawn_map.end())
			{
				os.error("Unknown thing ednum {}", os.getToken());
				return 0;
			}
			else
			{
				type = it->second->type;
			}

			// skip comma token
			os.mustScan();
			os.assertTokenNoCaseIs(",");
			os.mustScanInt();
			const int special = os.getTokenInt();
			os.mustScan();
			os.assertTokenNoCaseIs(",");
			os.mustScanInt();
			const int tag = os.getTokenInt();
			// allow no 0-tag specials here, unless a level exit.
			if (tag != 0 || special == 11 || special == 51 || special == 52 ||
			    special == 124)
			{
				bossaction_t new_bossaction;

				new_bossaction.special = static_cast<short>(special);
				new_bossaction.tag = static_cast<short>(tag);

				new_bossaction.type = type;

				mape->bossactions.push_back(new_bossaction);
			}
		}
	}
	else if (!stricmp(pname.c_str(), "jumping"))
	{
		switch (ParsePlayerAction(os))
		{
		case player_action_t::DISALLOW:
			mape->flags |= LEVEL_JUMP_NO;
			mape->flags &= ~LEVEL_JUMP_YES;
			break;
		case player_action_t::ALLOW:
			mape->flags &= ~(LEVEL_JUMP_NO | LEVEL_JUMP_YES);
			break;
		case player_action_t::REQUIRE:
			mape->flags &= ~LEVEL_JUMP_NO;
			mape->flags |= LEVEL_JUMP_YES;
			break;
		}
	}
	else if (!stricmp(pname.c_str(), "freeaim"))
	{
		switch (ParsePlayerAction(os))
		{
		case player_action_t::DISALLOW:
			mape->flags |= LEVEL_FREELOOK_NO;
			mape->flags &= ~LEVEL_FREELOOK_YES;
			break;
		case player_action_t::ALLOW:
			mape->flags &= ~(LEVEL_FREELOOK_NO | LEVEL_FREELOOK_YES);
			break;
		case player_action_t::REQUIRE:
			mape->flags &= ~LEVEL_FREELOOK_NO;
			mape->flags |= LEVEL_FREELOOK_YES;
			break;
		}
	}
	else if (!stricmp(pname.c_str(), "crouching"))
	{
		if (ParsePlayerAction(os) == player_action_t::REQUIRE)
			os.warning("Crouching is not supported in Odamex. Map {} may not work as intended.", mape->mapname);
	}
	else
	{
		do
		{
			if (!IsRealNum(os.getToken().c_str()))
				os.scan();

		} while (os.compareToken(","));
	}
	os.scan();

	return 1;
}

void ParseUMapInfoLump(int lump, const OLumpName& lumpname)
{
	LevelInfos& levels = getLevelInfos();

	const char* buffer = static_cast<char*>(W_CacheLumpNum(lump, PU_STATIC));

	const OScannerConfig config = {
	    lumpname, // lumpName
	    false,    // semiComments
	    true,     // cComments
	};
	OScanner os = OScanner::openBuffer(config, buffer, buffer + W_LumpLength(lump));

	while (os.scan())
	{
		if (!os.compareTokenNoCase("map"))
		{
			os.error("Expected map definition, got {}", os.getToken());
		}

		os.mustScan(8);
		const OLumpName mapname = os.getToken();

		if (!ValidateMapName(mapname))
		{
			os.error("Invalid map name {}", mapname);
		}

		// Find the level.
		level_pwad_info_t& info = (levels.findByName(mapname).exists())
			? levels.findByName(mapname)
			: levels.create();

		// if no sky is defined, it will show texture 0 (aastinky/aashitty)
		// so instead, lets just try to give it the first defined sky in the level set.
		if (levels.size() > 0 && info.skypic.empty())
		{
			level_pwad_info_t& def = levels.at(0);
			info.skypic = def.skypic;
		}

		pnamemodified = false;

		info.mapname = mapname;

		G_MapNameToLevelNum(info);
		G_MapNameToID24LevelNum(info);

		os.mustScan();
		os.assertTokenNoCaseIs("{");

		os.scan();
		while (!os.compareToken("}"))
		{
			// TODO: should this be actually checking the return value here?
			ParseStandardUmapInfoProperty(os, &info);
		}

		// Set default level progression here to simplify the checks elsewhere.
		// Doing this lets us skip all normal code for this if nothing has been defined.
		if (!info.nextmap[0] && !info.endpic[0])
		{
			if (info.mapname == "MAP30")
			{
				info.endpic = "$CAST";
				info.nextmap = "EndGameC";
			}
			else if (info.mapname == "E1M8")
			{
				info.endpic = gamemode == retail ? "CREDIT" : "HELP2";
				info.nextmap = "EndGameC";
			}
			else if (info.mapname == "E2M8")
			{
				info.endpic = "VICTORY";
				info.nextmap = "EndGame2";
			}
			else if (info.mapname == "E3M8")
			{
				info.endpic = "$BUNNY";
				info.nextmap = "EndGame3";
			}
			else if (info.mapname == "E4M8")
			{
				info.endpic = "ENDPIC";
				info.nextmap = "EndGame4";
			}
			else if (gamemission == chex && info.mapname == "E1M5")
			{
				info.endpic = "CREDIT";
				info.nextmap = "EndGame1";
			}
			else
			{
				int ep, map;
				ValidateMapName(info.mapname, &ep, &map);
				map++;
				if (gamemode == commercial)
				{
					info.nextmap = fmt::format("MAP{:02d}", map);
				}
				else
				{
					info.nextmap = fmt::format("E{}M{}", ep, map);
				}
			}
		}
	}
	// if an episode title patch is missing or invalid, fall back on text name
	for (auto& episode : EpisodeInfos) {
		episode.fulltext = episode.pic_name.empty() || W_CheckNumForName(episode.pic_name) == -1;
	}
}
