#include "rpl4.h"
#pragma hdrstop

#include "rpl4environ.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//########################################################################
// environ.ini - see rpl4environ.h for why the exe owns this rather than
// the packaging script.
//########################################################################

namespace
{
	const char kEnvironFileName[] = "environ.ini";

	//-------------------------------------------------------------------
	// The shipped configuration, verbatim. Lifted out of pack-dist.ps1
	// so there is one source of truth and the exe alone can produce a
	// working install.
	//-------------------------------------------------------------------
	const char kEnvironTemplate[] =
"# ============================================================================\n"
"#  environ.ini - Red Planet 4.12 configuration\n"
"# ============================================================================\n"
"#  One KEY=VALUE per line, read at game start. Lines starting with # or ;\n"
"#  are comments; anything without an = is ignored. Delete a line (or\n"
"#  comment it out) to fall back to the built-in default.\n"
"#\n"
"#  Input bindings live in bindings.txt beside the exe (written with the\n"
"#  full documented layout on first run; delete it to restore defaults).\n"
"#\n"
"#  Your callsign and loadout are remembered in pilot.cfg beside the exe.\n"
"#  Set them on the setup screen once and they come back every session,\n"
"#  however you left - launching, joining a lobby, or quitting. Delete\n"
"#  that file to start over.\n"
"\n"
"# ---- Core (the shipped configuration) --------------------------------------\n"
"\n"
"# Control stack: tokens separated by ; or , processed left to right.\n"
"#   PAD            the virtual RIO (XInput controller + keyboard,\n"
"#                  rebindable via bindings.txt)\n"
"#   RIO            real serial cockpit hardware on COM1\n"
"#   RIO:COMn       same, on another port (RIO:COM3, ...)\n"
"#   KEYBOARD       the engine keyboard handler\n"
"#   MOUSE, JOYSTICK, FLIGHTSTICKPRO, THRUSTMASTER, DIJOYSTICK\n"
"#                  legacy pointer/joystick drivers (untested here)\n"
"# Unset falls back to KEYBOARD alone.\n"
"L4CONTROLS=PAD;KEYBOARD\n"
"\n"
"# Renderer bring-up argument. Only its presence is checked (the DPL\n"
"# resolution parsing it once fed is gone) and the game refuses to start\n"
"# without it - any non-empty value works. Leave as shipped.\n"
"DPLARG=1\n"
"\n"
"# DPL (renderer/scene) configuration file, searched beside the exe.\n"
"# Any notation file name; RPDPL.INI is the one that ships.\n"
"L4DPLCFG=RPDPL.INI\n"
"\n"
"# Gauge (MFD/instrument) canvas. Must name a page of GAUGE\\L4GAUGE.INI:\n"
"#   640x480x8 | 640x480x16 | 800x600x16\n"
"# Unset disables the gauge renderer (and with it all MFDs).\n"
"L4GAUGE=640x480x16\n"
"\n"
"# Plasma display.\n"
"#   SCREEN         render the pod's plasma glass in-window (currently\n"
"#                  parked off-layout)\n"
"#   COM1, COM2...  drive real plasma glass on that serial port\n"
"#                  (9600 baud, N81)\n"
"# Unset = no plasma display.\n"
"L4PLASMA=SCREEN\n"
"\n"
"# 0 = classic separate gauge windows; 1 = the single-window glass\n"
"# cockpit (all seven displays composed on a locked 1920x1080 canvas\n"
"# around the viewscreen); 2 = exploded diagnostic view (each display\n"
"# in its own native-resolution desktop window - MFDs 640x480, map\n"
"# 480x640 - decoded exactly as the pod's VDB split them, no downscale).\n"
"L4MFDSPLIT=1\n"
"\n"
"# The game window - and in the exploded view (L4MFDSPLIT=2) each display\n"
"# window - is placed fresh every launch, so moving one somewhere useful\n"
"# never survived the menu-race-menu loop. This remembers where you put\n"
"# them, in mfd_layout.cfg beside this file:\n"
"#   off / 0 / unset   computed placement only, no file (default)\n"
"#   load              put the windows back where they were saved\n"
"#   save              the same, and re-save on every finished drag\n"
"# The game window gets its size back too, so you can size the cockpit to\n"
"# suit your monitor once and keep it. The display windows get position\n"
"# only: their size follows their content and their button banks, so an\n"
"# old one is never restored over them. Arrange everything once with\n"
"# save, then leave it on load.\n"
"#\n"
"# The plasma display window takes part too, under \"Plasma Display\".\n"
"#\n"
"# Each line in mfd_layout.cfg reads <title>=<x>,<y>,<w>,<h>, and you can\n"
"# append ,noframe to take that window's title bar and border off - a\n"
"# cockpit that fills the monitor edge to edge without -fit taking the\n"
"# whole screen. Put the window where you want it first: a bare window\n"
"# has nothing to drag by. Delete the flag to get the frame back.\n"
"#RP412MFDLAYOUT=off\n"
"\n"
"# Size of the six secondary displays in the glass cockpit, as a\n"
"# percentage of their pod size. The pod bolted them down at one size;\n"
"# on a big panel there is room to trade viewscreen for instrument, so\n"
"# turn these up if you want to actually read the other displays while\n"
"# you fly. 100 = as the pod had them. Range 25-200 (out-of-range and\n"
"# unreadable values fall back to the group setting, then to 100).\n"
"#\n"
"# The scaling is applied in canvas units, before the cockpit is fitted\n"
"# to your window, so a given number looks the same on every monitor.\n"
"# The layout stays legal whatever you ask for - the panes are clamped\n"
"# against their actual neighbours, shrinking uniformly so a display\n"
"# never comes out stretched. They do overlap the viewscreen, exactly\n"
"# as the pod's bezels did, but never each other.\n"
"#\n"
"# L4MFDSCALE sets all five green MFDs at once.\n"
"L4MFDSCALE=100\n"
"\n"
"# ...and any single display can override it. Uncomment one to size it\n"
"# on its own - useful if you only care about, say, the damage readout.\n"
"#   UL upper left     UC upper center    UR upper right\n"
"#   LL lower left     LR lower right\n"
"#L4MFDSCALE_UL=100\n"
"#L4MFDSCALE_UC=100\n"
"#L4MFDSCALE_UR=100\n"
"#L4MFDSCALE_LL=100\n"
"#L4MFDSCALE_LR=100\n"
"\n"
"# The portrait radar/map, sized on its own (it already sits at 1.35x\n"
"# the MFDs by default). It shares the canvas with whichever MFD is\n"
"# above it, so at extreme settings one of the two gives way.\n"
"L4RADARSCALE=100\n"
"\n"
"# Where the radar sits:\n"
"#   CENTER    bottom centre, under the viewscreen, as the pod had it\n"
"#             (default; BOTTOM and CENTRE mean the same)\n"
"#   LEFT      bottom left corner      (or BOTTOMLEFT)\n"
"#   RIGHT     bottom right corner     (or BOTTOMRIGHT)\n"
"#   MIDLEFT   left edge, halfway up   (or LEFTCENTER / LEFTCENTRE)\n"
"#   MIDRIGHT  right edge, halfway up  (or RIGHTCENTER / RIGHTCENTRE)\n"
"# Anywhere but CENTER stops it blocking the middle of the road, which\n"
"# is worth having on a wide screen.\n"
"#\n"
"# In a bottom corner it is one of three panes along the bottom, and the\n"
"# lower MFD whose corner it takes slides inboard beside it. Halfway up\n"
"# a side it leaves the bottom row entirely and sits between that side's\n"
"# two MFDs - roomy on a tall radar, but if the MFDs on that side are\n"
"# also scaled up, the radar is the one that gives way (it has to clear\n"
"# both of them, and it grows from the middle in both directions).\n"
"L4RADARPOS=CENTER\n"
"\n"
"# The Winners Circle: at the end of a race the finishers are stood on\n"
"# the award platform in finishing order, with each pilot's callsign on\n"
"# the plate beside their spot, and held there for a few seconds before\n"
"# the results screen. 1 = show it, 0 = straight to the results.\n"
"RP412PODIUM=1\n"
"\n"
"# The shot is framed for you, but these move the camera if you want it\n"
"# somewhere else. Distances are in game units, measured from the middle\n"
"# of the group of finishers.\n"
"#   STANDOFF  how far out in front of the stand the camera sits\n"
"#   HEIGHT    how far above the group\n"
"#   AIM       height of the point it looks at, relative to the group -\n"
"#             negative tilts down, positive tilts up\n"
"#   ASPECT    the stand was composed for a 4:3 pod monitor, so the shot\n"
"#             is cropped to that shape with black either side. 0 runs it\n"
"#             full width instead.\n"
"#   FADEIN    seconds to come up out of the black after the race fades\n"
"#   CAM       0 watches from your own cockpit rather than off the stand\n"
"#RP412PODIUMSTANDOFF=36\n"
"#RP412PODIUMHEIGHT=12\n"
"#RP412PODIUMAIM=2\n"
"#RP412PODIUMASPECT=1.333\n"
"#RP412PODIUMFADEIN=0.45\n"
"#RP412PODIUMCAM=1\n"
"\n"
"# Override the game length the menu picked, in seconds. The shortest the\n"
"# menu offers is 3:00, which is a long wait when what you are testing is\n"
"# what happens at the buzzer. Unset = use the menu's choice.\n"
"#RP412MISSIONSECONDS=20\n"
"\n"
"# Simulation/render frame rate, integer frames/second. The desktop\n"
"# default is 60; the arcade pods shipped at 25.\n"
"TARGETFPS=60\n"
"\n"
"# 1 = Steam networking (lobbies, FakeIP mesh). Needs the Steam client\n"
"# running and steam_appid.txt beside the exe; without them the game\n"
"# logs the reason and falls back to plain TCP. 0 = TCP only.\n"
"RP412STEAM=1\n"
"\n"
"# Line up each remote player's clock with ours, so their vehicle is\n"
"# extrapolated from when its update was SENT rather than when it\n"
"# arrived. Without it every remote pod sits one network latency behind\n"
"# where it should be - invisible on the 1ms arcade LAN the engine was\n"
"# written for, a constant 50-150ms of lag over the internet. 0 restores\n"
"# the old arrival-time behaviour if you want to compare.\n"
"#RP412NETCLOCK=0\n"
"\n"
"# ---- Optional ---------------------------------------------------------------\n"
"\n"
"# RGB keyboard lamp mirror (Windows Dynamic Lighting): keys bound to\n"
"# lamp buttons glow with the panel, flash modes and all.\n"
"# Unset or nonzero = on (the default); 0 = off.\n"
"#RP412KEYLIGHT=0\n"
"\n"
"# Invert the stick on top of whatever bindings.txt produces:\n"
"# X = invert X only, Y = invert Y only, XY = both (case-insensitive).\n"
"#L4PADFLIP=XY\n"
"\n"
"# Anti-aliasing sample count, passed straight to Direct3D 9:\n"
"# 0 = off, else 2..16 as the GPU supports (1 selects the driver's\n"
"# \"nonmaskable\" mode; unsupported counts fail device creation).\n"
"#MULTISAMPLE=0\n"
"\n"
"# Particle budget, integer. Default 8192.\n"
"#MAXPARTICLES=8192\n"
"\n"
"# On-screen plasma glass (L4PLASMA=SCREEN only). SCALE = integer pixel\n"
"# size 1..16, default 4 (out-of-range values are ignored). POS = window\n"
"# top-left as X,Y screen coordinates; unset = auto, parked below the\n"
"# main window.\n"
"#L4PLASMASCALE=4\n"
"#L4PLASMAPOS=0,0\n"
"\n"
"# Fixed random seed (repeatable runs): any unsigned integer.\n"
"# Unset seeds from the clock.\n"
"#RANDOM=12345\n"
"\n"
"# ---- LAN play without Steam -------------------------------------------------\n"
"# Host a race over plain TCP: list the member pods' console channels\n"
"# (members run: rpl4opt.exe -windowed -res 1920 1080 -net 1501).\n"
"#   RP412HOSTPODS  comma-separated IP[:port] list, one entry per member\n"
"#                  pod; port defaults to 1501 per entry\n"
"#   RP412HOSTPORT  this machine's console port, integer > 0\n"
"#                  (default 1501)\n"
"#   RP412HOSTADDR  this machine's LAN IP as members can reach it\n"
"#                  (default 127.0.0.1)\n"
"#RP412HOSTPODS=192.168.1.20:1501,192.168.1.21:1501\n"
"#RP412HOSTPORT=1501\n"
"#RP412HOSTADDR=192.168.1.10\n"
"\n"
"# ---- Developer / testing ----------------------------------------------------\n"
"\n"
"# Nonzero arms the debug keys: Alt+W wireframe, Alt+V predator vision,\n"
"# Alt+F frame dump, Alt+/ perf stats, Alt+E event-queue dump.\n"
"# 0 or unset = off. (Alt+Q, the mission abort, is always live.)\n"
"#RP412DEVKEYS=1\n"
"\n"
"# Console race-length override, integer seconds (short test races).\n"
"# Values <= 0 are ignored.\n"
"#L4CONSOLELEN=30\n"
"\n"
"# Nonzero = Steam transport loopback self-test at boot (logs PASS/FAIL).\n"
"#RP412STEAMSELFTEST=1\n"
"\n"
"# ---- Arcade heritage (multi-monitor pods; not used on the desktop) ----------\n"
"# PRIMGAUGE / SECGAUGE / MFDGAUGE / MFDGAUGE2 pin a display to a monitor\n"
"# by adapter index (0, 1, 2...). SPANDISABLE: 0 = let the MFDs span one\n"
"# wide surface, nonzero = separate windows (setting MFDGAUGE2 alone also\n"
"# forces spanning off). L4EYES = \"x y z xrot yrot zrot [type]\" floats\n"
"# for a detached camera; a type starting with r offsets it relative to\n"
"# the pod. L4INTERCOM enables the crew intercom - only its presence\n"
"# matters (traditionally COM2). NOMODES skips the mode/lamp programming;\n"
"# presence alone triggers it, even NOMODES=0. LOGSIZE > 0 sizes the\n"
"# trace log in dev builds compiled with tracing.\n"
"#PRIMGAUGE=1\n"
"#SECGAUGE=2\n"
"#MFDGAUGE=3\n"
"#MFDGAUGE2=4\n"
"#SPANDISABLE=1\n"
"#L4EYES=1\n"
"#L4INTERCOM=COM2\n"
"#NOMODES=1\n"
"#LOGSIZE=1000000\n"
		;

	//-------------------------------------------------------------------
	// Does the player's file mention this key at all - set, or commented
	// out, or with whitespace in front of it?
	//
	// Deliberately generous: a key that is mentioned in ANY form is left
	// alone. The alternative failure is worse than a missed notice, since
	// environ.ini is applied line by line and a second copy of a key
	// further down the file would silently override the player's own.
	//-------------------------------------------------------------------
	Logical FileMentionsKey(const char *text, const char *key, int key_length)
	{
		const char *cursor = text;
		while ((cursor = strstr(cursor, key)) != NULL)
		{
			//
			// Must be a whole key: preceded by start-of-line, whitespace
			// or a comment mark, and followed by '='.
			//
			const char *after = cursor + key_length;
			Logical starts_token =
				(cursor == text) ||
				(cursor[-1] == '\n') || (cursor[-1] == '\r') ||
				(cursor[-1] == ' ')  || (cursor[-1] == '\t') ||
				(cursor[-1] == '#')  || (cursor[-1] == ';');
			if (starts_token)
			{
				const char *scan = after;
				while (*scan == ' ' || *scan == '\t')
				{
					++scan;
				}
				if (*scan == '=')
				{
					return True;
				}
			}
			cursor = after;
		}
		return False;
	}

	//-------------------------------------------------------------------
	// Name every template key the player's file has never heard of. Not
	// a fix - their file stays theirs - but it puts the reason for a
	// missing feature in the log we already ask testers for.
	//-------------------------------------------------------------------
	void ReportUnmentionedKeys(const char *file_text)
	{
		char missing[1024];		// what gets printed
		char seen[1024];		// the same keys as "KEY=", so the mention
								// test above can dedupe against them
		missing[0] = '\0';
		seen[0] = '\0';
		int count = 0;			// how many are missing
		int listed = 0;			// how many fitted in the line

		const char *cursor = kEnvironTemplate;
		while (*cursor != '\0')
		{
			const char *line = cursor;
			const char *end = strchr(line, '\n');
			int length = (end != NULL) ? (int)(end - line) : (int) strlen(line);
			cursor = (end != NULL) ? (end + 1) : (line + length);

			//
			// A template key line is "KEY=..." or "#KEY=..." - the
			// commented ones are options that ship switched off, and a
			// player who has never seen them wants to know they exist.
			//
			const char *scan = line;
			int remaining = length;
			if (remaining > 0 && *scan == '#')
			{
				++scan;
				--remaining;
			}
			if (remaining <= 0 || !(isalpha((unsigned char) *scan) || *scan == '_'))
			{
				continue;
			}
			int key_length = 0;
			while (key_length < remaining &&
				(isalnum((unsigned char) scan[key_length]) || scan[key_length] == '_'))
			{
				++key_length;
			}
			if (key_length >= remaining || scan[key_length] != '=' || key_length > 60)
			{
				continue;
			}

			char key[64];
			memcpy(key, scan, key_length);
			key[key_length] = '\0';

			if (FileMentionsKey(file_text, key, key_length))
			{
				continue;
			}
			//
			// Templates list some keys twice (documented once, shown
			// again in an example); do not name one twice.
			//
			if (FileMentionsKey(seen, key, key_length))
			{
				continue;
			}
			++count;
			if (strlen(seen) + key_length + 3 < sizeof(seen))
			{
				strcat(seen, key);
				strcat(seen, "=\n");
			}
			if (strlen(missing) + key_length + 3 < sizeof(missing))
			{
				if (missing[0] != '\0')
				{
					strcat(missing, ", ");
				}
				strcat(missing, key);
				++listed;
			}
		}

		if (count > 0)
		{
			//
			// Say when the list is short of the count rather than letting
			// a full buffer quietly shorten the answer.
			//
			DEBUG_STREAM << "Environ: " << kEnvironFileName << " does not mention "
				<< count << " option(s) this build knows: " << missing;
			if (listed < count)
			{
				DEBUG_STREAM << ", and " << (count - listed) << " more";
			}
			DEBUG_STREAM << "\nEnviron: they are at their built-in defaults - delete "
				<< kEnvironFileName << " to get the documented file back\n"
				<< std::flush;
		}
	}
}

void
	RPL4Environ_Load()
{
	//
	// First run: lay down the documented default. From here on the file
	// belongs to whoever is sitting at this machine.
	//
	FILE *file = fopen(kEnvironFileName, "rb");
	if (file == NULL)
	{
		FILE *out = fopen(kEnvironFileName, "wb");
		if (out != NULL)
		{
			fwrite(kEnvironTemplate, 1, strlen(kEnvironTemplate), out);
			fclose(out);
			DEBUG_STREAM << "Environ: wrote default " << kEnvironFileName
				<< "\n" << std::flush;
		}
		else
		{
			DEBUG_STREAM << "Environ: could not write " << kEnvironFileName
				<< " - running on built-in defaults\n" << std::flush;
		}
		file = fopen(kEnvironFileName, "rb");
	}
	if (file == NULL)
	{
		return;
	}

	fseek(file, 0, SEEK_END);
	long size = ftell(file);
	fseek(file, 0, SEEK_SET);
	if (size <= 0)
	{
		fclose(file);
		return;
	}
	char *text = new char[size + 1];
	size_t read = fread(text, 1, size, file);
	text[read] = '\0';
	fclose(file);

	//
	// One KEY=VALUE per line. Comments, blanks and anything without an
	// '=' are skipped; everything else goes into the environment, which
	// is why a line here beats a variable set in the shell.
	//
	int applied = 0;
	char line[1024];
	const char *cursor = text;
	while (*cursor != '\0')
	{
		int length = 0;
		while (cursor[length] != '\0' && cursor[length] != '\n' &&
			length < (int) sizeof(line) - 1)
		{
			line[length] = cursor[length];
			++length;
		}
		line[length] = '\0';
		cursor += length;
		while (*cursor == '\n' || *cursor == '\r')
		{
			++cursor;
		}
		for (int i = length - 1; i >= 0; --i)
		{
			if (line[i] == '\r' || line[i] == '\n')
			{
				line[i] = '\0';
			}
		}

		char *setting = line;
		while (*setting == ' ' || *setting == '\t')
		{
			++setting;
		}
		if (*setting == '\0' || *setting == '#' || *setting == ';' ||
			strchr(setting, '=') == NULL)
		{
			continue;
		}
		putenv(setting);
		++applied;
	}

	DEBUG_STREAM << "Environ: " << applied << " setting(s) from "
		<< kEnvironFileName << "\n" << std::flush;

	ReportUnmentionedKeys(text);

	delete[] text;
}
