using System;
using System.Collections;
using System.Reflection;
using System.Xml;

using BLToolkit.TypeBuilder;

namespace BLToolkit.EditableObjects
{
	[Serializable]
	public class EditableXmlDocument: IEditable, ISetParent, IMemberwiseEditable, IPrintDebugState
	{
		private          Stack                      _changedNodes;
		private          XmlDocument                _original;
		private          XmlDocument                _current;
		private          IPropertyChanged           _parent;
		private          PropertyInfo               _propertyInfo;

		public EditableXmlDocument()
			: this(new XmlDocument())
		{
		}

		public EditableXmlDocument(XmlDocument value)
		{
			_changedNodes = null;
			_current      = value;
			_original     = value;

			StartXmlDocTracking();
		}

		[GetValue, SetValue]
		public XmlDocument Value
		{
			get { return _current; }
			set
			{
				if (_current == value)
					return;

				if (_current == _original)
					StopXmlDocTracking();

				// Drop changes, if any.
				//
				if (_changedNodes != null)
					_changedNodes.Clear();

				_current = value;

				if (_current == _original)
					StartXmlDocTracking();
			}
		}

		private void StartXmlDocTracking()
		{
			if (_current == null)
				return;

			_current.NodeInserted += HandleNodeChanged;
			_current.NodeRemoved  += HandleNodeChanged;
			_current.NodeChanged  += HandleNodeChanged;
		}

		private void StopXmlDocTracking()
		{
			if (_current == null)
				return;

			_current.NodeInserted -= HandleNodeChanged;
			_current.NodeRemoved  -= HandleNodeChanged;
			_current.NodeChanged  -= HandleNodeChanged;
		}

		private void HandleNodeChanged(object sender, XmlNodeChangedEventArgs ea)
		{
			if (ea.Action == XmlNodeChangedAction.Change && ea.NewValue == ea.OldValue)
			{
				// A void change can be ignored.
				//
				return;
			}

			if (_changedNodes == null)
				_changedNodes = new Stack();

			_changedNodes.Push(new XmlNodeTrackBack(ea));

			// Propagate changes to parent object, if set.
			//
			if (_parent != null)
				_parent.OnPropertyChanged(_propertyInfo);
		}

		#region IEditable Members

		public void AcceptChanges()
		{
			if (_original != _current)
			{
				_original = _current;
				StartXmlDocTracking();
			}
			else
			{
				// Let them go away.
				//
				if (_changedNodes != null)
					_changedNodes.Clear();
			}
		}

		public void RejectChanges()
		{
			if (_original != _current)
			{
				_current = _original;
				StartXmlDocTracking();
			}
			else if (_changedNodes != null && _changedNodes.Count > 0)
			{
				// Don't fall into an infinite loop.
				//
				StopXmlDocTracking();

				// A Stack enumerates from back to front.
				//
				foreach (XmlNodeTrackBack nodeTrackBack in _changedNodes)
				{
					switch (nodeTrackBack.Action)
					{
						case XmlNodeChangedAction.Insert:
							if (nodeTrackBack.Node.NodeType == XmlNodeType.Attribute)
								((XmlElement)nodeTrackBack.Value).Attributes.Remove((XmlAttribute)nodeTrackBack.Node);
							else
								((XmlNode)nodeTrackBack.Value).RemoveChild(nodeTrackBack.Node);
							break;
						case XmlNodeChangedAction.Remove:
							// NB: The order of children nodes may change.
							//
							if (nodeTrackBack.Node.NodeType == XmlNodeType.Attribute)
								((XmlElement)nodeTrackBack.Value).Attributes.Append((XmlAttribute)nodeTrackBack.Node);
							else
								((XmlNode)nodeTrackBack.Value).AppendChild(nodeTrackBack.Node);
							break;
						case XmlNodeChangedAction.Change:
							nodeTrackBack.Node.Value = (string)nodeTrackBack.Value;
							break;
					}
				}

				_changedNodes.Clear();
				StartXmlDocTracking();
			}
		}

		public bool IsDirty
		{
			get
			{
				if (_current == _original)
					return _changedNodes != null && _changedNodes.Count > 0;

				if (_current == null || _original == null)
					return true;

				return _current.InnerXml.TrimEnd() != _original.InnerXml.TrimEnd();
			}
		}

		#endregion

		#region IMemberwiseEditable Members

		public bool AcceptMemberChanges(PropertyInfo propertyInfo, string memberName)
		{
			if (memberName != propertyInfo.Name)
				return false;

			AcceptChanges();

			return true;
		}

		public bool RejectMemberChanges(PropertyInfo propertyInfo, string memberName)
		{
			if (memberName != propertyInfo.Name)
				return false;

			RejectChanges();

			return true;
		}

		public bool IsDirtyMember(PropertyInfo propertyInfo, string memberName, ref bool isDirty)
		{
			if (memberName != propertyInfo.Name)
				return false;

			isDirty = IsDirty;

			return true;
		}

		public void GetDirtyMembers(PropertyInfo propertyInfo, ArrayList list)
		{
			if (IsDirty)
				list.Add(propertyInfo);
		}

		#endregion

		#region IPrintDebugState Members

		public void PrintDebugState(PropertyInfo propertyInfo, ref string str)
		{
			str += string.Format("{0,-20} {1} {2,-80}\r\n",
				propertyInfo.Name, IsDirty? "*": " ", _current != null? _current.OuterXml: "(null)");
		}

		#endregion

		#region ISetParent Members

		public void SetParent(object parent, PropertyInfo propertyInfo)
		{
			_parent       = parent as IPropertyChanged;
			_propertyInfo = propertyInfo;
		}

		#endregion

		#region Inner types

		private struct XmlNodeTrackBack
		{
			public readonly XmlNode              Node;
			public readonly XmlNodeChangedAction Action;
			public readonly object               Value;

			public XmlNodeTrackBack(XmlNodeChangedEventArgs ea)
			{
				Node   = ea.Node;
				Action = ea.Action;
				switch(ea.Action)
				{
					case XmlNodeChangedAction.Insert:
						Value = ea.NewParent;
						break;
					case XmlNodeChangedAction.Remove:
						Value = ea.OldParent;
						break;
					case XmlNodeChangedAction.Change:
						Value = ea.OldValue;
						break;
					default:
						throw new ArgumentOutOfRangeException("ea", ea.Action, string.Format("Unknown XmlNodeChangedAction"));
				}
			}
		}

		#endregion

	}
}
