
#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 ================== Namespaces

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

#endregion

namespace CodeImp.DoomBuilder.IO
{
	public sealed class UniversalParser
	{
		#region ================== Constants
		
		// Path seperator
		//public const string DEFAULT_SEPERATOR = ".";
		
		// Allowed characters in a key
		private const string KEY_CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789_";
		
		// Parse mode constants
		private const int PM_NOTHING = 0;
		private const int PM_ASSIGNMENT = 1;
		private const int PM_NUMBER = 2;
		private const int PM_STRING = 3;
		private const int PM_KEYWORD = 4;
		
		// Error strings
		private const string ERROR_KEYMISSING = "Missing key name in assignment or scope.";
		private const string ERROR_KEYCHARACTERS = "Invalid characters in key name.";
		//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_KEYWITHOUTVALUE = "Key has no value assigned.";
		private const string ERROR_KEYWORDUNKNOWN = "Unknown keyword in assignment. Missing a previous terminator symbol?";
		
		#endregion
		
		#region ================== Variables
		
		// Error result
		private int cpErrorResult;
		private string cpErrorDescription = "";
		private int cpErrorLine;

		// Warnings
		private List<string> warnings = new List<string>();
		
		// Configuration root
		private UniversalCollection root;

		private const string newline = "\n";
		private StringBuilder key;  //mxd
		private StringBuilder val;  //mxd
		private Dictionary<string, UniversalEntry> matches; //mxd

		// Settings
		private bool strictchecking = true;
		
		#endregion
		
		#region ================== Properties
		
		// Properties
		public int ErrorResult { get { return cpErrorResult; } }
		public string ErrorDescription { get { return cpErrorDescription; } }
		public int ErrorLine { get { return cpErrorLine; } }
		public UniversalCollection Root { get { return root; } }
		public bool StrictChecking { get { return strictchecking; } set { strictchecking = value; } }
		public bool HasWarnings { get { return warnings.Count != 0; } }
		public List<string> Warnings { get { return warnings; } }
		
		#endregion
		
		#region ================== Constructor / Destructor
		
		// Constructor
		public UniversalParser()
		{
			// Standard new configuration
			NewConfiguration();
			
			// We have no destructor
			GC.SuppressFinalize(this);
		}
		
		// Constructor to load a file immediately
		public UniversalParser(string filename)
		{
			// Load configuration from file
			LoadConfiguration(filename);

			// We have no destructor
			GC.SuppressFinalize(this);
		}
		
		#endregion
		
		#region ================== Private Methods

		// This returns a string added with escape characters
		private static string EscapedString(string str)
		{
			// Replace the \ with \\ first!
			str = str.Replace("\\", "\\\\");
			str = str.Replace(newline, "\\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(int line, string description)
		{
			// Raise error
			cpErrorResult = 1;
			cpErrorDescription = description;
			cpErrorLine = line + 1; //mxd
		}
		
		
		// This validates a given key and sets
		// error properties if key is invalid and errorline > -1
		private bool ValidateKey(string key, int errorline)
		{
			// Check if key is an empty string
			if(key.Length == 0)
			{
				// ERROR: Missing key name in statement
				if(errorline > -1) RaiseError(errorline, ERROR_KEYMISSING);
				return false;
			}

			//Only when strict checking
			if(strictchecking) 
			{
				// Check if all characters are valid
				string keylc = key.ToLowerInvariant(); //mxd. UDMF key names are case-insensitive
				foreach(char c in keylc) 
				{
					if(KEY_CHARACTERS.IndexOf(c) == -1) 
					{
						// ERROR: Invalid characters in key name
						if(errorline > -1) RaiseError(errorline, ERROR_KEYCHARACTERS);
						return false;
					}
				}
			}
			
			// Key is valid
			return true;
		}
		
		
		// This parses a structure in the given data starting
		// from the given pos and line and updates pos and line.
		private UniversalCollection InputStructure(ref string[] data, ref int pos, ref int line, bool topLevel)
		{
			int pm = PM_NOTHING;			// current parse mode
			key.Remove(0, key.Length);
			val.Remove(0, val.Length);
			bool escape = false;			// escape sequence?
			bool endofstruct = false;		// true as soon as this level struct ends
			UniversalCollection cs = new UniversalCollection();
			
			// Go through all of the data until
			// the end or until the struct closes
			// or when an arror occurred
			while((cpErrorResult == 0) && (endofstruct == false))
			{
				// Get current character
				if (pos > data[line].Length - 1) 
				{
					pos = 0;
					line++;

					// Stop if we have reached the end of the data
					if (line == data.Length)
						break;

					if(string.IsNullOrEmpty(data[line])) continue; //mxd. Skip empty lines here so correct line number is displayed on errors
				}

				char c = data[line][pos]; // current data character
				
				// ================ What parse mode are we at?
				if(pm == PM_NOTHING)
				{
					// Now check what character this is
					switch(c)
					{
						case '{': // Begin of new struct
							// Validate key
							string s = key.ToString().Trim();
							if(ValidateKey(s, line))
							{
								// Next character
								pos++;
								
								// Parse this struct and add it
								cs.Add(new UniversalEntry(s.ToLowerInvariant(), InputStructure(ref data, ref pos, ref line, false)));
								
								// Check the last character
								pos--;
								
								// Reset the key
								key.Remove(0, key.Length);
							}
							
							// Leave switch
							break;
							
						case '}': // End of this struct
							// Stop parsing in this struct
							endofstruct = true;
							
							// Leave the loop
							break;
							
						case '=': // Assignment
							// Validate key
							if(ValidateKey(key.ToString().Trim(), line))
							{
								// Now parsing assignment
								pm = PM_ASSIGNMENT;
							}
							
							// Leave switch
							break;
							
						case ';': // Terminator
							
							// Validate key
							if(ValidateKey(key.ToString().Trim(), line))
							{
								// Error: No value
								RaiseError(line, ERROR_KEYWITHOUTVALUE);
							}
							
							// Leave switch
							break;
							
						case '\n': // New line
							// Count the line
							line++;
							pos = -1;
							
							// 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.Append(" ");
							
							// Leave switch
							break;
							
						case '\\': // Possible comment
						case '/':
							// Check for the line comment //
							if(data[line].Substring(pos, 2) == "//")
							{
								// Skip everything on this line
								pos = -1;
								
								// Have next line?
								if(line < data.Length - 1) line++;
							}
							// Check for the block comment /* */
							else if(data[line].Substring(pos, 2) == "/*")
							{
								// Block comment closes on the same line?.. (mxd)
								int np = data[line].IndexOf("*/", pos);
								if(np > -1)
								{
									pos = np + 1;
								}
								else
								{
									// Find the next closing block comment
									line++;
									while((np = data[line].IndexOf("*/", 0)) == -1) 
									{
										if(line == data.Length - 1) break;
										line++;
									}

									// Closing block comment found?
									if(np > -1) 
									{
										// Skip everything in this block
										pos = np + 1;
									}
								}
							}
							
							// Leave switch
							break;
							
						default: // Everything else
							// Add character to key
							if(pos != -1) key.Append(c);

							// Leave switch
							break;
					}
				}
				// ================ Parsing an assignment
				else if(pm == PM_ASSIGNMENT)
				{
					// Check for string opening
					if(c == '\"')
					{
						// Now parsing string
						pm = PM_STRING; //numbers
					}
					// Check for numeric character numbers
					else if(Configuration.NUMBERS2.IndexOf(c) > -1)
					{
						// Now parsing number
						pm = PM_NUMBER;
						
						// Go one byte back, because this
						// byte is part of the number!
						pos--;
					}
					// Check for new line
					else if(c == '\n')
					{
						// Count the new line
						line++;
					}
					// Check if assignment ends
					else if(c == ';')
					{
						// End of assignment
						pm = PM_NOTHING;
						
						// Remove this if it causes problems
						key.Remove(0, key.Length);
						val.Remove(0, val.Length);
					}
					// Otherwise (if not whitespace) it will be a keyword
					else if((c != ' ') && (c != '\t'))
					{
						// Now parsing a keyword
						pm = PM_KEYWORD;
						
						// Go one byte back, because this
						// byte is part of the keyword!
						pos--;
					}
				}
				// ================ Parsing a number
				else if(pm == PM_NUMBER)
				{
					// Check if number ends here
					if(c == ';')
					{
						// Hexadecimal?
						string s = val.ToString();
						if((s.Length > 2) && s.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
						{
							// Convert to int
							try
							{
								// Convert to value
								int ival = Convert.ToInt32(s.Substring(2).Trim(), 16);

								// Add it to struct
								UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), ival);
								cs.Add(entry);
							}
							catch(OverflowException)
							{
								// Too large for Int32, try Int64
								try
								{
									// Convert to value
									long lval = Convert.ToInt64(s.Substring(2).Trim(), 16);

									// Add it to struct
									UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), lval);
									cs.Add(entry);
								}
								catch(OverflowException)
								{
									// Too large for Int64, return error
									RaiseError(line, ERROR_VALUETOOBIG);
								}
								catch(FormatException)
								{
									// ERROR: Invalid value in assignment
									RaiseError(line, ERROR_VALUEINVALID + "\n\nUnrecognized token: \"" + s.Trim() + "\"");
								}
							}
							catch(FormatException)
							{
								// ERROR: Invalid value in assignment
								RaiseError(line, ERROR_VALUEINVALID + "\n\nUnrecognized token: \"" + s.Trim() + "\"");
							}
						}
						// Floating point?
						//mxd. Can be in scientific notation (like "1E-06")
						else if(s.IndexOf('.') > -1 || s.ToLowerInvariant().Contains("e-"))
						{
							double fval = 0;
							
							// Convert to float (remove the f first)
							try { fval = Convert.ToDouble(s.Trim(), CultureInfo.InvariantCulture); }
							catch(FormatException)
							{ 
								// ERROR: Invalid value in assignment
								RaiseError(line, ERROR_VALUEINVALID + "\n\nUnrecognized token: \"" + s.Trim() + "\"");
							}
							
							// Add it to struct
							UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), fval);
							cs.Add(entry);
						}
						else
						{
							// Convert to int
							try
							{
								// Convert to value
								int ival = Convert.ToInt32(s.Trim(), CultureInfo.InvariantCulture);
								
								// Add it to struct
								UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), ival);
								cs.Add(entry);
							}
							catch(OverflowException)
							{
								// Too large for Int32, try Int64
								try
								{
									// Convert to value
									long lval = Convert.ToInt64(s.Trim(), CultureInfo.InvariantCulture);
									
									// Add it to struct
									UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), lval);
									cs.Add(entry);
								}
								catch(OverflowException)
								{
									// Too large for Int64, return error
									RaiseError(line, ERROR_VALUETOOBIG);
								}
								catch(FormatException)
								{ 
									// ERROR: Invalid value in assignment
									RaiseError(line, ERROR_VALUEINVALID + "\n\nUnrecognized token: \"" + s.Trim() + "\"");
								}
							}
							catch(FormatException)
							{ 
								// ERROR: Invalid value in assignment
								RaiseError(line, ERROR_VALUEINVALID + "\n\nUnrecognized token: \"" + s.Trim() + "\"");
							}
						}
						
						// Reset key and value
						key.Remove(0, key.Length);
						val.Remove(0, val.Length);
						
						// End of assignment
						pm = PM_NOTHING;
					}
					// Check for new line
					else if(c == '\n')
					{
						// Count the new line
						line++;
						pos = -1;
					}
					// Everything else is part of the value
					else
					{
						val.Append(c);
					}
				}
				// ================ Parsing a string
				else if(pm == PM_STRING)
				{
					// Check if in an escape sequence
					if(escape)
					{
						// What character?
						switch(c)
						{
							case '\\': val.Append('\\'); break;
							case 'n': val.Append(newline); break;
							case '\"': val.Append('\"'); break;
							case 'r': val.Append('\r'); break;
							case 't': val.Append('\t'); break;
							default:
								// Is it a number?
								if(Configuration.NUMBERS.IndexOf(c) > -1)
								{
									int vv = 0;
									char vc = '0';
									
									// Convert the next 3 characters to a number
									string v = data[line].Substring(pos, 3);
									try { vv = Convert.ToInt32(v.Trim(), CultureInfo.InvariantCulture); }
									catch(FormatException)
									{ 
										// ERROR: Invalid value in assignment
										RaiseError(line, ERROR_VALUEINVALID + "\n\nUnrecognized token: \"" + v.Trim() + "\"");
									}
									
									// Convert the number to a char
									try { vc = Convert.ToChar(vv, CultureInfo.InvariantCulture); }
									catch(FormatException)
									{ 
										// ERROR: Invalid value in assignment
										RaiseError(line, ERROR_VALUEINVALID + "\n\nUnrecognized token: \"" + v.Trim() + "\"");
									}
									
									// Add the char
									val.Append(vc);
								}
								else
								{
									// Add the character as it is
									val.Append(c);
								}
								
								// 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 == '\"')
						{
							// Add string to struct
							UniversalEntry entry = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), val.ToString());
							cs.Add(entry);
							
							// End of assignment
							pm = PM_ASSIGNMENT;
							
							// Reset key and value
							key.Remove(0, key.Length);
							val.Remove(0, val.Length);
						}
						// Check for new line
						else if(c == '\n')
						{
							// Count the new line
							line++;
							pos = -1;
						}
						// Everything else is just part of string
						else
						{
							// Add to value
							val.Append(c);
						}
					}
				}
				// ================ Parsing a keyword
				else if(pm == PM_KEYWORD)
				{
					// Check if keyword ends
					if(c == ';')
					{
						// Add to the struct depending on the keyword
						switch(val.ToString().Trim().ToLowerInvariant())
						{
							case "true":
								// Add boolean true
								UniversalEntry t = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), true);
								cs.Add(t);
								break;
								
							case "false":
								// Add boolean false
								UniversalEntry f = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), false);
								cs.Add(f);
								break;

                            case "nan":
                                // Add float value
                                UniversalEntry nan = new UniversalEntry(key.ToString().Trim().ToLowerInvariant(), double.NaN);
								// Do not add NaN, just drop it with a warning
								// cs.Add(nan);
								warnings.Add("UDMF map data line " + (line+1) + ": value of field " + key.ToString().Trim().ToLowerInvariant() + " has a value of NaN (not a number). Field is being dropped permanently.");
                                break;

                            default:
								// Unknown keyword
								RaiseError(line, ERROR_KEYWORDUNKNOWN + "\n\nUnrecognized token: \"" + val.ToString().Trim() + "\"");
								break;
						}
						
						// End of assignment
						pm = PM_NOTHING;
						
						// Reset key and value
						key.Remove(0, key.Length);
						val.Remove(0, val.Length);
					}
					// Check for new line
					else if(c == '\n')
					{
						// Count the new line
						line++;
						pos = -1;
					}
					// Everything else is just part of keyword
					else
					{
						// Add to value
						val.Append(c);
					}
				}
				
				// Next character
				pos++;
			}
			
			// Return the parsed result
			return cs;
		}
		
		
		// This will create a data structure from the given object
		private string OutputStructure(UniversalCollection 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 = " ";
				}
				
				// Go for each item
				for(int i = 0; i < cs.Count; i++)
				{
					// Check if the value if of collection type
					if(cs[i].Value is UniversalCollection)
					{
						UniversalCollection c = (UniversalCollection)cs[i].Value;
						
						// Output recursive structure
						if(whitespace) { db.Append(leveltabs); db.Append(newline); }
						db.Append(leveltabs); db.Append(cs[i].Key);
						if(!string.IsNullOrEmpty(c.Comment))
						{
							if(whitespace) db.Append("\t");
							db.Append("// " + c.Comment);
						}
						db.Append(newline);
						db.Append(leveltabs); db.Append("{"); db.Append(newline);
						db.Append(OutputStructure(c, level + 1, newline, whitespace));
						db.Append(leveltabs); db.Append("}"); db.Append(newline);
						//if(whitespace) { db.Append(leveltabs); db.Append(newline); } //mxd. Let's save a few Kbs by using single line breaks...
					}
					// Check if the value is of boolean type
					else if(cs[i].Value is bool)
					{
						db.Append(leveltabs); db.Append(cs[i].Key); db.Append(spacing); db.Append("="); db.Append(spacing);
						db.Append((bool)cs[i].Value ? "true;" : "false;"); db.Append(newline);
					}
					// Check if value is of float type
					else if(cs[i].Value is float)
					{
						// Output the value as float (3 decimals)
						float f = (float)cs[i].Value;
						db.Append(leveltabs); db.Append(cs[i].Key); db.Append(spacing); db.Append("=");
						db.Append(spacing); db.Append(f.ToString("0.000", CultureInfo.InvariantCulture)); db.Append(";"); db.Append(newline);
					}
					//mxd. Check if value is of double type
					else if(cs[i].Value is double)
					{
						// Output the value as double (7 decimals)
						double d = (double)cs[i].Value;
						db.Append(leveltabs); db.Append(cs[i].Key); db.Append(spacing); db.Append("=");
						db.Append(spacing); db.Append(d.ToString("0.0##############", CultureInfo.InvariantCulture)); /* db.Append(d.ToString("0.0000000", CultureInfo.InvariantCulture)); */ db.Append(";"); db.Append(newline);
					}
					// Check if value is of other numeric type
					else if(cs[i].Value.GetType().IsPrimitive)
					{
						// Output the value unquoted
						db.Append(leveltabs); db.Append(cs[i].Key); db.Append(spacing); db.Append("=");
						db.Append(spacing); db.Append(String.Format(CultureInfo.InvariantCulture, "{0}", cs[i].Value)); db.Append(";"); db.Append(newline);
					}
					else
					{
						// Output the value with quotes and escape characters
						db.Append(leveltabs); db.Append(cs[i].Key); db.Append(spacing); db.Append("=");
						db.Append(spacing); db.Append("\""); db.Append(EscapedString(cs[i].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 = 0;
			cpErrorDescription = "";
			cpErrorLine = 0;
		}
		
		
		// This creates a new configuration
		public void NewConfiguration()
		{
			// Create new configuration
			root = new UniversalCollection();
		}
		
		
		// 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)) 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.ASCII.GetBytes(data);
			fstream.Write(baData, 0, baData.Length);
			fstream.Flush();
			fstream.Close();
			
			// Return true when done, false when errors occurred
			return cpErrorResult == 0;
		}
		
		
		// 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)
		{
			// Check if the file is missing
			if(!File.Exists(filename))
			{
				throw(new FileNotFoundException("File not found \"" + filename + "\"", filename));
			}
			else
			{
				// Load the file contents
				List<string> data = new List<string>(100);
				using(FileStream stream = File.OpenRead(filename)) 
				{
					StreamReader reader = new StreamReader(stream, Encoding.ASCII);
					
					while(!reader.EndOfStream) 
					{
						string line = reader.ReadLine();
						if(string.IsNullOrEmpty(line)) continue;
						data.Add(line);
					}
				}

				// Load the configuration from this data
				return InputConfiguration(data.ToArray());
			}
		}
		
		
		// This will load a configuration from string
		public bool InputConfiguration(string[] data)
		{
			// Clear errors
			ClearError();
			
			// Parse the data to the root structure
			int pos = 0;
			int line = 0; //mxd
			matches = new Dictionary<string, UniversalEntry>(StringComparer.Ordinal); //mxd
			key = new StringBuilder(16); //mxd
			val = new StringBuilder(16); //mxd
			root = InputStructure(ref data, ref pos, ref line, true);
			
			// Return true when done, false when errors occurred
			return (cpErrorResult == 0);
		}
		
		#endregion
	}
}
