using System;
using System.IO;
using System.Xml;
using System.Collections;
using System.Diagnostics;
using System.Threading;

namespace Orciid.Core
{
	/// <summary>
	/// Delegate for configuration change event
	/// </summary>
	/// <remarks>
	/// Delegate for configuration change event
	/// </remarks>
	public delegate void OnConfigurationChangeDelegate(IConfiguration configuration, object arg);

	/// <summary>
	/// Configuration interface
	/// </summary>
	/// <remarks>
	/// This interface defines all properties and methods that must be implemented
	/// by a configuration class.
	/// </remarks>
	public interface IConfiguration
	{
		/// <summary>
		/// Initialize configuration class
		/// </summary>
		/// <remarks>
		/// This method must be called once when the application is started.
		/// </remarks>
		/// <param name="applicationpath">The path the application runs in</param>
		void Initialize(string applicationpath);
		
		/// <summary>
		/// Physical path to the configuration file or other resource
		/// </summary>
		/// <remarks>
		/// This property returns the path to the configuration file or other resource
		/// that is in use.
		/// </remarks>
		/// <value>
		/// Path to the configuration file or other resource
		/// </value>
		string ConfigFile { get; }
		
		/// <summary>
		/// Checks key existance
		/// </summary>
		/// <remarks>
		/// This method can be used to check if a certain configuration key exists.
		/// The access methods in this class throw exceptions if a non-existant
		/// key is requested.
		/// </remarks>
		/// <param name="key">The key of the configuration value</param>
		/// <returns><c>true</c> if the key exists, <c>false</c> otherwise.</returns>
		bool ContainsKey(string key);
		
		/// <summary>
		/// List of keys
		/// </summary>
		/// <remarks>
		/// This method returns an array of all keys available in the configuration file.
		/// This includes any XML comments that may be included in the file.
		/// </remarks>
		/// <returns>An array of configuration keys</returns>
		string[] GetKeys();
		
		/// <summary>
		/// Checks if key has multiple values
		/// </summary>
		/// <remarks>
		/// Certain keys can be specified multiple times in the configuration file.
		/// This method checks if a given key has more than one value.
		/// </remarks>
		/// <param name="key">The key to check for multiple values</param>
		/// <returns><c>true</c> if the key has multiple values, <c>false</c> if the
		/// key only has one value or does not exist.</returns>
		bool IsMultiValue(string key);
		
		/// <summary>
		/// Returns a string value
		/// </summary>
		/// <remarks>
		/// This method throws an exception if the requested key does not exist
		/// or if this class has not been initialized.
		/// </remarks>
		/// <param name="key">The key of the configuration value</param>
		/// <returns>The configuration value</returns>
		string GetString(string key);
		
		/// <summary>
		/// Returns multiple string values
		/// </summary>
		/// <remarks>
		/// This method throws an exception if the requested key does not exist
		/// or if this class has not been initialized.
		/// Certain keys can be specified multiple times in the configuration file.
		/// This method returns an array of one or more string values.
		/// </remarks>
		/// <param name="key">The key of the configuration value</param>
		/// <returns>An array of configuration values.  The length of the array may be one.</returns>
		string[] GetMultiString(string key);
		
		/// <summary>
		/// Returns an integer value
		/// </summary>
		/// <remarks>
		/// This method throws an exception if the requested key does not exist,
		/// this class has not been initialized, or if the value cannot be parsed as an integer.
		/// </remarks>
		/// <param name="key">The key of the configuration value</param>
		/// <returns>The configuration value</returns>
		int GetInt(string key);
		
		/// <summary>
		/// Returns a boolean value
		/// </summary>
		/// <remarks>
		/// This method throws an exception if the requested key does not exist,
		/// this class has not been initialized, or if the value cannot be parsed as a boolean
		/// (<c>true</c>, any other value is assumed to be <c>false</c>).
		/// </remarks>
		/// <param name="key">The key of the configuration value</param>
		/// <returns>The configuration value</returns>
		bool GetBool(string key);

		/// <summary>
		/// Change event
		/// </summary>
		/// <remarks>
		/// This event is raised whenever the configuration changes
		/// </remarks>
		event OnConfigurationChangeDelegate OnChange;
	}

	/// <summary>
	/// Provides access to configuration object
	/// </summary>
	/// <remarks>
	/// By default the configuration object is a <see cref="XmlBasedConfiguration"/> object.
	/// It can be overridden internally for testing purposes.
	/// </remarks>
	public class Configuration
	{
		private static IConfiguration instance = new XmlBasedConfiguration();

		/// <summary>
		/// Get instance of configuration class
		/// </summary>
		/// <remarks>
		/// Returns the active instance of the configuration class
		/// </remarks>
		public static IConfiguration Instance
		{
			get
			{
				return instance;
			}
		}

		internal static IConfiguration SetInstance(IConfiguration i)
		{
			IConfiguration old = instance;
			instance = i;
			return old;
		}
	}
	
	/// <summary>
	/// System configuration
	/// </summary>
	/// <remarks>
	/// This class provides access to an XML based configuration file.  It must be initialized
	/// by calling the <see cref="Initialize"/> method providing an application path. The specified
	/// directory or one of its parent directories must contain a file called <c>config.xml</c>.
	/// The file itself can contain nested text nodes, which are accessible by the different <c>Get</c>
	/// methods in this class.  The configuration file is watched and reloaded upon change; the
	/// system does not need to be restarted for this to happen.  There is no guarantee though that
	/// changed values will be used immediately or at all, as other classes may cache configuration
	/// values themselves.  Values can be retrieved by using the node names in a dotted notation,
	/// omitting the root node. For example, the value in this file
	/// <code>
	///	&lt;config&gt;
	///		&lt;smtp&gt;
	///			&lt;server&gt;mail.example.com&lt;/server&gt;
	///		&lt;/smtp&gt;
	///	&lt;/config&gt;
	/// </code>
	/// would be retrieved by using <c>smtp.server</c> as the key.
	/// </remarks>
	public class XmlBasedConfiguration:
		IConfiguration
	{
		private const string configfilename = "config.xml";

		private static Hashtable configuration = null;
		private static string configfile = null;
		private static FileSystemWatcher watcher = null;

		internal XmlBasedConfiguration()
		{
		}

		/// <summary>
		/// Initialize configuration class
		/// </summary>
		/// <remarks>
		/// This method must be called once when the application is started.
		/// </remarks>
		/// <param name="applicationpath">The path the application runs in, i.e. the path
		/// in which the <c>config.xml</c> file can be found.</param>
		public void Initialize(string applicationpath)
		{
			DirectoryInfo d = new DirectoryInfo(applicationpath);
			while (d.Parent != null && !File.Exists(Path.Combine(d.FullName, configfilename)))
				d = d.Parent;
			configfile = Path.Combine(d.FullName, configfilename);
			ReloadConfiguration(true); // this may change configfile with a redirection
			if (watcher != null)
				watcher.EnableRaisingEvents = false;
			watcher = new FileSystemWatcher(Path.GetDirectoryName(configfile), 
				Path.GetFileName(configfile));
			watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Security;
			watcher.Changed += new FileSystemEventHandler(ConfigFileWatcher_Changed);
			watcher.EnableRaisingEvents = true;
		}

		private void ConfigFileWatcher_Changed(object sender, FileSystemEventArgs e)
		{
			try
			{
				ReloadConfiguration(false);
			}
			catch (Exception ex)
			{
				// FogBugz case 540: need to log error message somewhere
				throw ex;
			}
		}

		private void ReloadConfiguration(bool allowredirect)
		{
			lock (typeof(Configuration))
			{
				Thread.Sleep(500); // wait a little bit for file to become available
				configuration = new Hashtable();
				XmlDocument xml = new XmlDocument();
				// file stays locked/open sometimes, have not found out yet why
				using (FileStream stream = File.Open(configfile, FileMode.Open, FileAccess.Read, FileShare.Read))
				{
					xml.Load(stream);
				}
				XmlNode redirect = xml.SelectSingleNode("/config/redirect");
				if (allowredirect && redirect != null)
				{
					configfile = redirect.InnerText;
					ReloadConfiguration(false);
				}
				else
				{
					AddChildNodes(xml.SelectSingleNode("/config"), null);
					if (OnChange != null)
						OnChange(this, null);
				}				
			}
		}

		private void AddChildNodes(XmlNode node, string tag)
		{
			string localtag = "";
			if (node.Name != "#text")
			{
				if (tag != null && tag.Length > 0)
					localtag += tag + ".";
				localtag += node.Name;
			}
			else
				localtag = tag;
			string v = node.Value;
			if (v != null)
			{
				v = v.Trim();
				if (v.Length > 0)
				{
					if (configuration.ContainsKey(localtag))
					{
						object o = configuration[localtag];
						if (o is ArrayList)
							((ArrayList)o).Add(v);
						else
						{
							ArrayList a = new ArrayList();
							a.Add(o);
							a.Add(v);
							configuration[localtag] = a;
						}
					}
					else
						configuration.Add(localtag, v);
				}
			}
			if (node.HasChildNodes)
				foreach (XmlNode child in node.ChildNodes)
					AddChildNodes(child, localtag);
		}

		/// <summary>
		/// Physical path to the configuration file
		/// </summary>
		/// <remarks>
		/// Due to the recursive search for a configuration file, there may be doubt
		/// about which configuration file is actually used.  This property returns
		/// the path to the file in use.
		/// </remarks>
		/// <value>
		/// Physical path to the configuration file
		/// </value>
		public string ConfigFile
		{
			get
			{
				return configfile;
			}
		}

		/// <summary>
		/// Checks key existance
		/// </summary>
		/// <remarks>
		/// This method can be used to check if a certain configuration key exists.
		/// The access methods in this class throw exceptions if a non-existant
		/// key is requested.
		/// </remarks>
		/// <param name="key">The key of the configuration value</param>
		/// <returns><c>true</c> if the key exists, <c>false</c> otherwise.</returns>
		public bool ContainsKey(string key)
		{
			lock (typeof(Configuration))
			{
				return configuration.ContainsKey("config." + key);
			}
		}

		/// <summary>
		/// List of keys
		/// </summary>
		/// <remarks>
		/// This method returns an array of all keys available in the configuration file.
		/// This includes any XML comments that may be included in the file.
		/// </remarks>
		/// <returns>An array of configuration keys</returns>
		public string[] GetKeys()
		{
			ArrayList list = new ArrayList(configuration.Keys.Count);
			foreach (string key in configuration.Keys)
				list.Add(key.Substring(7));
			return (string[])list.ToArray(typeof(string));
		}

		/// <summary>
		/// Checks if key has multiple values
		/// </summary>
		/// <remarks>
		/// Certain keys can be specified multiple times in the configuration file.
		/// This method checks if a given key has more than one value.
		/// </remarks>
		/// <param name="key">The key to check for multiple values</param>
		/// <returns><c>true</c> if the key has multiple values, <c>false</c> if the
		/// key only has one value or does not exist.</returns>
		public bool IsMultiValue(string key)
		{
			lock (typeof(Configuration))
			{
				return configuration.ContainsKey("config." + key) &&
					configuration["config." + key] is ArrayList;
			}
		}

		/// <summary>
		/// Returns a string value
		/// </summary>
		/// <remarks>
		/// This method throws an exception if the requested key does not exist
		/// or if this class has not been initialized.
		/// </remarks>
		/// <param name="key">The key of the configuration value</param>
		/// <returns>The configuration value</returns>
		public string GetString(string key)
		{
			lock (typeof(Configuration))
			{
				if (configuration == null)
					throw new Exception("Configuration file is not loaded");
				if (!configuration.ContainsKey("config." + key))
					throw new Exception(String.Format("Unknown configuration key {0}", key));
				object o = configuration["config." + key];
				if (o is ArrayList)
					return (string)((ArrayList)o)[0];
				else
					return (string)o;
			}
		}

		/// <summary>
		/// Returns multiple string values
		/// </summary>
		/// <remarks>
		/// This method throws an exception if the requested key does not exist
		/// or if this class has not been initialized.
		/// Certain keys can be specified multiple times in the configuration file.
		/// This method returns an array of one or more string values.
		/// </remarks>
		/// <param name="key">The key of the configuration value</param>
		/// <returns>An array of configuration values.  The length of the array may be one.</returns>
		public string[] GetMultiString(string key)
		{
			lock (typeof(Configuration))
			{
				if (configuration == null)
					throw new Exception("Configuration file is not loaded");
				if (!configuration.ContainsKey("config." + key))
					throw new Exception(String.Format("Unknown configuration key {0}", key));
				object o = configuration["config." + key];
				if (o is ArrayList)
					return (string[])((ArrayList)o).ToArray(typeof(string));
				else
					return new string[] { (string)o };
			}
		}

		/// <summary>
		/// Returns an integer value
		/// </summary>
		/// <remarks>
		/// This method throws an exception if the requested key does not exist,
		/// this class has not been initialized, or if the value cannot be parsed as an integer.
		/// </remarks>
		/// <param name="key">The key of the configuration value</param>
		/// <returns>The configuration value</returns>
		public int GetInt(string key)
		{
			return Int32.Parse(GetString(key));
		}

		/// <summary>
		/// Returns a boolean value
		/// </summary>
		/// <remarks>
		/// This method throws an exception if the requested key does not exist,
		/// this class has not been initialized, or if the value cannot be parsed as a boolean
		/// (<c>true</c>, any other value is assumed to be <c>false</c>).
		/// </remarks>
		/// <param name="key">The key of the configuration value</param>
		/// <returns>The configuration value</returns>
		public bool GetBool(string key)
		{
			return (GetString(key) == "true");
		}

		/// <summary>
		/// Change event
		/// </summary>
		/// <remarks>
		/// This event is raised whenever the configuration changes
		/// </remarks>
		public event OnConfigurationChangeDelegate OnChange;
	}
}
