/*
con_utils.c - console helpers
Copyright (C) 2008 Uncle Mike

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 3 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.
*/

#include "common.h"
#include "client.h"
#include "const.h"

#define CON_MAXCMDS		4096	// auto-complete intermediate list

typedef struct autocomplete_list_s
{
	const char *name;
	int arg; // argument number to handle
	qboolean (*func)( const char *s, char *name, int length, qboolean print_suggestions );
} autocomplete_list_t;

typedef struct
{
	// console auto-complete
	string		shortestMatch;
	field_t		*completionField;	// con.input or dedicated server fake field-line
	const char	*completionString;
	const char	*completionBuffer;
	char		*cmds[CON_MAXCMDS];
	int		matchCount;
} con_autocomplete_t;

static con_autocomplete_t		con;

/*
=======================================================================

			FILENAME AUTOCOMPLETION

=======================================================================
*/
/*
=====================================
Cmd_ListMaps

=====================================
*/
int Cmd_ListMaps( search_t *t, char *lastmapname, size_t len, qboolean silent )
{
	file_t *f;
	int i, nummaps;
	string mapname, message, compiler, generator;

	for( i = 0, nummaps = 0; i < t->numfilenames; i++ )
	{
		char		entfilename[MAX_QPATH];
		const char	*ext = COM_FileExtension( t->filenames[i] );
		int		ver = -1, lumpofs = 0, lumplen = 0;
		char		*ents = NULL, *pfile;
		int		version = 0;
		string	version_description;

		if( Q_stricmp( ext, "bsp" )) continue;
		Q_strncpy( message, "^1error^7", sizeof( message ));
		compiler[0] = '\0';
		generator[0] = '\0';

		f = FS_Open( t->filenames[i], "rb", con_gamemaps.value );

		if( f )
		{
			dheader_t   *header;
			dextrahdr_t *hdrext;
			dlump_t     entities;
			fs_offset_t filelen;
			byte        buf[MAX_SYSPATH] = { 0 }; // 1 kb

			filelen = FS_Read( f, buf, sizeof( buf ));

			// check all the lumps and some other errors
			if( !Mod_TestBmodelLumps( f, t->filenames[i], buf, filelen, silent, &entities ))
			{
				FS_Close( f );
				continue;
			}

			lumpofs = entities.fileofs;
			lumplen = entities.filelen;

			header = (dheader_t *)buf;
			hdrext = (dextrahdr_t *)((byte *)buf + sizeof( dheader_t ));

			ver = header->version;
			if( hdrext->id == IDEXTRAHEADER ) version = hdrext->version;

			Q_strncpy( entfilename, t->filenames[i], sizeof( entfilename ));
			COM_ReplaceExtension( entfilename, ".ent", sizeof( entfilename ));
			ents = (char *)FS_LoadFile( entfilename, NULL, true );

			if( !ents && lumplen >= 10 )
			{
				FS_Seek( f, lumpofs, SEEK_SET );
				ents = (char *)Mem_Calloc( host.mempool, lumplen + 1 );
				FS_Read( f, ents, lumplen );
			}

			if( ents )
			{
				// if there are entities to parse, a missing message key just
				// means there is no title, so clear the message string now
				char	token[2048];

				message[0] = 0; // remove 'error'
				pfile = ents;

				while(( pfile = COM_ParseFile( pfile, token, sizeof( token ))) != NULL )
				{
					if( !Q_strcmp( token, "{" )) continue;
					else if( !Q_strcmp( token, "}" )) break;
					else if( !Q_strcmp( token, "message" ))
					{
						// get the message contents
						pfile = COM_ParseFile( pfile, message, sizeof( message ));
					}
					else if( !Q_strcmp( token, "compiler" ) || !Q_strcmp( token, "_compiler" ))
					{
						// get the message contents
						pfile = COM_ParseFile( pfile, compiler, sizeof( compiler ));
					}
					else if( !Q_strcmp( token, "generator" ) || !Q_strcmp( token, "_generator" ))
					{
						// get the message contents
						pfile = COM_ParseFile( pfile, generator, sizeof( generator ));
					}
				}
				Mem_Free( ents );
			}
		}

		if( f ) FS_Close(f);
		COM_FileBase( t->filenames[i], mapname, sizeof( mapname ));

		switch( ver )
		{
		case Q1BSP_VERSION:
			Q_strncpy( version_description, "Quake", sizeof( version_description ));
			break;
		case QBSP2_VERSION:
			Q_strncpy( version_description, "Darkplaces BSP2", sizeof( version_description ));
			break;
		case HLBSP_VERSION:
			switch( version )
			{
			case 1: Q_strncpy( version_description, "XashXT old format", sizeof( version_description )); break;
			case 2: Q_strncpy( version_description, "Paranoia 2: Savior", sizeof( version_description )); break;
			case 4: Q_strncpy( version_description, "Half-Life extended", sizeof( version_description )); break;
			default: Q_strncpy( version_description, "Half-Life", sizeof( version_description )); break;
			}
			break;
		default:	Q_strncpy( version_description, "??", sizeof( version_description )); break;
		}

		if( !silent )
			Con_Printf( "%16s (%s) ^3%s^7 ^2%s %s^7\n", mapname, version_description, message, compiler, generator );
		nummaps++;
	}

	if( lastmapname && len )
		Q_strncpy( lastmapname, mapname, len );

	return nummaps;
}

/*
=====================================
Cmd_GetMapList

Prints or complete map filename
=====================================
*/
static qboolean Cmd_GetMapList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	search_t *t;
	string   matchbuf;
	int	 i, nummaps;

	t = FS_Search( va( "maps/%s*.bsp", s ), true, con_gamemaps.value );
	if( !t ) return false;

	COM_FileBase( t->filenames[0], matchbuf, sizeof( matchbuf ));
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( t->numfilenames == 1 ) return true;

	nummaps = Cmd_ListMaps( t, matchbuf, sizeof( matchbuf ), !print_suggestions );

	if( print_suggestions )
		Con_Printf( "\n^3 %d maps found.\n", nummaps );

	Mem_Free( t );

	// cut shortestMatch to the amount common with s
	for( i = 0; matchbuf[i]; i++ )
	{
		if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
			completedname[i] = 0;
	}
	return true;
}

/*
=====================================
Cmd_GetDemoList

Prints or complete demo filename
=====================================
*/
static qboolean Cmd_GetDemoList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	search_t		*t;
	string		matchbuf;
	int		i, numdems;

	// lookup only in gamedir
	t = FS_Search( va( "%s*.dem", s ), true, true );
	if( !t ) return false;

	COM_FileBase( t->filenames[0], matchbuf, sizeof( matchbuf ));
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( t->numfilenames == 1 ) return true;

	for( i = 0, numdems = 0; i < t->numfilenames; i++ )
	{
		if( Q_stricmp( COM_FileExtension( t->filenames[i] ), "dem" ))
			continue;

		COM_FileBase( t->filenames[i], matchbuf, sizeof( matchbuf ));

		if( print_suggestions )
			Con_Printf( "%16s\n", matchbuf );
		numdems++;
	}

	if( print_suggestions )
		Con_Printf( "\n^3 %i demos found.\n", numdems );
	Mem_Free( t );

	// cut shortestMatch to the amount common with s
	if( completedname && length )
	{
		for( i = 0; matchbuf[i]; i++ )
		{
			if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
				completedname[i] = 0;
		}
	}
	return true;
}

/*
=====================================
Cmd_GetMovieList

Prints or complete movie filename
=====================================
*/
static qboolean Cmd_GetMovieList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	search_t		*t;
	string		matchbuf;
	int		i, nummovies;

	t = FS_Search( va( "media/%s*.avi", s ), true, false );
	if( !t ) return false;

	COM_FileBase( t->filenames[0], matchbuf, sizeof( matchbuf ));
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( t->numfilenames == 1 ) return true;

	for(i = 0, nummovies = 0; i < t->numfilenames; i++)
	{
		if( Q_stricmp( COM_FileExtension( t->filenames[i] ), "avi" ))
			continue;

		COM_FileBase( t->filenames[i], matchbuf, sizeof( matchbuf ));
		if( print_suggestions )
			Con_Printf( "%16s\n", matchbuf );
		nummovies++;
	}

	if( print_suggestions )
		Con_Printf( "\n^3 %i movies found.\n", nummovies );
	Mem_Free( t );

	// cut shortestMatch to the amount common with s
	if( completedname && length )
	{
		for( i = 0; matchbuf[i]; i++ )
		{
			if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
				completedname[i] = 0;
		}
	}

	return true;
}

/*
=====================================
Cmd_GetMusicList

Prints or complete background track filename
=====================================
*/
static qboolean Cmd_GetMusicList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	search_t		*t;
	string		matchbuf;
	int		i, numtracks;

	t = FS_Search( va( "media/%s*.*", s ), true, false );
	if( !t ) return false;

	COM_FileBase( t->filenames[0], matchbuf, sizeof( matchbuf ));
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( t->numfilenames == 1 ) return true;

	for(i = 0, numtracks = 0; i < t->numfilenames; i++)
	{
		const char *ext = COM_FileExtension( t->filenames[i] );

		if( Q_stricmp( ext, "wav" ) && Q_stricmp( ext, "mp3" ))
			continue;

		COM_FileBase( t->filenames[i], matchbuf, sizeof( matchbuf ));
		if( print_suggestions )
			Con_Printf( "%16s\n", matchbuf );
		numtracks++;
	}

	if( print_suggestions )
		Con_Printf( "\n^3 %i soundtracks found.\n", numtracks );
	Mem_Free(t);

	// cut shortestMatch to the amount common with s
	if( completedname && length )
	{
		for( i = 0; matchbuf[i]; i++ )
		{
			if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
				completedname[i] = 0;
		}
	}
	return true;
}

/*
=====================================
Cmd_GetSavesList

Prints or complete savegame filename
=====================================
*/
static qboolean Cmd_GetSavesList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	search_t		*t;
	string		matchbuf;
	int		i, numsaves;

	t = FS_Search( va( DEFAULT_SAVE_DIRECTORY "%s*.sav", s ), true, true );	// lookup only in gamedir
	if( !t ) return false;

	COM_FileBase( t->filenames[0], matchbuf, sizeof( matchbuf ));
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( t->numfilenames == 1 ) return true;

	for( i = 0, numsaves = 0; i < t->numfilenames; i++ )
	{
		if( Q_stricmp( COM_FileExtension( t->filenames[i] ), "sav" ))
			continue;

		COM_FileBase( t->filenames[i], matchbuf, sizeof( matchbuf ));
		if( print_suggestions )
			Con_Printf( "%16s\n", matchbuf );
		numsaves++;
	}

	if( print_suggestions )
		Con_Printf( "\n^3 %i saves found.\n", numsaves );
	Mem_Free( t );

	// cut shortestMatch to the amount common with s
	if( completedname && length )
	{
		for( i = 0; matchbuf[i]; i++ )
		{
			if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
				completedname[i] = 0;
		}
	}

	return true;
}

/*
=====================================
Cmd_GetConfigList

Prints or complete .cfg filename
=====================================
*/
static qboolean Cmd_GetConfigList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	search_t		*t;
	string		matchbuf;
	int		i, numconfigs;

	t = FS_Search( va( "%s*.cfg", s ), true, false );
	if( !t ) return false;

	COM_FileBase( t->filenames[0], matchbuf, sizeof( matchbuf ));
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( t->numfilenames == 1 ) return true;

	for( i = 0, numconfigs = 0; i < t->numfilenames; i++ )
	{
		if( Q_stricmp( COM_FileExtension( t->filenames[i] ), "cfg" ))
			continue;

		COM_FileBase( t->filenames[i], matchbuf, sizeof( matchbuf ));
		if( print_suggestions )
			Con_Printf( "%16s\n", matchbuf );
		numconfigs++;
	}

	if( print_suggestions )
		Con_Printf( "\n^3 %i configs found.\n", numconfigs );
	Mem_Free( t );

	// cut shortestMatch to the amount common with s
	if( completedname && length )
	{
		for( i = 0; matchbuf[i]; i++ )
		{
			if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
				completedname[i] = 0;
		}
	}

	return true;
}

/*
=====================================
Cmd_GetSoundList

Prints or complete sound filename
=====================================
*/
static qboolean Cmd_GetSoundList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	search_t		*t;
	string		matchbuf;
	int		i, numsounds;

	t = FS_Search( va( "%s%s*.*", DEFAULT_SOUNDPATH, s ), true, false );
	if( !t ) return false;

	Q_strncpy( matchbuf, t->filenames[0] + sizeof( DEFAULT_SOUNDPATH ) - 1, sizeof( matchbuf ));
	COM_StripExtension( matchbuf );
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( t->numfilenames == 1 ) return true;

	for(i = 0, numsounds = 0; i < t->numfilenames; i++)
	{
		const char *ext = COM_FileExtension( t->filenames[i] );

		if( Q_stricmp( ext, "wav" ) && Q_stricmp( ext, "mp3" ))
			continue;

		Q_strncpy( matchbuf, t->filenames[i] + sizeof( DEFAULT_SOUNDPATH ) - 1, sizeof( matchbuf ));
		COM_StripExtension( matchbuf );
		if( print_suggestions )
			Con_Printf( "%16s\n", matchbuf );
		numsounds++;
	}

	if( print_suggestions )
		Con_Printf( "\n^3 %i sounds found.\n", numsounds );
	Mem_Free( t );

	// cut shortestMatch to the amount common with s
	if( completedname && length )
	{
		for( i = 0; matchbuf[i]; i++ )
		{
			if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
				completedname[i] = 0;
		}
	}

	return true;
}

/*
=====================================
Cmd_GetItemsList

Prints or complete item classname (weapons only)
=====================================
*/
static qboolean Cmd_GetItemsList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
#if !XASH_DEDICATED
	search_t		*t;
	string		matchbuf;
	int		i, numitems;

	if( !clgame.itemspath[0] ) return false; // not in game yet
	t = FS_Search( va( "%s/%s*.txt", clgame.itemspath, s ), true, false );
	if( !t ) return false;

	COM_FileBase( t->filenames[0], matchbuf, sizeof( matchbuf ));
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( t->numfilenames == 1 ) return true;

	for( i = 0, numitems = 0; i < t->numfilenames; i++ )
	{
		if( Q_stricmp( COM_FileExtension( t->filenames[i] ), "txt" ))
			continue;

		COM_FileBase( t->filenames[i], matchbuf, sizeof( matchbuf ));
		if( print_suggestions )
			Con_Printf( "%16s\n", matchbuf );
		numitems++;
	}

	if( print_suggestions )
		Con_Printf( "\n^3 %i items found.\n", numitems );
	Mem_Free( t );

	// cut shortestMatch to the amount common with s
	if( completedname && length )
	{
		for( i = 0; matchbuf[i]; i++ )
		{
			if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
				completedname[i] = 0;
		}
	}
	return true;
#endif // !XASH_DEDICATED
	return false;
}

/*
===============
Con_AddCommandToList

===============
*/
static void Con_AddCommandToList( const char *s, const char *value, const void *ptoggle, void *_autocompleteList )
{
	con_autocomplete_t *list = (con_autocomplete_t*)_autocompleteList;
	qboolean toggle = ptoggle != NULL && *(qboolean *)ptoggle;

	if( *s == '@' ) return; // never show system cvars or cmds
	if( list->matchCount >= CON_MAXCMDS ) return; // list is full

	if( toggle )
	{
		if( Q_strcmp( value, "0" ) && Q_strcmp( value, "1" ))
			return; // exclude non-toggable cvars
	}

	if( Q_strnicmp( s, list->completionString, Q_strlen( list->completionString ) ) )
		return; // no match

	list->cmds[list->matchCount++] = copystring( s );
}

/*
=================
Con_SortCmds
=================
*/
static int Con_SortCmds( const void *arg1, const void *arg2 )
{
	return Q_stricmp( *(const char **)arg1, *(const char **)arg2 );
}

/*
=====================================
Cmd_GetCommandsList

Autocomplete for bind command
=====================================
*/
static qboolean Cmd_GetCommandsAndCvarsList( const char *s, char *completedname, int length, qboolean cmds, qboolean cvars, qboolean toggle, qboolean print_suggestions )
{
	size_t i;
	string matchbuf;
	con_autocomplete_t list = { 0 }; // local autocomplete list

	list.completionString = s;

	// skip backslash
	while( *list.completionString && (*list.completionString == '\\' || *list.completionString == '/') )
		list.completionString++;

	if( COM_StringEmpty( list.completionString ) )
		return false;

	// find matching commands and variables
	if( cvars )
	{
		Cvar_LookupVars( 0, &toggle, &list, (setpair_t)Con_AddCommandToList );
	}

	if( cmds )
	{
		toggle = false;
		Cmd_LookupCmds( &toggle, &list, (setpair_t)Con_AddCommandToList );
	}

	if( !list.matchCount ) return false;
	Q_strncpy( matchbuf, list.cmds[0], sizeof( matchbuf ));
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( list.matchCount == 1 ) return true;

	qsort( list.cmds, list.matchCount, sizeof( char* ), Con_SortCmds );

	for( i = 0; i < list.matchCount; i++ )
	{
		Q_strncpy( matchbuf, list.cmds[i], sizeof( matchbuf ));
		if( print_suggestions )
			Con_Printf( "%16s\n", matchbuf );
	}

	if( print_suggestions )
		Con_Printf( "\n^3 %i %s found.\n", list.matchCount, cmds ? "commands" : "variables" );

	if( completedname && length )
	{
		for( i = 0; matchbuf[i]; i++ )
		{
			if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
				completedname[i] = 0;
		}
	}

	for( i = 0; i < list.matchCount; i++ )
	{
		if( list.cmds[i] != NULL )
		{
			Mem_Free( list.cmds[i] );
		}
	}

	return true;
}

/*
=====================================
Cmd_GetCommandsList

Autocomplete for bind command
=====================================
*/
static qboolean Cmd_GetCommandsList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	return Cmd_GetCommandsAndCvarsList( s, completedname, length, true, true, false, print_suggestions );
}

/*
=====================================
Cmd_GetCvarList

Autocomplete for bind command
=====================================
*/
static qboolean Cmd_GetCvarsList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	qboolean toggle = !Q_stricmp( Cmd_Argv( 0 ), "toggle" );
	return Cmd_GetCommandsAndCvarsList( s, completedname, length, false, true, toggle, print_suggestions );
}

/*
=====================================
Cmd_GetCustomList

Prints or complete .HPK filenames
=====================================
*/
static qboolean Cmd_GetCustomList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	search_t		*t;
	string		matchbuf;
	int		i, numitems;

	t = FS_Search( va( "%s*.hpk", s ), true, false );
	if( !t ) return false;

	COM_FileBase( t->filenames[0], matchbuf, sizeof( matchbuf ));
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( t->numfilenames == 1 ) return true;

	for(i = 0, numitems = 0; i < t->numfilenames; i++)
	{
		if( Q_stricmp( COM_FileExtension( t->filenames[i] ), "hpk" ))
			continue;

		COM_FileBase( t->filenames[i], matchbuf, sizeof( matchbuf ));
		if( print_suggestions )
			Con_Printf( "%16s\n", matchbuf );
		numitems++;
	}

	if( print_suggestions )
		Con_Printf( "\n^3 %i items found.\n", numitems );
	Mem_Free( t );

	// cut shortestMatch to the amount common with s
	if( completedname && length )
	{
		for( i = 0; matchbuf[i]; i++ )
		{
			if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
				completedname[i] = 0;
		}
	}
	return true;
}

/*
=====================================
Cmd_GetGameList

Prints or complete gamedir name
=====================================
*/
static qboolean Cmd_GetGamesList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	int	i, numgamedirs;
	string	gamedirs[MAX_MODS];
	string	matchbuf;
	int	len;

	// compare gamelist with current keyword
	len = Q_strlen( s );

	for( i = 0, numgamedirs = 0; i < FI->numgames; i++ )
	{
		if(( *s == '*' ) || !Q_strnicmp( FI->games[i]->gamefolder, s, len))
			Q_strncpy( gamedirs[numgamedirs++], FI->games[i]->gamefolder, sizeof( gamedirs[0] ));
	}

	if( !numgamedirs ) return false;
	Q_strncpy( matchbuf, gamedirs[0], sizeof( matchbuf ));
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( numgamedirs == 1 ) return true;

	for( i = 0; i < numgamedirs; i++ )
	{
		Q_strncpy( matchbuf, gamedirs[i], sizeof( matchbuf ));
		if( print_suggestions )
			Con_Printf( "%16s\n", matchbuf );
	}

	if( print_suggestions )
		Con_Printf( "\n^3 %i games found.\n", numgamedirs );

	// cut shortestMatch to the amount common with s
	if( completedname && length )
	{
		for( i = 0; matchbuf[i]; i++ )
		{
			if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
				completedname[i] = 0;
		}
	}
	return true;
}

/*
=====================================
Cmd_GetCDList

Prints or complete CD command name
=====================================
*/
static qboolean Cmd_GetCDList( const char *s, char *completedname, int length, qboolean print_suggestions )
{
	int i, numcdcommands;
	string	cdcommands[8];
	string	matchbuf;
	int	len;

	const char *cd_command[] =
	{
	"info",
	"loop",
	"off",
	"on",
	"pause",
	"play",
	"resume",
	"stop",
	};

	// compare CD command list with current keyword
	len = Q_strlen( s );

	for( i = 0, numcdcommands = 0; i < 8; i++ )
	{
		if(( *s == '*' ) || !Q_strnicmp( cd_command[i], s, len))
			Q_strncpy( cdcommands[numcdcommands++], cd_command[i], sizeof( cdcommands[0] ));
	}

	if( !numcdcommands ) return false;
	Q_strncpy( matchbuf, cdcommands[0], sizeof( matchbuf ));
	if( completedname && length )
		Q_strncpy( completedname, matchbuf, length );
	if( numcdcommands == 1 ) return true;

	for( i = 0; i < numcdcommands; i++ )
	{
		Q_strncpy( matchbuf, cdcommands[i], sizeof( matchbuf ));
		if( print_suggestions )
			Con_Printf( "%16s\n", matchbuf );
	}

	if( print_suggestions )
		Con_Printf( "\n^3 %i commands found.\n", numcdcommands );

	// cut shortestMatch to the amount common with s
	if( completedname && length )
	{
		for( i = 0; matchbuf[i]; i++ )
		{
			if( Q_tolower( completedname[i] ) != Q_tolower( matchbuf[i] ))
				completedname[i] = 0;
		}
	}
	return true;
}

static qboolean Cmd_CheckMapsList_R( qboolean fRefresh, qboolean onlyingamedir )
{
	qboolean	use_filter = false;
	string	mpfilter;
	char	*buffer;
	size_t	buffersize;
	string	result;
	int	i, size;
	search_t	*t;
	file_t	*f;

	if( FS_FileSize( "maps.lst", onlyingamedir ) > 0 && !fRefresh )
		return true; // exist

	// setup mpfilter
	Q_snprintf( mpfilter, sizeof( mpfilter ), "maps/%s", GI->mp_filter );
	t = FS_Search( "maps/*.bsp", false, onlyingamedir );

	if( !t )
	{
		if( onlyingamedir )
		{
			// mod doesn't contain any maps (probably this is a bot)
			return Cmd_CheckMapsList_R( fRefresh, false );
		}
		return false;
	}

	buffersize = t->numfilenames * 2 * sizeof( result );
	buffer = Mem_Calloc( host.mempool, buffersize );
	use_filter = !COM_StringEmpty( GI->mp_filter );

	for( i = 0; i < t->numfilenames; i++ )
	{
		char		*ents = NULL, *pfile;
		int		lumpofs = 0, lumplen = 0;
		string		mapname, message, entfilename;

		if( Q_stricmp( COM_FileExtension( t->filenames[i] ), "bsp" ))
			continue;

		if( use_filter && Q_stristr( t->filenames[i], mpfilter ))
			continue;

		f = FS_Open( t->filenames[i], "rb", onlyingamedir );
		COM_FileBase( t->filenames[i], mapname, sizeof( mapname ));

		if( f )
		{
			qboolean    have_spawnpoints = false;
			dlump_t     entities;
			fs_offset_t filelen;
			byte        buf[MAX_SYSPATH] = { 0 };

			filelen = FS_Read( f, buf, MAX_SYSPATH );

			// check all the lumps and some other errors
			if( !Mod_TestBmodelLumps( f, t->filenames[i], buf, filelen, true, &entities ))
			{
				FS_Close( f );
				continue;
			}

			// after call Mod_TestBmodelLumps we gurantee what map is valid
			lumpofs = entities.fileofs;
			lumplen = entities.filelen;

			Q_strncpy( entfilename, t->filenames[i], sizeof( entfilename ));
			COM_ReplaceExtension( entfilename, ".ent", sizeof( entfilename ));
			ents = (char *)FS_LoadFile( entfilename, NULL, true );

			if( !ents && lumplen >= 10 )
			{
				FS_Seek( f, lumpofs, SEEK_SET );
				ents = Z_Calloc( lumplen + 1 );
				FS_Read( f, ents, lumplen );
			}

			if( ents )
			{
				// if there are entities to parse, a missing message key just
				// means there is no title, so clear the message string now
				char	token[MAX_TOKEN];
				qboolean	worldspawn = true;

				Q_strncpy( message, "No Title", sizeof( message ));
				pfile = ents;

				while(( pfile = COM_ParseFile( pfile, token, sizeof( token ))) != NULL )
				{
					if( token[0] == '}' && worldspawn )
					{
						worldspawn = false;

						// if mod has mp_filter set up, then it's a mod that
						// might not have valid mp_entity set in GI
						// if mod is multiplayer only, assume all maps are valid
						if( use_filter || GI->gamemode == GAME_MULTIPLAYER_ONLY )
						{
							have_spawnpoints = true;
							break;
						}
					}
					else if( !Q_strcmp( token, "message" ) && worldspawn )
					{
						// get the message contents
						pfile = COM_ParseFile( pfile, message, sizeof( message ));
					}
					else if( !Q_strcmp( token, "classname" ))
					{
						pfile = COM_ParseFile( pfile, token, sizeof( token ));

						if( !Q_strcmp( token, GI->mp_entity ))
						{
							have_spawnpoints = true;
							break;
						}
					}

					if( have_spawnpoints )
						break; // valid map
				}
				Mem_Free( ents );
			}

			if( f ) FS_Close( f );

			if( have_spawnpoints )
			{
				// format: mapname "maptitle"\n
				Q_snprintf( result, sizeof( result ), "%s \"%s\"\n", mapname, message );
				Q_strncat( buffer, result, buffersize ); // add new string
			}
		}
	}

	if( t ) Mem_Free( t ); // free search result
	size = Q_strlen( buffer );

	if( !size )
	{
		if( buffer ) Mem_Free( buffer );

		if( onlyingamedir )
			return Cmd_CheckMapsList_R( fRefresh, false );
		return false;
	}

	// write generated maps.lst
	if( FS_WriteFile( "maps.lst", buffer, size))
	{
		if( buffer ) Mem_Free( buffer );
		return true;
	}
	return false;
}

int GAME_EXPORT Cmd_CheckMapsList( int fRefresh )
{
	return Cmd_CheckMapsList_R( fRefresh, true );
}

// keep this sorted
static const autocomplete_list_t cmd_list[] =
{
#if !XASH_DEDICATED
{ "bind", 1, Cmd_GetKeysList },
{ "bind", 2, Cmd_GetCommandsList },
{ "cd", 1, Cmd_GetCDList },
#endif
{ "changelevel2", 1, Cmd_GetMapList },
{ "changelevel", 1, Cmd_GetMapList },
#if !XASH_DEDICATED
{ "drop", 1, Cmd_GetItemsList },
#endif
{ "entpatch", 1, Cmd_GetMapList },
{ "exec", 1, Cmd_GetConfigList },
{ "game", 1, Cmd_GetGamesList },
#if !XASH_DEDICATED
{ "give", 1, Cmd_GetItemsList },
#endif
{ "hpkextract", 1, Cmd_GetCustomList },
{ "hpklist", 1, Cmd_GetCustomList },
{ "hpkval", 1, Cmd_GetCustomList },
#if !XASH_DEDICATED
{ "listdemo", 1, Cmd_GetDemoList, },
#endif
{ "load", 1, Cmd_GetSavesList },
{ "map", 1, Cmd_GetMapList },
#if !XASH_DEDICATED
{ "map_background", 1, Cmd_GetMapList },
{ "movie", 1, Cmd_GetMovieList },
{ "mp3", 1, Cmd_GetCDList },
{ "music", 1, Cmd_GetMusicList, },
{ "play", 1, Cmd_GetSoundList },
{ "playdemo", 1, Cmd_GetDemoList, },
{ "playvol", 1, Cmd_GetSoundList },
#endif
{ "reset", 1, Cmd_GetCvarsList },
{ "save", 1, Cmd_GetSavesList },
{ "set", 1, Cmd_GetCvarsList },
#if !XASH_DEDICATED
{ "timedemo", 1, Cmd_GetDemoList },
#endif
{ "toggle", 1, Cmd_GetCvarsList },
#if !XASH_DEDICATED
{ "unbind", 1, Cmd_GetKeysList },
#endif
};

/*
===============
Cmd_CheckName

compare first argument with string
===============
*/
static qboolean Cmd_CheckName( const char *name )
{
	const char *p = Cmd_Argv( 0 );

	if( !Q_stricmp( p, name ))
		return true;

	if( p[0] == '\\' && !Q_stricmp( &p[1], name ))
		return true;

	return false;
}

/*
============
Cmd_AutocompleteName

Autocomplete filename
for various cmds
============
*/
static qboolean Cmd_AutocompleteName( const char *source, int arg, char *buffer, size_t bufsize, qboolean print_suggestions )
{
	int i;

	for( i = 0; i < ARRAYSIZE( cmd_list ); i++  )
	{
		if( cmd_list[i].arg == arg && Cmd_CheckName( cmd_list[i].name ))
			return cmd_list[i].func( source, buffer, bufsize, print_suggestions );
	}

	return false;
}

/*
===============
Con_PrintCmdMatches
===============
*/
static void Con_PrintCmdMatches( const char *s, const char *unused1, const char *m, void *unused2 )
{
	if( !Q_strnicmp( s, con.shortestMatch, Q_strlen( con.shortestMatch ) ) )
	{
		if( !COM_StringEmptyOrNULL( m ) )
			Con_Printf( "    %s ^3\"%s\"\n", s, m );
		else Con_Printf( "    %s\n", s ); // variable or command without description
	}
}

/*
===============
Con_PrintCvarMatches
===============
*/
static void Con_PrintCvarMatches( const char *s, const char *value, const char *m, void *unused2 )
{
	if( !Q_strnicmp( s, con.shortestMatch, Q_strlen( con.shortestMatch ) ) )
	{
		if( !COM_StringEmptyOrNULL( m ))
			Con_Printf( "    %s (%s)   ^3\"%s\"\n", s, value, m );
		else Con_Printf( "    %s  (%s)\n", s, value ); // variable or command without description
	}
}

/*
===============
Con_ConcatRemaining
===============
*/
static void Con_ConcatRemaining( const char *src, const char *start )
{
	const char	*arg;
	int	i;

	arg = Q_strstr( src, start );

	if( !arg )
	{
		for( i = 1; i < Cmd_Argc(); i++ )
		{
			Q_strncat( con.completionField->buffer, " ", sizeof( con.completionField->buffer ) );
			arg = Cmd_Argv( i );
			while( *arg )
			{
				if( *arg == ' ' )
				{
					Q_strncat( con.completionField->buffer, "\"", sizeof( con.completionField->buffer ) );
					break;
				}
				arg++;
			}

			Q_strncat( con.completionField->buffer, Cmd_Argv( i ), sizeof( con.completionField->buffer ) );
			if( *arg == ' ' ) Q_strncat( con.completionField->buffer, "\"", sizeof( con.completionField->buffer ) );
		}
		return;
	}

	arg += Q_strlen( start );
	Q_strncat( con.completionField->buffer, arg, sizeof( con.completionField->buffer ) );
}

/*
===============
Con_CompleteCommand

perform Tab expansion
===============
*/
void Con_CompleteCommand( field_t *field, qboolean print_suggestions )
{
	field_t	temp;
	string	filename;
	qboolean toggle = false;
	qboolean	nextcmd;
	int	i;

	// setup the completion field
	con.completionField = field;

	if( COM_StringEmpty( con.completionField->buffer ))
		return;

	// only look at the first token for completion purposes
	Cmd_TokenizeString( con.completionField->buffer );

	nextcmd = (con.completionField->buffer[Q_strlen( con.completionField->buffer ) - 1] == ' ') ? true : false;

	con.completionString = Cmd_Argv( 0 );

	// skip backslash
	while( *con.completionString && (*con.completionString == '\\' || *con.completionString == '/') )
		con.completionString++;

	if( COM_StringEmpty( con.completionString ) )
		return;

	// free the old autocomplete list
	for( i = 0; i < con.matchCount; i++ )
	{
		if( con.cmds[i] != NULL )
		{
			Mem_Free( con.cmds[i] );
			con.cmds[i] = NULL;
		}
	}

	con.matchCount = 0;
	con.shortestMatch[0] = 0;

	// find matching commands and variables
	Cmd_LookupCmds( &toggle, &con, (setpair_t)Con_AddCommandToList );
	Cvar_LookupVars( 0, &toggle, &con, (setpair_t)Con_AddCommandToList );

	if( !con.matchCount ) return; // no matches

	temp = *con.completionField;

	// autocomplete second arg
	if(( Cmd_Argc() >= 2 ) || ( Cmd_Argc() == 1 && nextcmd ))
	{
		con.completionBuffer = Cmd_Argv( Cmd_Argc() - 1 );

		// skip backslash
		while( *con.completionBuffer && (*con.completionBuffer == '\\' || *con.completionBuffer == '/') )
			con.completionBuffer++;

		if( COM_StringEmpty( con.completionBuffer ) )
			return;

		if( Cmd_AutocompleteName( con.completionBuffer, Cmd_Argc() - 1, filename, sizeof( filename ), print_suggestions ))
		{
			con.completionField->buffer[0] = 0;

			for( i = 0; i < Cmd_Argc() - 1; i++ )
			{
				Q_strncat( con.completionField->buffer, Cmd_Argv( i ), sizeof( con.completionField->buffer ));
				Q_strncat( con.completionField->buffer, " ", sizeof( con.completionField->buffer ));
			}
			Q_strncat( con.completionField->buffer, filename, sizeof( con.completionField->buffer ));
			con.completionField->cursor = Q_strlen( con.completionField->buffer );
		}

		// don't adjusting cursor pos if we nothing found
		return;
	}

	if( con.matchCount == 1 )
	{
		Q_strncpy( con.completionField->buffer, con.cmds[0], sizeof( con.completionField->buffer ));
		if( Cmd_Argc() == 1 ) Q_strncat( con.completionField->buffer, " ", sizeof( con.completionField->buffer ) );
		else Con_ConcatRemaining( temp.buffer, con.completionString );
		con.completionField->cursor = Q_strlen( con.completionField->buffer );
	}
	else
	{
		char	*first, *last;
		int	len = 0;

		qsort( con.cmds, con.matchCount, sizeof( char* ), Con_SortCmds );

		// find the number of matching characters between the first and
		// the last element in the list and copy it
		first = con.cmds[0];
		last = con.cmds[con.matchCount - 1];

		while( *first && *last && Q_tolower( *first ) == Q_tolower( *last ) )
		{
			first++;
			last++;

			con.shortestMatch[len] = con.cmds[0][len];
			len++;
		}
		con.shortestMatch[len] = 0;

		// multiple matches, complete to shortest
		Q_strncpy( con.completionField->buffer, con.shortestMatch, sizeof( con.completionField->buffer ));
		con.completionField->cursor = Q_strlen( con.completionField->buffer );
		Con_ConcatRemaining( temp.buffer, con.completionString );

		// run through again, printing matches
		if( print_suggestions )
		{
			Con_Printf( "]%s\n", con.completionField->buffer );

			Cmd_LookupCmds( NULL, NULL, (setpair_t)Con_PrintCmdMatches );
			Cvar_LookupVars( 0, NULL, NULL, (setpair_t)Con_PrintCvarMatches );
		}
	}
}

/*
=========
Cmd_AutoComplete

NOTE: input string must be equal or longer than MAX_STRING
=========
*/
void Cmd_AutoComplete( char *complete_string )
{
	field_t	input;

	if( !complete_string || !*complete_string )
		return;

	// setup input
	Q_strncpy( input.buffer, complete_string, sizeof( input.buffer ) );
	input.cursor = input.scroll = 0;

	Con_CompleteCommand( &input, true );

	// setup output
	if( input.buffer[0] == '\\' || input.buffer[0] == '/' )
		Q_strncpy( complete_string, input.buffer + 1, sizeof( input.buffer ) );
	else Q_strncpy( complete_string, input.buffer, sizeof( input.buffer ) );
}

/*
============
Cmd_AutoCompleteClear

============
*/
void Cmd_AutoCompleteClear( void )
{
	int i;

	// free the old autocomplete list
	for( i = 0; i < con.matchCount; i++ )
	{
		if( con.cmds[i] != NULL )
		{
			Mem_Free( con.cmds[i] );
			con.cmds[i] = NULL;
		}
	}

	con.matchCount = 0;
}

/*
============
Cmd_WriteVariables

Appends lines containing "set variable value" for all variables
with the archive flag set to true.
============
*/
static void Cmd_WriteOpenGLCvar( const char *name, const char *string, const char *desc, void *f )
{
	if( COM_StringEmptyOrNULL( desc ))
		return; // ignore cvars without description (fantom variables)
	FS_Printf( f, "%s \"%s\"\n", name, string );
}

static void Cmd_WriteHelp(const char *name, const char *unused, const char *desc, void *f )
{
	int	length;

	if( COM_StringEmptyOrNULL( desc ))
		return; // ignore fantom cmds

	if( name[0] == '+' || name[0] == '-' )
		return; // key bindings

	length = 3 - (Q_strlen( name ) / 10); // Asm_Ed default tab stop is 10

	if( length == 3 ) FS_Printf( f, "%s\t\t\t\"%s\"\n", name, desc );
	if( length == 2 ) FS_Printf( f, "%s\t\t\"%s\"\n", name, desc );
	if( length == 1 ) FS_Printf( f, "%s\t\"%s\"\n", name, desc );
	if( length == 0 ) FS_Printf( f, "%s \"%s\"\n", name, desc );
}

static void Cmd_WriteOpenGLVariables( file_t *f )
{
	Cvar_LookupVars( FCVAR_GLCONFIG, NULL, f, (setpair_t)Cmd_WriteOpenGLCvar );
}

void Host_InitializeConfig( file_t *f, const char *config, const char *description )
{
	FS_Printf( f, "//=======================================================================\n");
	FS_Printf( f, "//\tGenerated by "XASH_ENGINE_NAME" (%i, %s, %s, %s-%s)\n", Q_buildnum(), g_buildcommit, g_buildbranch, Q_buildos(), Q_buildarch());
	FS_Printf( f, "//\t\t%s - %s\n", config, description );
	FS_Printf( f, "//=======================================================================\n" );
}

void Host_FinalizeConfig( file_t *f, const char *config )
{
	string backup, newcfg;

	Q_snprintf( backup, sizeof( backup ), "%s.bak", config );
	Q_snprintf( newcfg, sizeof( newcfg ), "%s.new", config );

	FS_Printf( f, "// end of %s\n", config );
	FS_Close( f );
	FS_Delete( backup );
	FS_Rename( config, backup );
	FS_Rename( newcfg, config );
}


#if !XASH_DEDICATED
/*
===============
Host_WriteConfig

Writes key bindings and archived cvars to config.cfg
===============
*/
void Host_WriteConfig( void )
{
	kbutton_t	*mlook = NULL;
	kbutton_t	*jlook = NULL;
	file_t	*f;

	if( !clgame.hInstance || Sys_CheckParm( "-nowriteconfig" ) ) return;


	f = FS_Open( "config.cfg.new", "w", false );
	if( f )
	{
		Con_Reportf( "%s()\n", __func__ );
		Host_InitializeConfig( f, "config.cfg", "archive of cvars" );
		Key_WriteBindings( f );
		Cvar_WriteVariables( f, FCVAR_ARCHIVE );
		Info_WriteVars( f );

		if( clgame.hInstance )
		{
			mlook = (kbutton_t *)clgame.dllFuncs.KB_Find( "in_mlook" );
			jlook = (kbutton_t *)clgame.dllFuncs.KB_Find( "in_jlook" );
		}

		if( mlook && ( mlook->state & 1 ))
			FS_Printf( f, "+mlook\n" );

		if( jlook && ( jlook->state & 1 ))
			FS_Printf( f, "+jlook\n" );

		FS_Printf( f, "exec userconfig.cfg\n" );

		Host_FinalizeConfig( f, "config.cfg" );
	}
	else Con_DPrintf( S_ERROR "Couldn't write config.cfg.\n" );

	NET_SaveMasters();

	FS_SaveVFSConfig();
}

/*
===============
Host_WriteServerConfig

save serverinfo variables into server.cfg (using for dedicated server too)
===============
*/
void GAME_EXPORT Host_WriteServerConfig( const char *name )
{
	file_t	*f;
	string newconfigfile;

	Q_snprintf( newconfigfile, MAX_STRING, "%s.new", name );

	if(( f = FS_Open( newconfigfile, "w", false )) != NULL )
	{
		Host_InitializeConfig( f, "game.cfg", "multiplayer server temporary config" );

		Cvar_WriteVariables( f, FCVAR_SERVER );

		Host_FinalizeConfig( f, name );
	}
	else Con_DPrintf( S_ERROR "Couldn't write %s.\n", name );
}

/*
===============
Host_WriteOpenGLConfig

save opengl variables into opengl.cfg
===============
*/
void Host_WriteOpenGLConfig( void )
{
	const char *config_name;
	string name;
	file_t	*f;

	if( Sys_CheckParm( "-nowriteconfig" ) || !ref.dllFuncs.R_GetConfigName )
		return;

	config_name = ref.dllFuncs.R_GetConfigName();

	if( !config_name )
		return;

	Q_snprintf( name, sizeof( name ), "%s.cfg", config_name );

	f = FS_Open( va( "%s.new", name ), "w", false );
	if( f )
	{
		Con_Reportf( "%s()\n", __func__ );
		Host_InitializeConfig( f, name, "archive of renderer implementation cvars" );
		Cmd_WriteOpenGLVariables( f );

		Host_FinalizeConfig( f, name );
	}
	else Con_DPrintf( S_ERROR "can't update %s.\n", name );
}

/*
===============
Host_WriteVideoConfig

save render variables into video.cfg
===============
*/
void Host_WriteVideoConfig( void )
{
	file_t	*f;

	if( Sys_CheckParm( "-nowriteconfig" ) )
		return;

	f = FS_Open( "video.cfg.new", "w", false );
	if( f )
	{
		Con_Reportf( "%s()\n", __func__ );
		Host_InitializeConfig( f, "video.cfg", "archive of renderer variables" );
		Cvar_WriteVariables( f, FCVAR_RENDERINFO );
		Host_FinalizeConfig( f, "video.cfg" );
	}
	else Con_DPrintf( S_ERROR "can't update video.cfg.\n" );
}
#endif // XASH_DEDICATED

void Key_EnumCmds_f( void )
{
	file_t	*f;

	FS_AllowDirectPaths( true );
	if( FS_FileExists( "../help.txt", false ))
	{
		Con_Printf( "help.txt already exist\n" );
		FS_AllowDirectPaths( false );
		return;
	}

	f = FS_Open( "../help.txt", "w", false );
	if( f )
	{
		Host_InitializeConfig( f, "help.txt", "xash commands and console variables" );
		FS_Printf( f, "\n\n\t\t\tconsole variables\n\n");
		Cvar_LookupVars( 0, NULL, f, (setpair_t)Cmd_WriteHelp );
		FS_Printf( f, "\n\n\t\t\tconsole commands\n\n");
		Cmd_LookupCmds( NULL, f, (setpair_t)Cmd_WriteHelp );
  		FS_Printf( f, "\n\n");
		FS_Close( f );
		Con_Printf( "help.txt created\n" );
	}
	else Con_Printf( S_ERROR "couldn't write help.txt.\n");
	FS_AllowDirectPaths( false );
}
