#region Copyright
//
// Nini Configuration Project.
// Copyright (C) 2006 Brent R. Matzelle.  All rights reserved.
//
// This software is published under the terms of the MIT X11 license, a copy of 
// which has been included with this distribution in the LICENSE.txt file.
// 
#endregion

using System;
using System.IO;
using System.Xml;
using System.Collections;

namespace Nini.Config
{
	/// <include file='XmlConfigSource.xml' path='//Class[@name="XmlConfigSource"]/docs/*' />
	public class XmlConfigSource : ConfigSourceBase
	{
		#region Private variables
		XmlDocument configDoc = null;
		string savePath = null;
		#endregion

		#region Constructors
		/// <include file='XmlConfigSource.xml' path='//Constructor[@name="Constructor"]/docs/*' />
		public XmlConfigSource ()
		{
			configDoc = new XmlDocument ();
			configDoc.LoadXml ("<Nini/>");
			PerformLoad (configDoc);
		}

		/// <include file='XmlConfigSource.xml' path='//Constructor[@name="ConstructorPath"]/docs/*' />
		public XmlConfigSource (string path)
		{
			Load (path);
		}

		/// <include file='XmlConfigSource.xml' path='//Constructor[@name="ConstructorXmlReader"]/docs/*' />
		public XmlConfigSource (XmlReader reader)
		{
			Load (reader);
		}
		#endregion
		
		#region Public properties
		/// <include file='XmlConfigSource.xml' path='//Property[@name="SavePath"]/docs/*' />
		public string SavePath
		{
			get { return savePath; }
		}
		#endregion
		
		#region Public methods
		/// <include file='XmlConfigSource.xml' path='//Method[@name="LoadPath"]/docs/*' />
		public void Load (string path)
		{
			savePath = path;
			configDoc = new XmlDocument ();
			configDoc.Load (path);
			PerformLoad (configDoc);
		}

		/// <include file='XmlConfigSource.xml' path='//Method[@name="LoadXmlReader"]/docs/*' />
		public void Load (XmlReader reader)
		{
			configDoc = new XmlDocument ();
			configDoc.Load (reader);
			PerformLoad (configDoc);
		}

		/// <include file='XmlConfigSource.xml' path='//Method[@name="Save"]/docs/*' />
		public override void Save ()
		{
			if (!IsSavable ()) {
				throw new ArgumentException ("Source cannot be saved in this state");
			}

			MergeConfigsIntoDocument ();
			configDoc.Save (savePath);
			base.Save ();
		}
		
		/// <include file='XmlConfigSource.xml' path='//Method[@name="SavePath"]/docs/*' />
		public void Save (string path)
		{
			this.savePath = path;
			this.Save ();
		}
		
		/// <include file='XmlConfigSource.xml' path='//Method[@name="SaveTextWriter"]/docs/*' />
		public void Save (TextWriter writer)
		{
			MergeConfigsIntoDocument ();
			configDoc.Save (writer);
			savePath = null;
			OnSaved (new EventArgs ());
		}

		/// <include file='XmlConfigSource.xml' path='//Method[@name="SaveStream"]/docs/*' />
		public void Save (Stream stream)
		{
			MergeConfigsIntoDocument ();
			configDoc.Save (stream);
			savePath = null;
			OnSaved (new EventArgs ());
		}

		/// <include file='IConfigSource.xml' path='//Method[@name="Reload"]/docs/*' />
		public override void Reload ()
		{
			if (savePath == null) {
				throw new ArgumentException ("Error reloading: You must have "
							+ "the loaded the source from a file");
			}

			configDoc = new XmlDocument ();
			configDoc.Load (savePath);
			MergeDocumentIntoConfigs ();
			base.Reload ();
		}

		/// <include file='XmlConfigSource.xml' path='//Method[@name="ToString"]/docs/*' />
		public override string ToString ()
		{
			MergeConfigsIntoDocument ();
			StringWriter writer = new StringWriter ();
			configDoc.Save (writer);

			return writer.ToString ();
		}
		#endregion

		#region Private methods
		/// <summary>
		/// Merges all of the configs from the config collection into the 
		/// XmlDocument.
		/// </summary>
		private void MergeConfigsIntoDocument ()
		{
			RemoveSections ();
			foreach (IConfig config in this.Configs)
			{
				string[] keys = config.GetKeys ();

				XmlNode node = GetSectionByName (config.Name);
				if (node == null) {
					node = SectionNode (config.Name);
					configDoc.DocumentElement.AppendChild (node);
				}
				RemoveKeys (config.Name);
				
				for (int i = 0; i < keys.Length; i++)
				{
					SetKey (node, keys[i], config.Get (keys[i]));
				}
			}
		}
		
		/// <summary>
		/// Removes all XML sections that were removed as configs.
		/// </summary>
		private void RemoveSections ()
		{
			XmlAttribute attr = null;

			foreach (XmlNode node in configDoc.DocumentElement.ChildNodes)
			{
				if (node.NodeType == XmlNodeType.Element
					&& node.Name == "Section") {

					attr = node.Attributes["Name"];
					if (attr != null) {
						if (this.Configs[attr.Value] == null) {
							configDoc.DocumentElement.RemoveChild (node);
						}
					} else {
						throw new ArgumentException ("Section name attribute not found");
					}
				}
			}
		}
		
		/// <summary>
		/// Removes all XML keys that were removed as config keys.
		/// </summary>
		private void RemoveKeys (string sectionName)
		{
			XmlNode sectionNode = GetSectionByName (sectionName);
			XmlAttribute keyName = null;
			
			if (sectionNode != null) {
				foreach (XmlNode node in sectionNode.ChildNodes)
				{
					if (node.NodeType == XmlNodeType.Element
						&& node.Name == "Key") {

						keyName = node.Attributes["Name"];
						if (keyName != null) {
							if (this.Configs[sectionName].Get (keyName.Value) == null) {
								sectionNode.RemoveChild (node);
							}
						} else {
							throw new ArgumentException ("Name attribute not found in key");
						}
					}
				}
			}
		}

		/// <summary>
		/// Loads all sections and keys.
		/// </summary>
		private void PerformLoad (XmlDocument document)
		{
			this.Configs.Clear ();

			this.Merge (this); // required for SaveAll
			
			if (document.DocumentElement.Name != "Nini") {
				throw new ArgumentException ("Did not find Nini XML root node");
			}
			
			LoadSections (document.DocumentElement);
		}
		
		/// <summary>
		/// Loads all configuration sections.
		/// </summary>
		private void LoadSections (XmlNode rootNode)
		{
			ConfigBase config = null;

			foreach (XmlNode child in rootNode.ChildNodes)
			{
				if (child.NodeType == XmlNodeType.Element
					&& child.Name == "Section") {
					config = new ConfigBase (child.Attributes["Name"].Value, this);
					this.Configs.Add (config);
					LoadKeys (child, config);
				}
			}
		}
		
		/// <summary>
		/// Loads all keys for a config.
		/// </summary>
		private void LoadKeys (XmlNode node, ConfigBase config)
		{
			foreach (XmlNode child in node.ChildNodes)
			{
				if (child.NodeType == XmlNodeType.Element
					&& child.Name == "Key") {
					config.Add (child.Attributes["Name"].Value,
								child.Attributes["Value"].Value);
				}
			}
		}
		
		/// <summary>
		/// Sets an XML key.  If it does not exist then it is created.
		/// </summary>
		private void SetKey (XmlNode sectionNode, string key, string value)
		{
			XmlNode node = GetKeyByName (sectionNode, key);
			
			if (node == null) {
				CreateKey (sectionNode, key, value);
			} else {
				node.Attributes["Value"].Value = value;
			}
		}
		
		/// <summary>
		/// Creates a key node and adds it to the collection at the end.
		/// </summary>
		private void CreateKey (XmlNode sectionNode, string key, string value)
		{
			XmlNode node = configDoc.CreateElement ("Key");
			XmlAttribute keyAttr = configDoc.CreateAttribute ("Name");
			XmlAttribute valueAttr = configDoc.CreateAttribute ("Value");
			keyAttr.Value = key;
			valueAttr.Value = value;

			node.Attributes.Append (keyAttr);
			node.Attributes.Append (valueAttr);

			sectionNode.AppendChild (node);
		}
		
		/// <summary>
		/// Returns a new section node.
		/// </summary>
		private XmlNode SectionNode (string name)
		{
			XmlNode result = configDoc.CreateElement ("Section");
			XmlAttribute nameAttr = configDoc.CreateAttribute ("Name");
			nameAttr.Value = name;
			result.Attributes.Append (nameAttr);
			
			return result;
		}

		/// <summary>
		/// Returns a section node by name.
		/// </summary>
		private XmlNode GetSectionByName (string name)
		{
			XmlNode result = null;

			foreach (XmlNode node in configDoc.DocumentElement.ChildNodes)
			{
				if (node.NodeType == XmlNodeType.Element
					&& node.Name == "Section"
					&& node.Attributes["Name"].Value == name) {
					result = node;
					break;
				}
			}

			return result;
		}

		/// <summary>
		/// Returns a key node by name.
		/// </summary>
		private XmlNode GetKeyByName (XmlNode sectionNode, string name)
		{
			XmlNode result = null;

			foreach (XmlNode node in sectionNode.ChildNodes)
			{
				if (node.NodeType == XmlNodeType.Element
					&& node.Name == "Key"
					&& node.Attributes["Name"].Value == name) {
					result = node;
					break;
				}
			}

			return result;
		}
		
		/// <summary>
		/// Returns true if this instance is savable.
		/// </summary>
		private bool IsSavable ()
		{
			return (this.savePath != null
					&& configDoc != null);
		}

		/// <summary>
		/// Merges the XmlDocument into the Configs when the document is 
		/// reloaded.  
		/// </summary>
		private void MergeDocumentIntoConfigs ()
		{
			// Remove all missing configs first
			RemoveConfigs ();

			foreach (XmlNode node in configDoc.DocumentElement.ChildNodes)
			{
				// If node is a section node
				if (node.NodeType == XmlNodeType.Element
					&& node.Name == "Section") {

					string sectionName = node.Attributes["Name"].Value;
					IConfig config = this.Configs[sectionName];
					if (config == null) {
						// The section is new so add it
						config = new ConfigBase (sectionName, this);
						this.Configs.Add (config);
					}				
					RemoveConfigKeys (config);
				}
			}
		}

		/// <summary>
		/// Removes all configs that are not in the newly loaded XmlDocument.  
		/// </summary>
		private void RemoveConfigs ()
		{
			IConfig config = null;
			for (int i = this.Configs.Count - 1; i > -1; i--)
			{
				config = this.Configs[i];
				// If the section is not present in the XmlDocument
				if (GetSectionByName (config.Name) == null) {
					this.Configs.Remove (config);
				}
			}
		}

		/// <summary>
		/// Removes all XML keys that were removed as config keys.
		/// </summary>
		private void RemoveConfigKeys (IConfig config)
		{
			XmlNode section = GetSectionByName (config.Name);

			// Remove old keys
			string[] configKeys = config.GetKeys ();
			foreach (string configKey in configKeys)
			{
				if (GetKeyByName (section, configKey) == null) {
					// Key doesn't exist, remove
					config.Remove (configKey);
				}
			}

			// Add or set all new keys
			foreach (XmlNode node in section.ChildNodes)
			{
				// Loop through all key nodes and add to config
				if (node.NodeType == XmlNodeType.Element
					&& node.Name == "Key") {
					config.Set (node.Attributes["Name"].Value,
								node.Attributes["Value"].Value);
				}
			}
		}
		#endregion
	}
}