// log.c: handles console logging functions and cvars

#include "quakedef.h"

// cvar callbacks
static void QDECL Log_Dir_Callback (struct cvar_s *var, char *oldvalue);
static void QDECL Log_Name_Callback (struct cvar_s *var, char *oldvalue);

// cvars
#define CONLOGGROUP "Console logging"
cvar_t		log_enable[LOG_TYPES]	= {	CVARF("log_enable", "0", CVAR_NOTFROMSERVER),
										CVARF("log_enable_players", "0", CVAR_NOTFROMSERVER),
										CVARF("log_enable_rcon", "1", CVAR_NOTFROMSERVER)
								};
cvar_t		log_name[LOG_TYPES] = { CVARFC("log_name", "", CVAR_NOTFROMSERVER, Log_Name_Callback),
									CVARFC("log_name_players", "players", CVAR_NOTFROMSERVER, Log_Name_Callback),
									CVARFC("log_name_rcon", "rcon", CVAR_NOTFROMSERVER, Log_Name_Callback)};
cvar_t		log_dir = CVARFC("log_dir", "", CVAR_NOTFROMSERVER, Log_Dir_Callback);
cvar_t		log_readable = CVARFD("log_readable", "7", CVAR_NOTFROMSERVER, "Bitfield describing what to convert/strip. If 0, exact byte representation will be used.\n&1: Dequakify text.\n&2: Strip special markup.\n&4: Strip ansi control codes.");
cvar_t		log_developer = CVARF("log_developer", "0", CVAR_NOTFROMSERVER);
cvar_t		log_rotate_files = CVARF("log_rotate_files", "0", CVAR_NOTFROMSERVER);
cvar_t		log_rotate_size = CVARF("log_rotate_size", "131072", CVAR_NOTFROMSERVER);
cvar_t		log_timestamps = CVARF("log_timestamps", "1", CVAR_NOTFROMSERVER);
#ifdef _WIN32
cvar_t		log_dosformat = CVARF("log_dosformat", "1", CVAR_NOTFROMSERVER);
#else
cvar_t		log_dosformat = CVARF("log_dosformat", "0", CVAR_NOTFROMSERVER);
#endif
qboolean	log_newline[LOG_TYPES];

// externals
extern char gamedirfile[];

// Log_Dir_Callback: called when a log_dir is changed
static void QDECL Log_Dir_Callback (struct cvar_s *var, char *oldvalue)
{
	char *t = var->string;
	char *e = t + (*t?strlen(t):0);

	// sanity check for directory. // is equivelent to /../ on some systems, so make sure that can't be used either. : is for drives on windows or amiga, or alternative thingies on windows, so block thoses completely.
	if (strstr(t, "..") || strstr(t, ":") || *t == '/' || *t == '\\' || *e == '/' || *e == '\\' || strstr(t, "//") || strstr(t, "\\\\"))
	{
		Con_Printf(CON_NOTICE "%s forced to default due to invalid characters.\n", var->name);
		// recursion is avoided by assuming the default value is sane
		Cvar_ForceSet(var, var->defaultstr);
	}
}

// Log_Name_Callback: called when a log_name is changed
static void QDECL Log_Name_Callback (struct cvar_s *var, char *oldvalue)
{
	char *t = var->string;

	// sanity check for directory
	if (strstr(t, "..") || strstr(t, ":") || strstr(t, "/") || strstr(t, "\\"))
	{
		Con_Printf(CON_NOTICE "%s forced to default due to invalid characters.\n", var->name);
		// recursion is avoided by assuming the default value is sane
		Cvar_ForceSet(var, var->defaultstr);
	}
}

// Con_Log: log string to console log
void Log_String (logtype_t lognum, char *s)
{
	vfsfile_t *fi;
	char *d; // directory
	char *f; // filename
	char *t;
	char utf8[2048];
	int i;
	char fbase[MAX_QPATH];
	char fname[MAX_QPATH];
	conchar_t cline[2048], *c;
	unsigned int u;

	f = NULL;
	switch(lognum)
	{
	case LOG_CONSOLE:
		f = "qconsole";
		break;
	case LOG_PLAYER:
		f = "players";
		break;
	case LOG_RCON:
		f = "rcon";
		break;
	default:
		return;
	}

	if (!log_enable[lognum].value)
		return;

	// get directory/filename
	if (log_dir.string[0])
		d = log_dir.string;
	else
		d = "";//gamedirfile;

	if (log_name[lognum].string[0])
		f = log_name[lognum].string;

	if (!f)
		return;

	COM_ParseFunString(CON_WHITEMASK, s, cline, sizeof(cline), !(log_readable.ival & 2));
	t = utf8;
	for (c = cline; *c; c++)
	{
		if ((*c & CON_HIDDEN) && (log_readable.ival & 2))
			continue;
		if (log_readable.ival&1)
			u = COM_DeQuake(*c);
		else
			u = *c&CON_CHARMASK;

		//at the start of a new line, we might want a timestamp (so timestamps are correct for the first char of the line, instead of the preceeding \n)
		if (log_newline[lognum])
		{
			if (log_timestamps.ival)
			{
				time_t unixtime = time(NULL);
				int bufferspace = utf8+sizeof(utf8)-1-t;
				if (bufferspace > 0)
				{
					strftime(t, bufferspace, "%Y-%m-%d %H:%M:%S ", localtime(&unixtime));
					t += strlen(t);
				}
			}
			log_newline[lognum] = false;
		}

		//make sure control codes are stripped. no exploiting xterm bugs please.
		if ((log_readable.ival & 4) && ((u < 32 && u != '\t' && u != '\n') || u == 127 || (u >= 128 && u < 128+32))) //\r is stripped too
			u = '?';
		//if dos format logs, we insert a \r before every \n (also flag next char as the start of a new line)
		if (u == '\n')
		{
			log_newline[lognum] = true;
			if (log_dosformat.ival)
				t += utf8_encode(t, '\r', utf8+sizeof(utf8)-1-t);
		}
		t += utf8_encode(t, u, utf8+sizeof(utf8)-1-t);
	}
	*t = 0;

	if (*d)
		Q_snprintfz(fbase, sizeof(fname)-4, "%s/%s", d, f);
	else
		Q_snprintfz(fbase, sizeof(fname)-4, "%s", f);
	Q_snprintfz(fname, sizeof(fname), "%s.log", fbase);

	// file rotation
	if (log_rotate_size.value >= 4096 && log_rotate_files.value >= 1)
	{
		int x;
		vfsfile_t *fi;

		// check file size, use x as temp
		if ((fi = FS_OpenVFS(fname, "rb", FS_GAMEONLY)))
		{
			x = VFS_GETLEN(fi);
			VFS_CLOSE(fi);
			x += strlen(utf8); // add string size to file size to never go over
		}
		else
			x = 0;

		if (x > (int)log_rotate_size.value)
		{
			char newf[MAX_OSPATH];
			char oldf[MAX_OSPATH];

			i = log_rotate_files.value;

			// unlink file at the top of the chain
			Q_snprintfz(oldf, sizeof(oldf), "%s.%i.log", fbase, i);
			FS_Remove(oldf, FS_GAMEONLY);

			// rename files through chain
			for (x = i-1; x > 0; x--)
			{
				strcpy(newf, oldf);
				Q_snprintfz(oldf, sizeof(oldf), "%s.%i.log", fbase, x);

				// check if file exists, otherwise skip
				if ((fi = FS_OpenVFS(oldf, "rb", FS_GAMEONLY)))
					VFS_CLOSE(fi);
				else
					continue; // skip nonexistant files

				if (!FS_Rename(oldf, newf, FS_GAMEONLY))
				{
					// rename failed, disable log and bug out
					Cvar_ForceSet(&log_enable[lognum], "0");
					Con_Printf("Unable to rotate log files. Logging disabled.\n");
					return;
				}
			}

			// TODO: option to compress file somewhere in here?
			// rename our base file, which had better exist...
			if (!FS_Rename(fname, oldf, FS_GAMEONLY))
			{
				// rename failed, disable log and bug out
				Cvar_ForceSet(&log_enable[lognum], "0");
				Con_Printf("Unable to rename base log file. Logging disabled.\n");
				return;
			}
		}
	}

	FS_CreatePath(fname, FS_GAMEONLY);
	if ((fi = FS_OpenVFS(fname, "ab", FS_GAMEONLY)))
	{
		VFS_WRITE(fi, utf8, strlen(utf8));
		VFS_CLOSE(fi);
	}
	else
	{
		// write failed, bug out
		Cvar_ForceSet(&log_enable[lognum], "0");
		Con_Printf("Unable to write to log file. Logging disabled.\n");
		return;
	}
}

void Con_Log (char *s)
{
	Log_String(LOG_CONSOLE, s);
}


#ifndef CLIENTONLY
//still to add stuff at:
//connects
//disconnects
//kicked
void SV_LogPlayer(client_t *cl, char *msg)
{
	char line[2048];
	char remote_adr[MAX_ADR_SIZE];
	char realip_adr[MAX_ADR_SIZE];

	if (cl->protocol == SCP_BAD)
		return;	//don't log botclients

	snprintf(line, sizeof(line),
			"%s\\%s\\%i\\%s\\%s\\%i\\guid\\%s%s\n",
			msg, cl->name, cl->userid,
			NET_BaseAdrToString(remote_adr, sizeof(remote_adr), &cl->netchan.remote_address), (cl->realip_status > 0 ? NET_BaseAdrToString(realip_adr, sizeof(realip_adr), &cl->realip) : "??"),
			cl->netchan.remote_address.port, cl->guid, cl->userinfo);

	Log_String(LOG_PLAYER, line);
}
#endif





void Log_Logfile_f (void)
{
	extern char gamedirfile[];

	if (log_enable[LOG_CONSOLE].value)
	{
		Cvar_SetValue(&log_enable[LOG_CONSOLE], 0);
		Con_Printf("Logging disabled.\n");
	}
	else
	{
		char *d, *f;

		d = gamedirfile;
		if (log_dir.string[0])
			d = log_dir.string;

		f = "qconsole";
		if (log_name[LOG_CONSOLE].string[0])
			f = log_name[LOG_CONSOLE].string;

		Con_Printf("%s", va("Logging to %s/%s.log.\n", d, f));
		Cvar_SetValue(&log_enable[LOG_CONSOLE], 1);
	}

}
/*
void SV_Fraglogfile_f (void)
{
	char	name[MAX_OSPATH];
	int		i;

	if (sv_fraglogfile)
	{
		Con_TPrintf ("Frag file logging off.\n");
		VFS_CLOSE (sv_fraglogfile);
		sv_fraglogfile = NULL;
		return;
	}

	// find an unused name
	for (i=0 ; i<1000 ; i++)
	{
		sprintf (name, "frag_%i.log", i);
		sv_fraglogfile = FS_OpenVFS(name, "rb", FS_GAME);
		if (!sv_fraglogfile)
		{	// can't read it, so create this one
			sv_fraglogfile = FS_OpenVFS (name, "wb", FS_GAME);
			if (!sv_fraglogfile)
				i=1000;	// give error
			break;
		}
		VFS_CLOSE (sv_fraglogfile);
	}
	if (i==1000)
	{
		Con_TPrintf ("Can't open any logfiles.\n");
		sv_fraglogfile = NULL;
		return;
	}

	Con_TPrintf ("Logging frags to %s.\n", name);
}
*/


void Log_Init(void)
{
	int i;
	// register cvars
	for (i = 0; i < LOG_TYPES; i++)
	{
		Cvar_Register (&log_enable[i], CONLOGGROUP);
		Cvar_Register (&log_name[i], CONLOGGROUP);
		log_newline[i] = true;
	}
	Cvar_Register (&log_dir, CONLOGGROUP);
	Cvar_Register (&log_readable, CONLOGGROUP);
	Cvar_Register (&log_developer, CONLOGGROUP);
	Cvar_Register (&log_rotate_size, CONLOGGROUP);
	Cvar_Register (&log_rotate_files, CONLOGGROUP);
	Cvar_Register (&log_dosformat, CONLOGGROUP);
	Cvar_Register (&log_timestamps, CONLOGGROUP);

	Cmd_AddCommand("logfile", Log_Logfile_f);

	// cmd line options, debug options
#ifdef CRAZYDEBUGGING
	Cvar_ForceSet(&log_enable[LOG_CONSOLE], "1");
	TRACE(("dbg: Con_Init: log_enable forced\n"));
#endif

	if (COM_CheckParm("-condebug"))
		Cvar_ForceSet(&log_enable[LOG_CONSOLE], "1");
}
