/**
 * AutoExecConfig 
 *
 * Copyright (C) 2013-2019 Impact
 * 
 * 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
 * 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.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>
 */
 
#if defined _autoexecconfig_included
	#endinput
#endif
#define _autoexecconfig_included


#include <sourcemod>

#define AUTOEXECCONFIG_VERSION "0.1.5"
#define AUTOEXECCONFIG_URL     "https://forums.alliedmods.net/showthread.php?t=204254"

// Append
#define AUTOEXEC_APPEND_BAD_FILENAME    0
#define AUTOEXEC_APPEND_FILE_NOT_FOUND  1
#define AUTOEXEC_APPEND_BAD_HANDLE      2
#define AUTOEXEC_APPEND_SUCCESS         3



// Find
#define AUTOEXEC_FIND_BAD_FILENAME     10
#define AUTOEXEC_FIND_FILE_NOT_FOUND   11
#define AUTOEXEC_FIND_BAD_HANDLE       12
#define AUTOEXEC_FIND_NOT_FOUND        13
#define AUTOEXEC_FIND_SUCCESS          14



// Clean
#define AUTOEXEC_CLEAN_FILE_NOT_FOUND  20
#define AUTOEXEC_CLEAN_BAD_HANDLE      21
#define AUTOEXEC_CLEAN_SUCCESS         22



// General
#define AUTOEXEC_NO_CONFIG             30



// Formatter
#define AUTOEXEC_FORMAT_BAD_FILENAME   40
#define AUTOEXEC_FORMAT_SUCCESS        41



// Global variables
static char g_sConfigFile[PLATFORM_MAX_PATH];
static char g_sRawFileName[PLATFORM_MAX_PATH];
static char g_sFolderPath[PLATFORM_MAX_PATH];

static bool g_bCreateFile = false;
static Handle g_hPluginHandle = null;

static bool g_bCreateDirectory = false;
static int g_bCreateDirectoryMode = FPERM_U_READ|FPERM_U_WRITE|FPERM_U_EXEC|FPERM_G_READ|FPERM_G_EXEC|FPERM_O_READ|FPERM_O_EXEC;


// Workaround for now
static int g_iLastFindResult;
static int g_iLastAppendResult;




/**
 * Returns the last result from the parser.
 *
 * @return        Returns one of the AUTOEXEC_FIND values or -1 if not set.
*/
stock int AutoExecConfig_GetFindResult()
{
	return g_iLastFindResult;
}





/**
 * Returns the last result from the appender.
 *
 * @return        Returns one of the AUTOEXEC_APPEND values or -1 if not set.
*/
stock int AutoExecConfig_GetAppendResult()
{
	return g_iLastAppendResult;
}


/**
 * Set if the config file should be created by the autoexecconfig include itself if it doesn't exist.
 *
 * @param create        True if config file should be created, false otherwise.
 * @noreturn
 */
stock void AutoExecConfig_SetCreateFile(bool create)
{
	g_bCreateFile = create;
}


/**
 * Set if the config file's folder should be created by the autoexecconfig include itself if it doesn't exist.
 * Note: Must be used before AutoExecConfig_SetFile as the potential creation of it happens there 
 *
 * @param create        True if config file should be created, false otherwise.
 * @param mode          Folder permission mode, default is u=rwx,g=rx,o=rx.
 * @noreturn
 */
stock void AutoExecConfig_SetCreateDirectory(bool create, int mode=FPERM_U_READ|FPERM_U_WRITE|FPERM_U_EXEC|FPERM_G_READ|FPERM_G_EXEC|FPERM_O_READ|FPERM_O_EXEC)
{
	g_bCreateDirectory = create;
	g_bCreateDirectoryMode = mode;
}


/**
 * Returns if the config file should be created if it doesn't exist.
 *
 * @return        Returns true, if the config file should be created or false if it should not.
 */
stock bool AutoExecConfig_GetCreateFile()
{
	return g_bCreateFile;
}


/**
 * Set the plugin for which the config file should be created.
 * Set to null to use the calling plugin.
 * Used to print the correct filename in the top comment when creating the file.
 *
 * @param plugin        The plugin to create convars for or null to use the calling plugin.
 * @noreturn
 */
stock void AutoExecConfig_SetPlugin(Handle plugin)
{
	g_hPluginHandle = plugin;
}


/**
 * Returns the plugin's handle for which the config file is created.
 *
 * @return        The plugin handle
 */
stock Handle AutoExecConfig_GetPlugin()
{
	return g_hPluginHandle;
}


/**
 * Set the global autoconfigfile used by functions of this file.
 * Note: does not support subfolders like folder1/folder2
 *
 * @param file          Name of the config file, path and .cfg extension is being added if not given.
 * @param folder        Folder under cfg/ to use.  By default this is "sourcemod."
 * @return              True if formatter returned success, false otherwise.
*/
stock bool AutoExecConfig_SetFile(char[] file, char[] folder="sourcemod")
{
	Format(g_sConfigFile, sizeof(g_sConfigFile), "%s", file);
	
	// Global buffers for cfg execution
	strcopy(g_sRawFileName, sizeof(g_sRawFileName), file);
	strcopy(g_sFolderPath, sizeof(g_sFolderPath), folder);
	
	
	// Format the filename
	return AutoExecConfig_FormatFileName(g_sConfigFile, sizeof(g_sConfigFile), folder) == AUTOEXEC_FORMAT_SUCCESS;
}






/**
 * Get the formatted autoconfigfile used by functions of this file.
 *
 * @param buffer        String to format.
 * @param size          Maximum size of buffer
 * @return              True if filename was set, false otherwise.
*/
stock bool AutoExecConfig_GetFile(char[] buffer,int size)
{
	if (strlen(g_sConfigFile) > 0)
	{
		strcopy(buffer, size, g_sConfigFile);
		
		return true;
	}
	
	// Security for decl users
	buffer[0] = '\0';
	
	return false;
}






/**
 * Creates a convar and appends it to the autoconfigfile if not found.
 * FCVAR_DONTRECORD will be skipped.
 *
 * @param name                Name of new convar.
 * @param defaultValue        String containing the default value of new convar.
 * @param description         Optional description of the convar.
 * @param flags               Optional bitstring of flags determining how the convar should be handled. See FCVAR_* constants for more details.
 * @param hasMin              Optional boolean that determines if the convar has a minimum value.
 * @param min                 Minimum floating point value that the convar can have if hasMin is true.
 * @param hasMax              Optional boolean that determines if the convar has a maximum value.
 * @param max                 Maximum floating point value that the convar can have if hasMax is true.
 * @return                    A handle to the newly created convar. If the convar already exists, a handle to it will still be returned.
 * @error                     Convar name is blank or is the same as an existing console command.
*/
stock ConVar AutoExecConfig_CreateConVar(const char[] name, const char[] defaultValue, const char[] description="", int flags=0, bool hasMin=false, float min=0.0, bool hasMax=false, float max=0.0)
{
	// If configfile was set and convar has no dontrecord flag
	if (!(flags & FCVAR_DONTRECORD) && strlen(g_sConfigFile) > 0)
	{
		// Reset the results
		g_iLastFindResult = -1;
		g_iLastAppendResult = -1;
		
		
		// Add it if not found
		char buffer[64];
		
		g_iLastFindResult = AutoExecConfig_FindValue(name, buffer, sizeof(buffer), true);
		
		// We only add this convar if it doesn't exist, or the file doesn't exist and it should be auto-generated
		if (g_iLastFindResult == AUTOEXEC_FIND_NOT_FOUND || (g_iLastFindResult == AUTOEXEC_FIND_FILE_NOT_FOUND && g_bCreateFile))
		{
			g_iLastAppendResult = AutoExecConfig_AppendValue(name, defaultValue, description, flags, hasMin, min, hasMax, max);
		}
	}
	
	
	// Create the convar
	return CreateConVar(name, defaultValue, description, flags, hasMin, min, hasMax, max);
}




/**
 * Executes the autoconfigfile and adds it to the OnConfigsExecuted forward.
 * If we didn't create it ourselves we let SourceMod create it.
 *
 * @noreturn
*/
stock void AutoExecConfig_ExecuteFile()
{
	// Only let sourcemod create the file, if we didn't do that already. 
	AutoExecConfig(!g_bCreateFile, g_sRawFileName, g_sFolderPath);
}





/**
 * Formats a autoconfigfile, prefixes path and adds .cfg extension if missing.
 *
 * @param buffer        String to format.
 * @param size          Maximum size of buffer.
 * @return              Returns one of the AUTOEXEC_FORMAT values..
*/
stock static int AutoExecConfig_FormatFileName(char[] buffer, int size, char[] folder="sourcemod")
{
	// No config set
	if (strlen(g_sConfigFile) < 1)
	{
		return AUTOEXEC_NO_CONFIG;
	}
	
	
	// Can't be an cfgfile
	if (StrContains(g_sConfigFile, ".cfg") != -1 && strlen(g_sConfigFile) < 4)
	{
		return AUTOEXEC_FORMAT_BAD_FILENAME;
	}
	
	
	// Pathprefix
	char pathprefixbuffer[PLATFORM_MAX_PATH];
	if (strlen(folder) > 0)
	{
		Format(pathprefixbuffer, sizeof(pathprefixbuffer), "cfg/%s/", folder);
		
		if (g_bCreateDirectory && !DirExists(pathprefixbuffer))
		{
			CreateDirectory(pathprefixbuffer, g_bCreateDirectoryMode);
		}
	}
	else
	{
		Format(pathprefixbuffer, sizeof(pathprefixbuffer), "cfg/");
	}
	
	
	char filebuffer[PLATFORM_MAX_PATH];
	filebuffer[0] = '\0';
	
	// Add path if file doesn't begin with it
	if (StrContains(buffer, pathprefixbuffer) != 0)
	{
		StrCat(filebuffer, sizeof(filebuffer), pathprefixbuffer);
	}
	
	StrCat(filebuffer, sizeof(filebuffer), g_sConfigFile);
	
	
	// Add .cfg extension if file doesn't end with it
	if (StrContains(filebuffer[strlen(filebuffer) - 4], ".cfg") != 0)
	{
		StrCat(filebuffer, sizeof(filebuffer), ".cfg");
	}
	
	strcopy(buffer, size, filebuffer);
	
	return AUTOEXEC_FORMAT_SUCCESS;
}






/**
 * Appends a convar to the global autoconfigfile
 *
 * @param name                Name of new convar.
 * @param defaultValue        String containing the default value of new convar.
 * @param description         Optional description of the convar.
 * @param flags               Optional bitstring of flags determining how the convar should be handled. See FCVAR_* constants for more details.
 * @param hasMin              Optional boolean that determines if the convar has a minimum value.
 * @param min                 Minimum floating point value that the convar can have if hasMin is true.
 * @param hasMax              Optional boolean that determines if the convar has a maximum value.
 * @param max                 Maximum floating point value that the convar can have if hasMax is true.
 * @return                    Returns one of the AUTOEXEC_APPEND values
*/
stock int AutoExecConfig_AppendValue(const char[] name, const char[] defaultValue, const char[] description, int flags, bool hasMin, float min, bool hasMax, float max)
{
	// No config set
	if (strlen(g_sConfigFile) < 1)
	{
		return AUTOEXEC_NO_CONFIG;
	}
	
	
	char filebuffer[PLATFORM_MAX_PATH];
	strcopy(filebuffer, sizeof(filebuffer), g_sConfigFile);
	
	
	//PrintToServer("pathbuffer: %s", filebuffer);
	
	bool bFileExists = FileExists(filebuffer);
	
	if (g_bCreateFile || bFileExists)
	{
		// If the file already exists we open it in append mode, otherwise we use a write mode which creates the file
		File fFile = OpenFile(filebuffer, (bFileExists ? "a" : "w"));
		char writebuffer[2048];
		
		
		if (fFile == null)
		{
			return AUTOEXEC_APPEND_BAD_HANDLE;
		}
		
		// We just created the file, so add some header about version and stuff
		if (g_bCreateFile && !bFileExists)
		{
			fFile.WriteLine( "// This file was auto-generated by AutoExecConfig v%s (%s)", AUTOEXECCONFIG_VERSION, AUTOEXECCONFIG_URL);
			
			GetPluginFilename(g_hPluginHandle, writebuffer, sizeof(writebuffer));
			Format(writebuffer, sizeof(writebuffer), "// ConVars for plugin \"%s\"", writebuffer);
			fFile.WriteLine(writebuffer);
		}
		
		// Spacer
		fFile.WriteLine("\n");
		

		// This is used for multiline comments
		int newlines = GetCharCountInStr('\n', description);
		if (newlines == 0)
		{
			// We have no newlines, we can write the description to the file as is
			Format(writebuffer, sizeof(writebuffer), "// %s", description);
			fFile.WriteLine(writebuffer);
		}
		else
		{
			char[][] newlineBuf = new char[newlines +1][2048];
			ExplodeString(description, "\n", newlineBuf, newlines +1, 2048, false);
			
			// Each newline gets a commented newline
			for (int i; i <= newlines; i++)
			{
				if (strlen(newlineBuf[i]) > 0)
				{
					fFile.WriteLine("// %s", newlineBuf[i]);
				}
			}
		}
		
		
		// Descspacer
		fFile.WriteLine("// -");
		
		
		// Default
		Format(writebuffer, sizeof(writebuffer), "// Default: \"%s\"", defaultValue);
		fFile.WriteLine(writebuffer);
		
		
		// Minimum
		if (hasMin)
		{		
			Format(writebuffer, sizeof(writebuffer), "// Minimum: \"%f\"", min);
			fFile.WriteLine(writebuffer);
		}
		
		
		// Maximum
		if (hasMax)
		{
			Format(writebuffer, sizeof(writebuffer), "// Maximum: \"%f\"", max);
			fFile.WriteLine(writebuffer);
		}
		
		
		// Write end and defaultvalue
		Format(writebuffer, sizeof(writebuffer), "%s \"%s\"", name, defaultValue);
		fFile.WriteLine(writebuffer);
		
		
		fFile.Close();		
		
		return AUTOEXEC_APPEND_SUCCESS;
	}
	
	return AUTOEXEC_APPEND_FILE_NOT_FOUND;
}






/**
 * Returns a convar's value from the global autoconfigfile
 *
 * @param cvar                 Cvar to search for.
 * @param value                Buffer to store result into.
 * @param size                 Maximum size of buffer.
 * @param caseSensitive        Whether or not the search should be case sensitive.
 * @return                     Returns one of the AUTOEXEC_FIND values
*/
stock int AutoExecConfig_FindValue(const char[] cvar, char[] value, int size, bool caseSensitive=false)
{
	// Security for decl users
	value[0] = '\0';
	
	
	// No config set
	if (strlen(g_sConfigFile) < 1)
	{
		return AUTOEXEC_NO_CONFIG;
	}
	
	
	char filebuffer[PLATFORM_MAX_PATH];
	strcopy(filebuffer, sizeof(filebuffer), g_sConfigFile);
	
	
	
	//PrintToServer("pathbuffer: %s", filebuffer);
	
	bool bFileExists = FileExists(filebuffer);
	
	// We want to create the config file and it doesn't exist yet.
	if (g_bCreateFile && !bFileExists)
	{
		return AUTOEXEC_FIND_FILE_NOT_FOUND;
	}
	
	
	if (bFileExists)
	{
		File fFile = OpenFile(filebuffer, "r");
		int valuestart;
		int valueend;
		int cvarend;
		
		// Just an reminder to self, leave the values that high
		char sConvar[64];
		char sValue[64];
		char readbuffer[2048];
		char copybuffer[2048];
		
		if (fFile == null)
		{
			return AUTOEXEC_FIND_BAD_HANDLE;
		}
		
		
		while (!fFile.EndOfFile() && fFile.ReadLine(readbuffer, sizeof(readbuffer)))
		{
			// Is a comment or not valid
			if (IsCharSpace(readbuffer[0]) || readbuffer[0] == '/' || (!IsCharNumeric(readbuffer[0]) && !IsCharAlpha(readbuffer[0])) )
			{
				continue;
			}
			
			
			// Has not enough spaces, must have at least 1
			if (GetCharCountInStr(' ', readbuffer) < 1)
			{
				continue;
			}

			
			// Ignore cvars which aren't quoted
			if (GetCharCountInStr('"', readbuffer) != 2)
			{
				continue;
			}
			
			
			
			// Get the start of the value
			if ( (valuestart = StrContains(readbuffer, "\"")) == -1 )
			{
				continue;
			}
			
			
			// Get the end of the value
			if ( (valueend = StrContains(readbuffer[valuestart+1], "\"")) == -1 )
			{
				continue;
			}
			
			
			// Get the start of the cvar, 
			if ( (cvarend = StrContains(readbuffer, " ")) == -1 || cvarend >= valuestart)
			{
				continue;
			}
			
			
			// Skip if cvarendindex is before valuestartindex
			if (cvarend >= valuestart)
			{
				continue;
			}
			
			
			// Convar
			// Tempcopy for security
			strcopy(copybuffer, sizeof(copybuffer), readbuffer);
			copybuffer[cvarend] = '\0';
			
			strcopy(sConvar, sizeof(sConvar), copybuffer);
			

			// Value
			// Tempcopy for security
			strcopy(copybuffer, sizeof(copybuffer), readbuffer[valuestart+1]);
			copybuffer[valueend] = '\0';
			
			strcopy(sValue, sizeof(sValue), copybuffer);
			
			
			//PrintToServer("Cvar %s has a value of %s", sConvar, sValue);
			
			if (StrEqual(sConvar, cvar, caseSensitive))
			{
				Format(value, size, "%s", sConvar);
				
				fFile.Close();
				return AUTOEXEC_FIND_SUCCESS;
			}					
		}
		
		fFile.Close();	
		return AUTOEXEC_FIND_NOT_FOUND;
	}
	
	
	return AUTOEXEC_FIND_FILE_NOT_FOUND;
}






/**
 * Cleans the global autoconfigfile from too much spaces
 *
 * @return        One of the AUTOEXEC_CLEAN values.
*/
stock int AutoExecConfig_CleanFile()
{
	// No config set
	if (strlen(g_sConfigFile) < 1)
	{
		return AUTOEXEC_NO_CONFIG;
	}
	
	
	char sfile[PLATFORM_MAX_PATH];
	strcopy(sfile, sizeof(sfile), g_sConfigFile);
	
	
	// Security
	if (!FileExists(sfile))
	{
		return AUTOEXEC_CLEAN_FILE_NOT_FOUND;
	}
	
	
	
	char sfile2[PLATFORM_MAX_PATH];
	Format(sfile2, sizeof(sfile2), "%s_tempcopy", sfile);
	
	
	char readbuffer[2048];
	int count;
	bool firstreached;
	
	
	// Open files
	File fFile1 = OpenFile(sfile, "r");
	File fFile2 = OpenFile(sfile2, "w");
	
	
	
	// Check filehandles
	if (fFile1 == null || fFile2 == null)
	{
		if (fFile1 != null)
		{
			//PrintToServer("Handle1 invalid");
			fFile1.Close();
		}
		
		if (fFile2 != null)
		{
			//PrintToServer("Handle2 invalid");
			fFile2.Close();
		}
		
		return AUTOEXEC_CLEAN_BAD_HANDLE;
	}
	
	
	
	while (!fFile1.EndOfFile() && fFile1.ReadLine(readbuffer, sizeof(readbuffer)))
	{
		// Is space
		if (IsCharSpace(readbuffer[0]))
		{
			count++;
		}
		// No space, count from start
		else
		{
			count = 0;
		}
		
		
		// Don't write more than 1 space if seperation after informations have been reached
		if (count < 2 || !firstreached)
		{
			ReplaceString(readbuffer, sizeof(readbuffer), "\n", "");
			fFile2.WriteLine(readbuffer);
		}
		
		
		// First bigger seperation after informations has been reached
		if (count == 2)
		{
			firstreached = true;
		}
	}
	
	
	fFile1.Close();
	fFile2.Close();
	
	
	// This might be a risk, for now it works
	DeleteFile(sfile);
	RenameFile(sfile, sfile2);
	
	return AUTOEXEC_CLEAN_SUCCESS;
}






/**
 * Returns how many times the given char occures in the given string.
 *
 * @param str        String to search for in.
 * @return           Occurences of the given char found in string.
*/
stock static int GetCharCountInStr(int character, const char[] str)
{
	int len = strlen(str);
	int count;
	
	for (int i; i < len; i++)
	{
		if (str[i] == character)
		{
			count++;
		}
	}
	
	return count;
}






#pragma deprecated
stock bool AutoExecConfig_CacheConvars()
{
	return false;
}