/*
 * This file is part of the DXX-Rebirth project <https://www.dxx-rebirth.com/>.
 * It is copyright by its individual contributors, as recorded in the
 * project's Git history.  See COPYING.txt at the top level for license
 * terms and a link to the Git history.
 */

/*
 *
 * Functions for accessing arguments.
 *
 */

#include <string>
#include <vector>
#include <stdlib.h>
#include <string.h>
#include <SDL_stdinc.h>
#include "physfsx.h"
#include "args.h"
#include "u_mem.h"
#include "strutil.h"
#include "digi.h"
#include "game.h"
#include "console.h"
#include "mission.h"
#if DXX_USE_SDLMIXER
#include "digi_mixer.h"
#endif
#if DXX_USE_UDP
#include "net_udp.h"
#endif

#include "partial_range.h"

namespace dcx {
CArg CGameArg;

namespace {

constexpr std::integral_constant<std::size_t, 1000> MAX_ARGS{};
typedef std::vector<std::string> Arglist;

class ini_entry
{
public:
	const std::string filename;
	ini_entry(std::string &&f) :
		filename(std::move(f))
	{
	}
};

typedef std::vector<ini_entry> Inilist;

class argument_exception
{
public:
	const std::string arg;
	argument_exception(std::string &&a) :
		arg(std::move(a))
	{
	}
};

class missing_parameter : public argument_exception
{
public:
	using argument_exception::argument_exception;
};

class unhandled_argument : public argument_exception
{
public:
	using argument_exception::argument_exception;
};

class conversion_failure : public argument_exception
{
public:
	const std::string value;
	conversion_failure(std::string &&a, std::string &&v) :
		argument_exception(std::move(a)), value(std::move(v))
	{
	}
};

class nesting_depth_exceeded
{
};

[[nodiscard]]
static Arglist BuildArglistFromIni(const char *filename)
{
	Arglist Args;
	if (auto f = PHYSFSX_openReadBuffered(filename).first)
	{
		for (PHYSFSX_gets_line_t<1024> line; Args.size() < MAX_ARGS && PHYSFSX_fgets(line, f);)
		{
			auto &separator = " \t";
			for(char *token = strtok(line, separator); token != NULL; token = strtok(NULL, separator))
			{
				if (*token == ';')
					break;
				Args.push_back(token);
			}
		}
	}
	return Args;
}

static void advance_arg_iterator(Arglist::iterator &pp, Arglist::const_iterator end)
{
	auto arg{pp};
	if (++pp == end)
		throw missing_parameter(std::move(*arg));
}

static std::string &&arg_string(Arglist::iterator &pp, Arglist::const_iterator end)
{
	advance_arg_iterator(pp, std::move(end));
	return std::move(*pp);
}

static long arg_integer(Arglist::iterator &pp, Arglist::const_iterator end)
{
	const auto arg{pp};
	advance_arg_iterator(pp, std::move(end));
	auto &&value{std::move(*pp)};
	char *p;
	const auto i{strtol(value.c_str(), &p, 10)};
	if (*p)
		throw conversion_failure(std::move(*arg), std::move(value));
	return i;
}

template<typename E> E arg_enum(Arglist::iterator &pp, Arglist::const_iterator end)
{
	return static_cast<E>(arg_integer(pp, std::move(end)));
}

#if DXX_USE_UDP
static void arg_port_number(Arglist::iterator &pp, Arglist::const_iterator end, uint16_t &out, bool allow_privileged)
{
	auto port = arg_integer(pp, end);
	if (static_cast<uint16_t>(port) == port && (allow_privileged || port >= 1024))
		out = port;
}
#endif

static void InitGameArg()
{
	CGameArg.SysMaxFPS = MAXIMUM_FPS;
	CGameArg.SysRenderZoomAdjustment = 0;
#if DXX_USE_UDP
	CGameArg.MplUdpHostAddr = UDP_MANUAL_ADDR_DEFAULT;
#if DXX_USE_TRACKER
	CGameArg.MplTrackerAddr = TRACKER_ADDR_DEFAULT;
	CGameArg.MplTrackerPort = TRACKER_PORT_DEFAULT;
#endif
#endif
	CGameArg.DbgVerbose = CON_NORMAL;
	CGameArg.DbgBpp = 32;
#if DXX_USE_OGL
	CGameArg.OglSyncMethod = OGL_SYNC_METHOD_DEFAULT;
	CGameArg.OglSyncWait = OGL_SYNC_WAIT_DEFAULT;
#if DXX_USE_STEREOSCOPIC_RENDER
	CGameArg.OglStereo = false;
#endif
	CGameArg.DbgGlIntensity4Ok 	= true;
	CGameArg.DbgGlLuminance4Alpha4Ok = true;
	CGameArg.DbgGlRGBA2Ok = true;
	CGameArg.DbgGlReadPixelsOk = true;
	CGameArg.DbgGlGetTexLevelParamOk = true;
#endif
}

}

}

namespace dsx {

Arg GameArg;

namespace {

static void InitGameArg()
{
#if DXX_BUILD_DESCENT == 2
	GameArg.SndDigiSampleRate = sound_sample_rate::_22k;
#endif
	::dcx::InitGameArg();
}

static void ReadCmdArgs(Inilist &ini, Arglist &&Args);

static void ReadIniArgs(Inilist &ini)
{
	ReadCmdArgs(ini, BuildArglistFromIni(ini.back().filename.c_str()));
	ini.pop_back();
}

static void ReadCmdArgs(Inilist &ini, Arglist &&Args)
{
	for (Arglist::iterator pp = Args.begin(), end = Args.end(); pp != end; ++pp)
	{
		const char *p = pp->c_str();
	// System Options

		if (!d_stricmp(p, "-help") || !d_stricmp(p, "-h") || !d_stricmp(p, "-?") || !d_stricmp(p, "?"))
			CGameArg.SysShowCmdHelp = true;
		else if (!d_stricmp(p, "-nonicefps"))
			CGameArg.SysNoNiceFPS = true;
		else if (!d_stricmp(p, "-maxfps"))
			CGameArg.SysMaxFPS = arg_integer(pp, end);
		else if (!d_stricmp(p, "-render-zoom"))
			CGameArg.SysRenderZoomAdjustment = arg_integer(pp, end);
		else if (!d_stricmp(p, "-hogdir"))
			CGameArg.SysHogDir = arg_string(pp, end);
#if PHYSFS_VER_MAJOR >= 2
		else if (!d_stricmp(p, "-add-missions-dir"))
			CGameArg.SysMissionDir = arg_string(pp, end);
#endif
		else if (!d_stricmp(p, "-nohogdir"))
		{
			/* No effect if no DXX_SHAREPATH.  Ignore it so that players can
			 * pass it via a cross-platform ini.
			 */
#if DXX_USE_SHAREPATH
			CGameArg.SysNoHogDir = true;
#endif
		}
		else if (!d_stricmp(p, "-use_players_dir"))
			CGameArg.SysUsePlayersDir = static_cast<int8_t>(- (sizeof(PLAYER_DIRECTORY_TEXT) - 1));
		else if (!d_stricmp(p, "-lowmem"))
			CGameArg.SysLowMem = true;
		else if (!d_stricmp(p, "-pilot"))
			CGameArg.SysPilot = arg_string(pp, end);
		else if (!d_stricmp(p, "-record-demo-format"))
			CGameArg.SysRecordDemoNameTemplate = arg_string(pp, end);
		else if (!d_stricmp(p, "-auto-record-demo"))
			CGameArg.SysAutoRecordDemo = true;
		else if (!d_stricmp(p, "-window"))
			CGameArg.SysWindow = true;
		else if (!d_stricmp(p, "-noborders"))
			CGameArg.SysNoBorders = true;
		else if (!d_stricmp(p, "-notitles"))
			CGameArg.SysNoTitles = true;
		else if (!d_stricmp(p, "-nomovies"))
			GameArg.SysNoMovies 		= 1;
		else if (!d_stricmp(p, "-autodemo"))
			CGameArg.SysAutoDemo = true;

	// Control Options

		else if (!d_stricmp(p, "-nocursor"))
			CGameArg.CtlNoCursor 		= true;
		else if (!d_stricmp(p, "-nomouse"))
			CGameArg.CtlNoMouse 		= true;
		else if (!d_stricmp(p, "-nojoystick"))
		{
#if DXX_MAX_JOYSTICKS
			CGameArg.CtlNoJoystick		= 1;
#endif
		}
		else if (!d_stricmp(p, "-nostickykeys"))
			CGameArg.CtlNoStickyKeys	= true;

	// Sound Options

		else if (!d_stricmp(p, "-nosound"))
			CGameArg.SndNoSound		= 1;
		else if (!d_stricmp(p, "-nomusic"))
			CGameArg.SndNoMusic = true;
#if DXX_BUILD_DESCENT == 2
		else if (!d_stricmp(p, "-sound11k"))
			GameArg.SndDigiSampleRate = sound_sample_rate::_11k;
#endif
		else if (!d_stricmp(p, "-nosdlmixer"))
		{
#if DXX_USE_SDLMIXER
			CGameArg.SndDisableSdlMixer = true;
#endif
		}
		else if (!strncmp(p, "-sdlmixer-resampler=", 20))
		{
#if DXX_USE_SDLMIXER
#if DXX_FEATURE_EXTERNAL_RESAMPLER_SDL_NATIVE
			if (!strcmp(&p[20], "sdl-native"))
				CGameArg.SndMixerMethod = digi_mixer_method::sdl_native;
			else
#endif
#if DXX_FEATURE_INTERNAL_RESAMPLER_EMULATE_SDL1
				if (!strcmp(&p[20], "emulate-sdl1"))
					CGameArg.SndMixerMethod = digi_mixer_method::emulate_sdl1;
				else
#endif
#if DXX_FEATURE_INTERNAL_RESAMPLER_EMULATE_SOUNDBLASTER16
					if (!strcmp(&p[20], "emulate-soundblaster16"))
						CGameArg.SndMixerMethod = digi_mixer_method::emulate_soundblaster16;
					else
#endif
				throw unhandled_argument(std::move(*pp));
#endif
		}

	// Graphics Options

		else if (!d_stricmp(p, "-lowresfont"))
			CGameArg.GfxSkipHiresFNT = true;
#if DXX_BUILD_DESCENT == 2
		else if (!d_stricmp(p, "-lowresgraphics"))
			GameArg.GfxSkipHiresGFX	= 1;
		else if (!d_stricmp(p, "-lowresmovies"))
			GameArg.GfxSkipHiresMovie 		= 1;
#endif
#if DXX_USE_OGL
	// OpenGL Options

		else if (!d_stricmp(p, "-gl_fixedfont"))
			CGameArg.OglFixedFont = true;
		else if (!d_stricmp(p, "-gl_syncmethod"))
			CGameArg.OglSyncMethod = arg_enum<SyncGLMethod>(pp, end);
		else if (!d_stricmp(p, "-gl_syncwait"))
			CGameArg.OglSyncWait = arg_integer(pp, end);
		else if (!d_stricmp(p, "-gl_darkedges"))
			CGameArg.OglDarkEdges = true;
#if DXX_USE_STEREOSCOPIC_RENDER
		else if (!d_stricmp(p, "-gl_stereo"))
			CGameArg.OglStereo = true;
		else if (!d_stricmp(p, "-gl_stereoview"))
			CGameArg.OglStereoView = arg_integer(pp, end);
#endif
#endif

	// Multiplayer Options

#if DXX_USE_UDP
		else if (!d_stricmp(p, "-udp_hostaddr"))
			CGameArg.MplUdpHostAddr = arg_string(pp, end);
		else if (!d_stricmp(p, "-udp_hostport"))
			/* Peers use -udp_myport to change, so peer cannot set a
			 * privileged port.
			 */
			arg_port_number(pp, end, CGameArg.MplUdpHostPort, false);
		else if (!d_stricmp(p, "-udp_myport"))
		{
			arg_port_number(pp, end, CGameArg.MplUdpMyPort, false);
		}
		else if (!d_stricmp(p, "-no-tracker"))
		{
			/* Always recognized.  No-op if tracker support compiled
			 * out. */
#if DXX_USE_TRACKER
			CGameArg.MplTrackerAddr.clear();
#endif
		}
#if DXX_USE_TRACKER
		else if (!d_stricmp(p, "-tracker_hostaddr"))
		{
			CGameArg.MplTrackerAddr = arg_string(pp, end);
		}
		else if (!d_stricmp(p, "-tracker_hostport"))
			arg_port_number(pp, end, CGameArg.MplTrackerPort, true);
#endif
#endif

#if DXX_BUILD_DESCENT == 1
		else if (!d_stricmp(p, "-nobm"))
			GameArg.EdiNoBm 		= 1;
#elif DXX_BUILD_DESCENT == 2
#if DXX_USE_EDITOR
	// Editor Options

		else if (!d_stricmp(p, "-autoload"))
			GameArg.EdiAutoLoad = arg_string(pp, end);
		else if (!d_stricmp(p, "-macdata"))
			GameArg.EdiMacData 		= 1;
		else if (!d_stricmp(p, "-hoarddata"))
			GameArg.EdiSaveHoardData 	= 1;
#endif
#endif

	// Debug Options

		else if (!d_stricmp(p, "-debug"))
		{
			if (CGameArg.DbgVerbose < CON_DEBUG)
				CGameArg.DbgVerbose = CON_DEBUG;
		}
		else if (!d_stricmp(p, "-verbose"))
		{
			if (CGameArg.DbgVerbose < CON_VERBOSE)
				CGameArg.DbgVerbose = CON_VERBOSE;
		}

		else if (!d_stricmp(p, "-no-grab"))
			CGameArg.DbgForbidConsoleGrab = true;
		else if (!d_stricmp(p, "-safelog"))
			CGameArg.DbgSafelog = true;
		else if (!d_stricmp(p, "-norun"))
			CGameArg.DbgNoRun = true;
		else if (!d_stricmp(p, "-renderstats"))
			CGameArg.DbgRenderStats = true;
		else if (!d_stricmp(p, "-text"))
			CGameArg.DbgAltTex = arg_string(pp, end);
		else if (!d_stricmp(p, "-showmeminfo"))
			CGameArg.DbgShowMemInfo 		= 1;
		else if (!d_stricmp(p, "-nodoublebuffer"))
			CGameArg.DbgNoDoubleBuffer = true;
		else if (!d_stricmp(p, "-bigpig"))
			CGameArg.DbgNoCompressPigBitmap = true;
		else if (!d_stricmp(p, "-16bpp"))
			CGameArg.DbgBpp		= 16;

#if DXX_USE_OGL
		else if (!d_stricmp(p, "-gl_oldtexmerge"))
			CGameArg.DbgUseOldTextureMerge = true;
		else if (!d_stricmp(p, "-gl_intensity4_ok"))
			CGameArg.DbgGlIntensity4Ok = arg_integer(pp, end);
		else if (!d_stricmp(p, "-gl_luminance4_alpha4_ok"))
			CGameArg.DbgGlLuminance4Alpha4Ok = arg_integer(pp, end);
		else if (!d_stricmp(p, "-gl_rgba2_ok"))
			CGameArg.DbgGlRGBA2Ok = arg_integer(pp, end);
		else if (!d_stricmp(p, "-gl_readpixels_ok"))
			CGameArg.DbgGlReadPixelsOk = arg_integer(pp, end);
		else if (!d_stricmp(p, "-gl_gettexlevelparam_ok"))
			CGameArg.DbgGlGetTexLevelParamOk = arg_integer(pp, end);
#else
		else if (!d_stricmp(p, "-tmap"))
			CGameArg.DbgTexMap = arg_string(pp, end);
		else if (!d_stricmp(p, "-hwsurface"))
			CGameArg.DbgSdlHWSurface = true;
		else if (!d_stricmp(p, "-asyncblit"))
			CGameArg.DbgSdlASyncBlit = true;
#endif
		else if (!d_stricmp(p, "-ini"))
		{
			ini.emplace_back(arg_string(pp, end));
			if (ini.size() > 10)
				throw nesting_depth_exceeded();
			ReadIniArgs(ini);
		}
#if defined(__APPLE__) && defined(__MACH__)
		else if (!strncmp(p, "-psn", 4))
		{
			//do nothing/gobble it up
		}
#endif
		else
			throw unhandled_argument(std::move(*pp));
	}
}

}

}

namespace dcx {

namespace {

static void PostProcessGameArg()
{
	if (CGameArg.SysMaxFPS < MINIMUM_FPS)
		CGameArg.SysMaxFPS = MINIMUM_FPS;
	else if (CGameArg.SysMaxFPS > MAXIMUM_FPS)
		CGameArg.SysMaxFPS = MAXIMUM_FPS;
#if PHYSFS_VER_MAJOR >= 2
	if (!CGameArg.SysMissionDir.empty())
		PHYSFS_mount(CGameArg.SysMissionDir.c_str(), MISSION_DIR, 1);
#endif

#if SDL_MAJOR_VERSION == 1
	static char sdl_disable_lock_keys[] = "SDL_DISABLE_LOCK_KEYS=0";
	if (CGameArg.CtlNoStickyKeys) // Must happen before SDL_Init!
		sdl_disable_lock_keys[sizeof(sdl_disable_lock_keys) - 2] = '1';
	SDL_putenv(sdl_disable_lock_keys);
#endif
}

static std::string ConstructIniStackExplanation(const Inilist &ini)
{
	Inilist::const_reverse_iterator i = ini.rbegin(), e = ini.rend();
	if (i == e)
		return " while processing <command line>";
	std::string result;
	result.reserve(ini.size() * 128);
	result += " while processing \"";
	for (;;)
	{
		result += i->filename;
		if (++ i == e)
			return result += "\"";
		result += "\"\n    included from \"";
	}
}

}

}

namespace dsx {

bool InitArgs(std::span<char *> argv)
{
	InitGameArg();

	Inilist ini;
	try {
		{
			assert(ini.empty());
#if DXX_BUILD_DESCENT == 1
			const auto INI_FILENAME = "d1x.ini";
#elif DXX_BUILD_DESCENT == 2
			const auto INI_FILENAME = "d2x.ini";
#endif
			ini.emplace_back(INI_FILENAME);
			ReadIniArgs(ini);
		}
		ReadCmdArgs(ini, {argv.begin(), argv.end()});
		PostProcessGameArg();
		return true;
	} catch(const missing_parameter& e) {
		UserError("Missing parameter for argument \"%s\"%s", e.arg.c_str(), ConstructIniStackExplanation(ini).c_str());
	} catch(const unhandled_argument& e) {
		UserError("Unhandled argument \"%s\"%s", e.arg.c_str(), ConstructIniStackExplanation(ini).c_str());
	} catch(const conversion_failure& e) {
		UserError("Failed to convert argument \"%s\" parameter \"%s\"%s", e.arg.c_str(), e.value.c_str(), ConstructIniStackExplanation(ini).c_str());
	} catch(const nesting_depth_exceeded &) {
		UserError("Nesting depth exceeded%s", ConstructIniStackExplanation(ini).c_str());
	}
	return false;
}

}
