
#region ================== Copyright (c) 2007 Pascal vd Heiden

/*
 * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com
 * This program is released under GNU General Public License
 * 
 * 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.
 * 
 */

#endregion

#region ================== CFG file structure syntax

/*
' ====================================================================================
'    CONFIGURATION FILE STRUCTURE SYNTAX
' ====================================================================================
'
'    Whitepace is always allowed. This includes spaces, tabs
'    linefeeds (10) and carriage returns (13)
'
'    Keys may not have spaces or assignment operator = in them.
'
'    Comments start with // (unless used within strings)
'    and count as comment for the rest of the line. Or use /* and */				/*
'    to mark the beginning and end of a comment.
'
'    Simple setting:
'
'              key = value;
'
'    Example:  speed = 345;
'              cars = 8;
'
'    Strings must be in quotes.
'
'    Example:  nickname = "Gherkin";
'              altnick = "Gherk inn";
'
'    String Escape Sequences:
'        \n    New line (10)
'        \r    Carriage return (13)
'        \t    Tab (9)
'        \"    Double-quotation mark
'        \\    Backslash
'        \000  Any ASCII character (MUST be 3 digits! So for 13 you use \013)
'
'    Decimals ALWAYS use a dot, NEVER comma!
'
'    Example:  pressure = 15.29;
'              acceleration = 1.0023;
'
'    true, false and null are valid keywords.
'    null values can be left out.
'
'    In this example, both items are null.
'    Example:  myitem = null;
'              myotheritem;
'
'    Structures must use brackets.
'
'    Structure Example:
'
'              key
'              {
'                   key = value;
'                   key = value;
'
'                   key
'                   {
'                        key = value;
'                        key = value;
'                        key = value;
'                   }
'
'                   key = value;
'                   key = value;
'                   key = value;
'                   key = value;
'                   key = value;
'              }
'
'    As you can see, structures inside structures are allowed
'    and you may go as deep as you want. Note that only the root structures
'    can be readed from config using ReadSetting. ReadSetting will return a
'    Dictionary object containing everything in that root structure.
'
'    Key names must be unique within their scope.
'
'    This is NOT allowed, it may not have 'father' more
'    than once in the same scope:
'
'              mother = 45;
'              father = 52;
'
'              father
'              {
'                   length = 1.87;
'              }
'
'    This however is allowed, because father
'    now exists in a different scope:
'
'              mother = 45;
'              father = 52;
'
'              parents
'              {
'                   father = 52;
'              }
'
'    This too is allowed, both 'age' are in a different scope:
'
'              mother
'              {
'                   age = 45;
'              }
'
'              father
'              {
'                   age = 52;
'              }
*/

#endregion

#region ================== Namespaces

using System;
using System.IO;
using System.Text;
using System.Globalization;
using System.Collections;
using System.Collections.Specialized;

#endregion

namespace CodeImp.DoomBuilder.IO
{
	public sealed class Configuration
	{
		#region ================== Constants
		
		// Path seperator
		public const string DEFAULT_SEPERATOR = ".";
		
		// Error strings
		private const string ERROR_KEYMISSING = "Missing key name in assignment or scope.";
		private const string ERROR_KEYSPACES = "Spaces not allowed in key names.";
		private const string ERROR_ASSIGNINVALID = "Invalid assignment. Missing a previous terminator symbol?";
		private const string ERROR_VALUEINVALID = "Invalid value in assignment. Missing a previous terminator symbol?";
		private const string ERROR_VALUETOOBIG = "Value too big.";
		private const string ERROR_KEYNOTUNQIUE = "Key is not unique within scope.";
		private const string ERROR_KEYWORDUNKNOWN = "Unknown keyword in assignment. Missing a previous terminator symbol?";
		private const string ERROR_UNEXPECTED_END = "Unexpected end of data. Missing a previous terminator symbol?";
		private const string ERROR_UNKNOWN_FUNCTION = "Unknown function call.";
		private const string ERROR_INVALID_ARGS = "Invalid function arguments.";
		private const string ERROR_INCLUDE_UNSUPPORTED = "Include function is not supported in data parsed from stream.";
		
		#endregion
		
		#region ================== Variables
		
		// Error result
		private bool cpErrorResult = false;
		private string cpErrorDescription = "";
		private int cpErrorLine = 0;
		private string cpErrorFile = "";
		
		// Configuration root
		private IDictionary root = null;
		
		#endregion
		
		#region ================== Properties
		
		// Properties
		public bool ErrorResult { get { return cpErrorResult; } }
		public string ErrorDescription { get { return cpErrorDescription; } }
		public int ErrorLine { get { return cpErrorLine; } }
		public string ErrorFile { get { return cpErrorFile; } }
		public IDictionary Root { get { return root; } set { root = value; } }
		public bool Sorted { get { return (root is ListDictionary); } }
		
		#endregion
		
		#region ================== Constructor / Destructor
		
		// Constructor
		public Configuration()
		{
			// Standard new configuration
			NewConfiguration();
			
			// We have no destructor
			GC.SuppressFinalize(this);
		}
		
		// Constructor
		public Configuration(bool sorted)
		{
			// Standard new configuration
			NewConfiguration(sorted);

			// We have no destructor
			GC.SuppressFinalize(this);
		}
		
		// Constructor to load a file immediately
		public Configuration(string filename)
		{
			// Load configuration from file
			LoadConfiguration(filename);

			// We have no destructor
			GC.SuppressFinalize(this);
		}
		
		// Constructor to load a file immediately
		public Configuration(string filename, bool sorted)
		{
			// Load configuration from file
			LoadConfiguration(filename, sorted);

			// We have no destructor
			GC.SuppressFinalize(this);
		}
		
		#endregion
		
		#region ================== Private Methods
		
		// This merges two structures
		private static IDictionary Combine(IDictionary d1, IDictionary d2, bool sorted)
		{
			// Create new dictionary
			IDictionary result;
			if(sorted) result = new ListDictionary(); else result = new Hashtable();
			
			// Copy all items from d1 to result
			IDictionaryEnumerator d1e = d1.GetEnumerator();
			while(d1e.MoveNext()) result.Add(d1e.Key, d1e.Value);
			
			// Go for all items in d2
			IDictionaryEnumerator d2e = d2.GetEnumerator();
			while(d2e.MoveNext())
			{
				// Check if this is another Hashtable
				if(d2e.Value is IDictionary)
				{
					// Check if already in result
					if(result.Contains(d2e.Key) && (result[d2e.Key] is IDictionary))
					{
						// Modify result
						result[d2e.Key] = Combine((IDictionary)result[d2e.Key], (IDictionary)d2e.Value, sorted);
					}
					else
					{
						// Copy from d2
						if(sorted)
						{
							// Sorted combine
							result[d2e.Key] = Combine(new ListDictionary(), (IDictionary)d2e.Value, sorted);
						}
						else
						{
							// Unsorted combine
							result[d2e.Key] = Combine(new Hashtable(), (IDictionary)d2e.Value, sorted);
						}
					}
				}
				else
				{
					// Check if also in d1
					if(result.Contains(d2e.Key))
					{
						// Modify result
						result[d2e.Key] = d2e.Value;
					}
					else
					{
						// Copy
						result.Add(d2e.Key, d2e.Value);
					}
				}
			}
			
			// Return result
			return result;
		}
		
		// This is called by all the ReadSetting overloads to perform the read
		private bool CheckSetting(IDictionary dic, string setting, string pathseperator)
		{
			IDictionary cs = null;

			// Split the path in an array
			string[] keys = setting.Split(pathseperator.ToCharArray());

			// Get the root item
			object item = dic;

			// Go for each item
			for(int i = 0; i < keys.Length; i++)
			{
				// Check if the current item is of ConfigStruct type
				if(item is IDictionary)
				{
					// Check if the key is valid
					if(ValidateKey(null, keys[i].Trim(), "", -1) == true)
					{
						// Cast to ConfigStruct
						cs = (IDictionary)item;
						
						// Check if the requested item exists
						if(cs.Contains(keys[i]) == true)
						{
							// Set the item to the next item
							item = cs[keys[i]];
						}
						else
						{
							// Key not found
							return false;
						}
					}
					else
					{
						// Invalid key in path
						return false;
					}
				}
				else
				{
					// Unable to go any further
					return false;
				}
			}

			// Return result
			return true;
		}
		
		// This is called by all the ReadSetting overloads to perform the read
		private object ReadAnySetting(string setting, object defaultsetting, string pathseperator) { return ReadAnySetting(root, setting, defaultsetting, pathseperator); }
		private object ReadAnySetting(IDictionary dic, string setting, object defaultsetting, string pathseperator) { return ReadAnySetting(dic, "", -1, setting, defaultsetting, pathseperator); }
		private object ReadAnySetting(IDictionary dic, string file, int line, string setting, object defaultsetting, string pathseperator)
		{
			IDictionary cs = null;
			
			// Split the path in an array
			string[] keys = setting.Split(pathseperator.ToCharArray());
			
			// Get the root item
			object item = dic;
			
			// Go for each item
			for(int i = 0; i < keys.Length; i++)
			{
				// Check if the current item is of ConfigStruct type
				if(item is IDictionary)
				{
					// Check if the key is valid
					if(ValidateKey(null, keys[i].Trim(), file, line) == true)
					{
						// Cast to ConfigStruct
						cs = (IDictionary)item;
						
						// Check if the requested item exists
						if(cs.Contains(keys[i]) == true)
						{
							// Set the item to the next item
							item = cs[keys[i]];
						}
						else
						{
							// Key not found
							// return default setting
							return defaultsetting;
						}
					}
					else
					{
						// Invalid key in path
						// return default setting
						return defaultsetting;
					}
				}
				else
				{
					// Unable to go any further
					// return default setting
					return defaultsetting;
				}
			}
			
			// Return the item
			return item;
		}
		
		
		// This returns a string added with escape characters
		private string EscapedString(string str)
		{
			// Replace the \ with \\ first!
			str = str.Replace("\\", "\\\\");
			str = str.Replace("\n", "\\n");
			str = str.Replace("\r", "\\r");
			str = str.Replace("\t", "\\t");
			str = str.Replace("\"", "\\\"");
			
			// Return result
			return str;
		}
		
		
		// This raises an error
		private void RaiseError(string file, int line, string description)
		{
			// Raise error
			if(!cpErrorResult)
			{
				cpErrorResult = true;
				cpErrorDescription = description;
				cpErrorLine = line;
				cpErrorFile = file;
			}
		}
		
		
		// This validates a given key and sets
		// error properties if key is invalid and errorline > -1
		private bool ValidateKey(IDictionary container, string key, string file, int errorline)
		{
			bool validateresult;
			
			// Check if key is an empty string
			if(key == "")
			{
				// ERROR: Missing key name in statement
				if(errorline > -1) RaiseError(file, errorline, ERROR_KEYMISSING);
				validateresult = false;
			}
			else
			{
				// Check if there are spaces in the key
				if(key.IndexOfAny(" ".ToCharArray()) > -1)
				{
					// ERROR: Spaces not allowed in key names
					if(errorline > -1) RaiseError(file, errorline, ERROR_KEYSPACES);
					validateresult = false;
				}
				else
				{
					// Check if we can test existance
					if(container != null)
					{
						/*
						// Test if the key exists in this container
						if(container.Contains(key) == true)
						{
							// ERROR: Key is not unique within struct
							if(errorline > -1) RaiseError(file, errorline, ERROR_KEYNOTUNQIUE);
							validateresult = false;
						}
						else
						*/
						{
							// Key OK
							validateresult = true;
						}
					}
					else
					{
						// Key OK
						validateresult = true;
					}
				}
			}
			
			// Return result
			return validateresult;
		}
		
		
		// This validates a given keyword and sets
		// error properties if keyword is invalid and errorline > -1
		private bool ValidateKeyword(string keyword, string file, int errorline)
		{
			bool validateresult;
			
			// Check if key is an empty string
			if(keyword == "")
			{
				// ERROR: Missing key name in statement
				if(errorline > -1) RaiseError(file, errorline, ERROR_ASSIGNINVALID);
				validateresult = false;
			}
			else
			{
				// Check if there are spaces in the key
				if(keyword.IndexOfAny(" ".ToCharArray()) > -1)
				{
					// ERROR: Spaces not allowed in key names
					if(errorline > -1) RaiseError(file, errorline, ERROR_ASSIGNINVALID);
					validateresult = false;
				}
				else
				{
					// Key OK
					validateresult = true;
				}
			}
			
			// Return result
			return validateresult;
		}
		
		#endregion
		
		#region ================== Parsing
		
		// This parses an assignment
		private object ParseAssignment(ref string file, ref string data, ref int pos, ref int line)
		{
			object val = null;
			
			while((pos < data.Length) && !cpErrorResult)
			{
				// Get current character
				char c = data[pos++];
				
				// Check for string opening
				if(c == '\"')
				{
					// Now parsing a string
					val = ParseString(ref file, ref data, ref pos, ref line);
					if(cpErrorResult) return null;
				}
				// Check for numeric character
				else if("0123456789-.&".IndexOf(c.ToString(CultureInfo.InvariantCulture)) > -1)
				{
					// Go one byte back, because this
					// byte is part of the number!
					pos--;
					
					// Now parsing a number
					val = ParseNumber(ref file, ref data, ref pos, ref line);
					if(cpErrorResult) return null;
				}
				// Check for new line
				else if(c == '\n')
				{
					// Count the new line
					line++;
				}
				// Check if assignment ends
				else if(c == ';')
				{
					// End of assignment
					return val;
				}
				// Otherwise (if not whitespace) it is a keyword
				else if((c != ' ') && (c != '\t'))
				{
					// Go one byte back, because this
					// byte is part of the keyword!
					pos--;
					
					// Now parsing a keyword
					val = ParseKeyword(ref file, ref data, ref pos, ref line);
					if(cpErrorResult) return null;
				}
			}
			
			RaiseError(file, line, ERROR_UNEXPECTED_END);
			return null;
		}
		
		
		// This parses a string
		private string ParseString(ref string file, ref string data, ref int pos, ref int line)
		{
			string val = "";
			
			// In escape sequence?
			bool escape = false;
			
			while((pos < data.Length) && !cpErrorResult)
			{
				// Get current character
				char c = data[pos++];
				
				// Check if in an escape sequence
				if(escape)
				{
					// What character?
					switch(c)
					{
						case '\\': val += "\\"; break;
						case 'n': val += "\n"; break;
						case '\"': val += "\""; break;
						case 'r': val += "\r"; break;
						case 't': val += "\t"; break;
						default:

							// Is it a number?
							if("0123456789".IndexOf(c.ToString(CultureInfo.InvariantCulture)) > -1)
							{
								int vv = 0;
								char vc = '0';

								// Convert the next 3 characters to a number
								string v = data.Substring(pos, 3);
								try { vv = System.Convert.ToInt32(v.Trim(), CultureInfo.InvariantCulture); }
								catch(System.FormatException)
								{
									// ERROR: Invalid value in assignment
									RaiseError(file, line, ERROR_VALUEINVALID);
									return null;
								}

								// Convert the number to a char
								try { vc = System.Convert.ToChar(vv, CultureInfo.InvariantCulture); }
								catch(System.FormatException)
								{
									// ERROR: Invalid value in assignment
									RaiseError(file, line, ERROR_VALUEINVALID);
									return null;
								}

								// Add the char
								val += vc.ToString(CultureInfo.InvariantCulture);
							}
							else
							{
								// Add the character as it is
								val += c.ToString(CultureInfo.InvariantCulture);
							}

							// Leave switch
							break;
					}

					// End of escape sequence
					escape = false;
				}
				else
				{
					// Check for sequence start
					if(c == '\\')
					{
						// Next character is of escape sequence
						escape = true;
					}
					// Check if string ends
					else if(c == '\"')
					{
						return val;
					}
					// Check for new line
					else if(c == '\n')
					{
						// Count the new line
						line++;
					}
					// Everything else is just part of string
					else
					{
						// Add to value
						val += c.ToString(CultureInfo.InvariantCulture);
					}
				}
			}
			
			RaiseError(file, line, ERROR_UNEXPECTED_END);
			return null;
		}
		
		
		// Parsing a number
		private object ParseNumber(ref string file, ref string data, ref int pos, ref int line)
		{
			string val = "";
			
			while((pos < data.Length) && !cpErrorResult)
			{
				// Get current character
				char c = data[pos++];
				
				// Check if number ends here
				if((c == ';') || (c == ',') || (c == ')'))
				{
					// One byte back, because this
					// is also the end of the assignment
					pos--;
					
					// Floating point?
					if(val.IndexOf("f") > -1)
					{
						float fval = 0;
						
						// Convert to float (remove the f first)
						try { fval = System.Convert.ToSingle(val.Trim().Replace("f", ""), CultureInfo.InvariantCulture); }
						catch(System.FormatException)
						{
							// ERROR: Invalid value in assignment
							RaiseError(file, line, ERROR_VALUEINVALID);
							return null;
						}
						return fval;
					}
					else
					{
						int ival = 0;
						long lval = 0;
						
						// Convert to int
						try
						{
							// Convert to value
							ival = System.Convert.ToInt32(val.Trim(), CultureInfo.InvariantCulture);
							return ival;
						}
						catch(System.OverflowException)
						{
							// Too large for Int32, try Int64
							try
							{
								// Convert to value
								lval = System.Convert.ToInt64(val.Trim(), CultureInfo.InvariantCulture);
								return lval;
							}
							catch(System.OverflowException)
							{
								// Too large for Int64, return error
								RaiseError(file, line, ERROR_VALUETOOBIG);
								return null;
							}
							catch(System.FormatException)
							{
								// ERROR: Invalid value in assignment
								RaiseError(file, line, ERROR_VALUEINVALID);
								return null;
							}
						}
						catch(System.FormatException)
						{
							// ERROR: Invalid value in assignment
							RaiseError(file, line, ERROR_VALUEINVALID);
							return null;
						}
					}
				}
				// Check for new line
				else if(c == '\n')
				{
					// Count the new line
					line++;
				}
				// Everything else is part of the value
				else
				{
					val += c.ToString(CultureInfo.InvariantCulture);
				}
			}
			
			RaiseError(file, line, ERROR_UNEXPECTED_END);
			return null;
		}
		
		
		// Parsing a keyword
		private object ParseKeyword(ref string file, ref string data, ref int pos, ref int line)
		{
			string val = "";
			
			while((pos < data.Length) && !cpErrorResult)
			{
				// Get current character
				char c = data[pos++];
				
				// Check if keyword ends
				if((c == ';') || (c == ',') || (c == ')'))
				{
					// One byte back, because this
					// is also the end of the assignment
					pos--;
					
					// Validate the keyword
					if(ValidateKeyword(val.Trim(), file, line))
					{
						// Return result depending on the keyword
						switch(val.Trim().ToLowerInvariant())
						{
							case "true": return (bool)true;
							case "false": return (bool)false;
							case "null": return null;
							default: RaiseError(file, line, ERROR_KEYWORDUNKNOWN); return null;
						}
					}
				}
				// Check for new line
				else if(c == '\n')
				{
					// Count the new line
					line++;
				}
				// Everything else is just part of keyword
				else
				{
					// Add to value
					val += c.ToString(CultureInfo.InvariantCulture);
				}
			}
			
			RaiseError(file, line, ERROR_UNEXPECTED_END);
			return null;
		}
		
		
		// This includes another file
		private void FunctionInclude(IDictionary cs, ArrayList args, ref string file, int line)
		{
			string data;
			
			if(string.IsNullOrEmpty(file)) RaiseError(file, line, ERROR_INCLUDE_UNSUPPORTED);
			if(args.Count < 1) RaiseError(file, line, ERROR_INVALID_ARGS);
			if(!(args[0] is string)) RaiseError(file, line, ERROR_INVALID_ARGS + " Expected a string for argument 1.");
			if((args.Count > 1) && !(args[1] is string)) RaiseError(file, line, ERROR_INVALID_ARGS + " Expected a string for argument 2.");
			if(cpErrorResult) return;
			
			// Determine the full path of the file to include
			string includefile = Path.GetDirectoryName(file) + Path.DirectorySeparatorChar + args[0].ToString();
			
			try
			{
				// Load the file contents
				FileStream fstream = File.OpenRead(includefile);
				byte[] fbuffer = new byte[fstream.Length];
				fstream.Read(fbuffer, 0, fbuffer.Length);
				fstream.Close();
				
				// Convert byte array to string
				data = Encoding.UTF8.GetString(fbuffer);
			}
			catch(Exception e)
			{
				RaiseError(file, line, "Unable to include file '" + includefile + "'. " + e.GetType().Name + ": " + e.Message);
				return;
			}
			
			// Remove returns and tabs because the
			// parser only uses newline for new lines.
			data = data.Replace("\r", "");
			data = data.Replace("\t", "");
			
			// Parse the data
			IDictionary inc;
			if(cs is ListDictionary) inc = new ListDictionary(); else inc = new Hashtable();
			int npos = 0, nline = 1;
			InputStructure(inc, ref includefile, ref data, ref npos, ref nline);
			if(!cpErrorResult)
			{
				// Check if a path is given
				if((args.Count > 1) && !string.IsNullOrEmpty(args[1].ToString()))
				{
					IDictionary def;
					if(cs is ListDictionary) def = new ListDictionary(); else def = new Hashtable();
					if(CheckSetting(inc, args[1].ToString(), DEFAULT_SEPERATOR))
					{
						inc = (IDictionary)ReadAnySetting(inc, file, line, args[1].ToString(), def, DEFAULT_SEPERATOR);
					}
					else
					{
						RaiseError(file, line, "Include missing structure '" + args[1].ToString() + "' in file '" + includefile + "'");
						return;
					}
				}
				
				// Recursively merge the structures with the current structure
				IDictionary newcs = Combine(cs, inc, (cs is ListDictionary));
				cs.Clear();
				foreach(DictionaryEntry de in newcs) cs.Add(de.Key, de.Value);
			}
		}
		
		
		// This parses a function
		private void ParseFunction(IDictionary cs, ref string file, ref string data, ref int pos, ref int line, ref string functionname)
		{
			// We now parse arguments, separated by commas, until we reach the end of the function
			ArrayList args = new ArrayList();
			object val = null;
			while((pos < data.Length) && !cpErrorResult)
			{
				// Get current character
				char c = data[pos++];
				
				// Check for end of function
				if(c == ')')
				{
					// Check what function to run
					switch(functionname.Trim().ToLowerInvariant())
					{
						// Include another file in here
						case "include":
							FunctionInclude(cs, args, ref file, line);
							return;
							
						default:
							RaiseError(file, line, ERROR_UNKNOWN_FUNCTION);
							return;
					}
				}
				// Check for string opening
				else if(c == '\"')
				{
					// Now parsing a string
					val = ParseString(ref file, ref data, ref pos, ref line);
					if(cpErrorResult) return;
					args.Add(val);
				}
				// Check for numeric character
				else if("0123456789-.&".IndexOf(c.ToString(CultureInfo.InvariantCulture)) > -1)
				{
					// Go one byte back, because this
					// byte is part of the number!
					pos--;
					
					// Now parsing a number
					val = ParseNumber(ref file, ref data, ref pos, ref line);
					if(cpErrorResult) return;
					args.Add(val);
				}
				// Check for new line
				else if(c == '\n')
				{
					// Count the new line
					line++;
				}
				// Check if argument ends
				else if(c == ',')
				{
					// End of argument
				}
				// Otherwise (if not whitespace) it is a keyword
				else if((c != ' ') && (c != '\t'))
				{
					// Go one byte back, because this
					// byte is part of the keyword!
					pos--;
					
					// Now parsing a keyword
					val = ParseKeyword(ref file, ref data, ref pos, ref line);
					if(cpErrorResult) return;
					args.Add(val);
				}
			}
			
			RaiseError(file, line, ERROR_UNEXPECTED_END);
			return;
		}
		
		
		// This parses a structure in the given data starting
		// from the given pos and line and updates pos and line.
		private void InputStructure(IDictionary cs, ref string file, ref string data, ref int pos, ref int line)
		{
			string key = "";
			
			// Go through all of the data until
			// the end or until the struct closes
			// or when an arror occurred
			while ((pos < data.Length) && !cpErrorResult)
			{
				// Get current character
				char c = data[pos++];
				
				// Check what character this is
				switch(c)
				{
					case '{': // Begin of new struct
						
						// Validate key
						if(ValidateKey(cs, key.Trim(), file, line))
						{
							// Parse this struct and add it
							IDictionary cs2;
							if(cs is ListDictionary) cs2 = new ListDictionary(); else cs2 = new Hashtable();
							InputStructure(cs2, ref file, ref data, ref pos, ref line);
							if(cs.Contains(key.Trim()) && (cs[key.Trim()] is IDictionary))
								cs[key.Trim()] = Combine((IDictionary)cs[key.Trim()], cs2, (cs is ListDictionary));
							else
								cs[key.Trim()] = cs2;
							key = "";
						}
						break;
						
					case '}': // End of this struct
						
						// Stop parsing in this struct
						return;
						
					case '(': // Function
						ParseFunction(cs, ref file, ref data, ref pos, ref line, ref key);
						key = "";
						break;
						
					case '=': // Assignment
						
						// Validate key
						if(ValidateKey(cs, key.Trim(), file, line))
						{
							// Now parsing assignment
							object val = ParseAssignment(ref file, ref data, ref pos, ref line);
							if(!cpErrorResult)
							{
								cs[key.Trim()] = val;
								key = "";
							}
						}
						break;
						
					case ';': // Terminator
						
						// Validate key
						if(!string.IsNullOrEmpty(key))
						{
							if(ValidateKey(cs, key.Trim(), file, line))
							{
								// Add the key with null as value
								cs[key.Trim()] = null;
								key = "";
							}
						}
						break;
						
					case '\n': // New line
						
						// Count the line
						line++;
						
						// Add this to the key as a space.
						// Spaces are not allowed, but it will be trimmed
						// when its the first or last character.
						key += " ";
						break;
						
					case '\\': // Possible comment
					case '/':
						
						// Backtrack to use previous character also
						pos--;
						
						// Check for the line comment //
						if(data.Substring(pos, 2) == "//")
						{
							// Find the next line
							int np = data.IndexOf("\n", pos);
							
							// Next line found?
							if(np > -1)
							{
								// Count the line
								line++;
								
								// Skip everything on this line
								pos = np + 1;
							}
							else
							{
								// No end of line
								// Skip everything else
								pos = data.Length + 1;
							}
						}
						// Check for the block comment /* */
						else if(data.Substring(pos, 2) == "/*")
						{
							// Find the next closing block comment
							int np = data.IndexOf("*/", pos);
							
							// Closing block comment found?
							if(np > -1)
							{
								// Count the lines in the block comment
								string blockdata = data.Substring(pos, np - pos + 2);
								line += (blockdata.Split("\n".ToCharArray()).Length - 1);
								
								// Skip everything in this block
								pos = np + 2;
							}
							else
							{
								// No end of line
								// Skip everything else
								pos = data.Length + 1;
							}
						}
						else
						{
							// No whitespace
							pos++;
						}
						break;
						
					default: // Everything else
						
						// Add character to key
						key += c.ToString(CultureInfo.InvariantCulture);
						break;
				}
			}
		}
		
		#endregion

		#region ================== Writing

		// This will create a data structure from the given object
		private string OutputStructure(IDictionary cs, int level, string newline, bool whitespace)
		{
			string leveltabs = "";
			string spacing = "";
			StringBuilder db = new StringBuilder("");
			
			// Check if this ConfigStruct is not empty
			if(cs.Count > 0)
			{
				// Create whitespace
				if(whitespace)
				{
					for(int i = 0; i < level; i++) leveltabs += "\t";
					spacing = " ";
				}
				
				// Get enumerator
				IDictionaryEnumerator de = cs.GetEnumerator();
				
				// Go for each item
				for(int i = 0; i < cs.Count; i++)
				{
					// Go to next item
					de.MoveNext();
					
					// Check if the value is null
					if(de.Value == null)
					{
						// Output the keyword "null"
						//db.Append(leveltabs); db.Append(de.Key.ToString()); db.Append(spacing);
						//db.Append("="); db.Append(spacing); db.Append("null;"); db.Append(newline);
						
						// Output key only
						db.Append(leveltabs); db.Append(de.Key.ToString()); db.Append(";"); db.Append(newline);
					}
					// Check if the value if of ConfigStruct type
					else if(de.Value is IDictionary)
					{
						// Output recursive structure
						if(whitespace) { db.Append(leveltabs); db.Append(newline); }
						db.Append(leveltabs); db.Append(de.Key); db.Append(newline);
						db.Append(leveltabs); db.Append("{"); db.Append(newline);
						db.Append(OutputStructure((IDictionary)de.Value, level + 1, newline, whitespace));
						db.Append(leveltabs); db.Append("}"); db.Append(newline);
						if(whitespace) { db.Append(leveltabs); db.Append(newline); }
					}
					// Check if the value is of boolean type
					else if(de.Value is bool)
					{
						// Check value
						if((bool)de.Value == true)
						{
							// Output the keyword "true"
							db.Append(leveltabs); db.Append(de.Key.ToString()); db.Append(spacing);
							db.Append("="); db.Append(spacing); db.Append("true;"); db.Append(newline);
						}
						else
						{
							// Output the keyword "false"
							db.Append(leveltabs); db.Append(de.Key.ToString()); db.Append(spacing);
							db.Append("="); db.Append(spacing); db.Append("false;"); db.Append(newline);
						}
					}
					// Check if value is of float type
					else if(de.Value is float)
					{
						// Output the value with a postfixed f
						db.Append(leveltabs); db.Append(de.Key); db.Append(spacing); db.Append("=");
						db.Append(spacing); db.Append(String.Format(CultureInfo.InvariantCulture, "{0}", de.Value)); db.Append("f;"); db.Append(newline);
					}
					// Check if value is of other numeric type
					else if(de.Value.GetType().IsPrimitive)
					{
						// Output the value unquoted
						db.Append(leveltabs); db.Append(de.Key); db.Append(spacing); db.Append("=");
						db.Append(spacing); db.Append(String.Format(CultureInfo.InvariantCulture, "{0}", de.Value)); db.Append(";"); db.Append(newline);
					}
					else
					{
						// Output the value with quotes and escape characters
						db.Append(leveltabs); db.Append(de.Key); db.Append(spacing); db.Append("=");
						db.Append(spacing); db.Append("\""); db.Append(EscapedString(de.Value.ToString())); db.Append("\";"); db.Append(newline);
					}
				}
			}
			
			// Return the structure
			return db.ToString();
		}
		
		#endregion
		
		#region ================== Public Methods
		
		// This clears the last error
		public void ClearError()
		{
			// Clear error
			cpErrorResult = false;
			cpErrorDescription = "";
			cpErrorLine = 0;
			cpErrorFile = "";
		}
		
		
		// This creates a new configuration
		public void NewConfiguration() { NewConfiguration(false); }
		public void NewConfiguration(bool sorted)
		{
			// Create new configuration
			if(sorted) root = new ListDictionary(); else root = new Hashtable();
		}
		
		// This checks if a given setting exists (disregards type)
		public bool SettingExists(string setting) { return CheckSetting(root, setting, DEFAULT_SEPERATOR); }
		public bool SettingExists(string setting, string pathseperator) { return CheckSetting(root, setting, pathseperator); }
		
		// This can give a value of a key specified in a path form
		// also, this does not error when the setting does not exist,
		// but instead returns the given default value.
		public string ReadSetting(string setting, string defaultsetting) { object r = ReadAnySetting(setting, defaultsetting, DEFAULT_SEPERATOR); if(r != null) return r.ToString(); else return null; }
		public string ReadSetting(string setting, string defaultsetting, string pathseperator) { object r = ReadAnySetting(setting, defaultsetting, pathseperator); if(r != null) return r.ToString(); else return null; }
		public int ReadSetting(string setting, int defaultsetting) { return Convert.ToInt32(ReadAnySetting(setting, defaultsetting, DEFAULT_SEPERATOR), CultureInfo.InvariantCulture); }
		public int ReadSetting(string setting, int defaultsetting, string pathseperator) { return Convert.ToInt32(ReadAnySetting(setting, defaultsetting, pathseperator), CultureInfo.InvariantCulture); }
		public float ReadSetting(string setting, float defaultsetting) { return Convert.ToSingle(ReadAnySetting(setting, defaultsetting, DEFAULT_SEPERATOR), CultureInfo.InvariantCulture); }
		public float ReadSetting(string setting, float defaultsetting, string pathseperator) { return Convert.ToSingle(ReadAnySetting(setting, defaultsetting, pathseperator), CultureInfo.InvariantCulture); }
		public short ReadSetting(string setting, short defaultsetting) { return Convert.ToInt16(ReadAnySetting(setting, defaultsetting, DEFAULT_SEPERATOR), CultureInfo.InvariantCulture); }
		public short ReadSetting(string setting, short defaultsetting, string pathseperator) { return Convert.ToInt16(ReadAnySetting(setting, defaultsetting, pathseperator), CultureInfo.InvariantCulture); }
		public long ReadSetting(string setting, long defaultsetting) { return Convert.ToInt64(ReadAnySetting(setting, defaultsetting, DEFAULT_SEPERATOR), CultureInfo.InvariantCulture); }
		public long ReadSetting(string setting, long defaultsetting, string pathseperator) { return Convert.ToInt64(ReadAnySetting(setting, defaultsetting, pathseperator), CultureInfo.InvariantCulture); }
		public bool ReadSetting(string setting, bool defaultsetting) { return Convert.ToBoolean(ReadAnySetting(setting, defaultsetting, DEFAULT_SEPERATOR), CultureInfo.InvariantCulture); }
		public bool ReadSetting(string setting, bool defaultsetting, string pathseperator) { return Convert.ToBoolean(ReadAnySetting(setting, defaultsetting, pathseperator), CultureInfo.InvariantCulture); }
		public byte ReadSetting(string setting, byte defaultsetting) { return Convert.ToByte(ReadAnySetting(setting, defaultsetting, DEFAULT_SEPERATOR), CultureInfo.InvariantCulture); }
		public byte ReadSetting(string setting, byte defaultsetting, string pathseperator) { return Convert.ToByte(ReadAnySetting(setting, defaultsetting, pathseperator), CultureInfo.InvariantCulture); }
		public IDictionary ReadSetting(string setting, IDictionary defaultsetting) { return (IDictionary)ReadAnySetting(setting, defaultsetting, DEFAULT_SEPERATOR); }
		public IDictionary ReadSetting(string setting, IDictionary defaultsetting, string pathseperator) { return (IDictionary)ReadAnySetting(setting, defaultsetting, pathseperator); }

		public object ReadSettingObject(string setting, object defaultsetting) { return ReadAnySetting(setting, defaultsetting, DEFAULT_SEPERATOR); }
		
		
		// This writes a given setting to the configuration.
		// Wont change existing structs, but will add them as needed.
		// Returns true when written, false when failed.
		public bool WriteSetting(string setting, object settingvalue) { return WriteSetting(setting, settingvalue, DEFAULT_SEPERATOR); }
		public bool WriteSetting(string setting, object settingvalue, string pathseperator)
		{
			IDictionary cs = null;
			
			// Split the path in an array
			string[] keys = setting.Split(pathseperator.ToCharArray());
			string finalkey = keys[keys.Length - 1];
			
			// Get the root item
			object item = root;
			
			// Go for each path item
			for(int i = 0; i < (keys.Length - 1); i++)
			{
				// Check if the key is valid
				if(ValidateKey(null, keys[i].Trim(), "", -1) == true)
				{
					// Cast to ConfigStruct
					cs = (IDictionary)item;
					
					// Check if the requested item exists
					if(cs.Contains(keys[i]) == true)
					{
						// Check if the requested item is a ConfigStruct
						if(cs[keys[i]] is IDictionary)
						{
							// Set the item to the next item
							item = cs[keys[i]];
						}
						else
						{
							// Cant proceed with path
							return false;
						}
					}
					else
					{
						// Key not found
						// Create it now
						IDictionary ncs;
						if(root is ListDictionary) ncs = new ListDictionary(); else ncs = new Hashtable();
						cs.Add(keys[i], ncs);
						
						// Set the item to the next item
						item = cs[keys[i]];
					}
				}
				else
				{
					// Invalid key in path
					return false;
				}
			}
			
			// Cast to ConfigStruct
			cs = (IDictionary)item;
			
			// Check if the key already exists
			if(cs.Contains(finalkey) == true)
			{
				// Update the value
				cs[finalkey] = settingvalue;
			}
			else
			{
				// Create the key/value pair
				cs.Add(finalkey, settingvalue);
			}
			
			// Return success
			return true;
		}
		
		
		// This removes a given setting from the configuration.
		public bool DeleteSetting(string setting) { return DeleteSetting(setting, DEFAULT_SEPERATOR); }
		public bool DeleteSetting(string setting, string pathseperator)
		{
			IDictionary cs = null;
			
			// Split the path in an array
			string[] keys = setting.Split(pathseperator.ToCharArray());
			string finalkey = keys[keys.Length - 1];
			
			// Get the root item
			object item = root;
			
			// Go for each path item
			for(int i = 0; i < (keys.Length - 1); i++)
			{
				// Check if the key is valid
				if(ValidateKey(null, keys[i].Trim(), "", -1) == true)
				{
					// Cast to ConfigStruct
					cs = (IDictionary)item;
					
					// Check if the requested item exists
					if(cs.Contains(keys[i]) == true)
					{
						// Check if the requested item is a ConfigStruct
						if(cs[keys[i]] is IDictionary)
						{
							// Set the item to the next item
							item = cs[keys[i]];
						}
						else
						{
							// Cant proceed with path
							return false;
						}
					}
					else
					{
						// Key not found
						// Create it now
						IDictionary ncs;
						if(root is ListDictionary) ncs = new ListDictionary(); else ncs = new Hashtable();
						cs.Add(keys[i], ncs);
						
						// Set the item to the next item
						item = cs[keys[i]];
					}
				}
				else
				{
					// Invalid key in path
					return false;
				}
			}
			
			// Cast to ConfigStruct
			cs = (IDictionary)item;
			
			// Arrived at our destination
			// Delete the key if the key exists
			if(cs.Contains(finalkey) == true)
			{
				// Key exists, delete it
				cs.Remove(finalkey);
				
				// Return success
				return true;
			}
			else
			{
				// Key not found, return fail
				return false;
			}
		}
		
		
		// This will save the current configuration to the specified file
		public bool SaveConfiguration(string filename) { return SaveConfiguration(filename, "\r\n", true); }
		public bool SaveConfiguration(string filename, string newline) { return SaveConfiguration(filename, newline, true); }
		public bool SaveConfiguration(string filename, string newline, bool whitespace)
		{
			// Kill the file if it exists
			if(File.Exists(filename) == true) File.Delete(filename);
			
			// Open file stream for writing
			FileStream fstream = File.OpenWrite(filename);
			
			// Create output structure and write to file
			string data = OutputConfiguration(newline, whitespace);
			byte[] baData= Encoding.UTF8.GetBytes(data);
			fstream.Write(baData, 0, baData.Length);
			fstream.Flush();
			fstream.Close();
			
			// Return true when done, false when errors occurred
			return !cpErrorResult;
		}
		
		
		// This will output the current configuration as a string
		public string OutputConfiguration() { return OutputConfiguration("\r\n", true); }
		public string OutputConfiguration(string newline) { return OutputConfiguration(newline, true); }
		public string OutputConfiguration(string newline, bool whitespace)
		{
			// Simply return the configuration structure as string
			return OutputStructure(root, 0, newline, whitespace);
		}
		
		
		// This will load a configuration from file
		public bool LoadConfiguration(string filename) { return LoadConfiguration(filename, false); }
		public bool LoadConfiguration(string filename, bool sorted)
		{
			// Check if the file is missing
			if(File.Exists(filename) == false)
			{
				throw(new FileNotFoundException("File not found \"" + filename + "\"", filename));
			}
			else
			{
				// Load the file contents
				FileStream fstream = File.OpenRead(filename);
				byte[] fbuffer = new byte[fstream.Length];
				fstream.Read(fbuffer, 0, fbuffer.Length);
				fstream.Close();
				
				// Convert byte array to string
				string data = Encoding.UTF8.GetString(fbuffer);
				
				// Load the configuration from this data
				return InputConfiguration(filename, data, sorted);
			}
		}
		
		
		// This will load a configuration from string
		public bool InputConfiguration(string data) { return InputConfiguration(data, false); }
		public bool InputConfiguration(string data, bool sorted) { return InputConfiguration("", data, sorted); }
		private bool InputConfiguration(string file, string data, bool sorted)
		{
			// Remove returns and tabs because the
			// parser only uses newline for new lines.
			data = data.Replace("\r", "");
			data = data.Replace("\t", "");
			
			// Clear errors
			ClearError();
			
			// Parse the data to the root structure
			if(sorted) root = new ListDictionary(); else root = new Hashtable();
			int pos = 0, line = 1;
			InputStructure(root, ref file, ref data, ref pos, ref line);
			
			// Return true when done, false when errors occurred
			return !cpErrorResult;
		}
		
		#endregion
	}
}
