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

namespace MonoDevelop.Components.Diff {
	public abstract class StructuredDiff {
		internal Hashtable nodeInterfaces = new Hashtable();
		internal Hashtable comparisonCache = new Hashtable();
		
		public virtual void AddInterface(Type type, NodeInterface nodeInterface) {
			nodeInterfaces[type] = nodeInterface;
		}
		
		internal NodeInterface GetInterface(object obj) {
			bool store = false;
			Type type = obj.GetType();
			while (type != null) {
				NodeInterface ret = (NodeInterface)nodeInterfaces[type];
				if (ret != null) {
					if (store) nodeInterfaces[obj.GetType()] = ret;
					return ret;
				}
				type = type.BaseType;
				store = true;
			}
			throw new ArgumentException("Node type has no interface defined: " + obj.GetType());
		}
		
		public float CompareLists(IList left, IList right) {
			return CompareLists(left, right, 0, false);
		}
		
		public void Compare(object left, object right) {
			comparisonCache.Clear();
			CompareLists(new object[] { left }, new object[] { right }, 0, true);
		}
		
		private float CompareLists(IList left, IList right, float threshold, bool output) {
			// Given two lists, find the elements in the list that correspond.
			// Two elements correspond if their 'difference metric' is less than
			// or equal to threshold.  For the hunks of correspondent items,
			// recursively descend into items not truly equal.  For hunks of
			// irreconsiliable material, raise the threshold to the next useful
			// level and rescan the items.
			
			if (left.Count == 0 && right.Count == 0)
				return 0;
			
			NodeComparerWrapper comparer = new NodeComparerWrapper(threshold, this);
			
			Diff diff = new Diff(left, right, comparer, comparer);
			
			int nitems = 0, ndiffs = 0;
			
			foreach (Diff.Hunk hunk in diff) {
				if (hunk.Same || (hunk.Left.Count == 1 && hunk.Right.Count == 1)) {
					// This comprises a block of correspondent items who
					// differ by no more than the threshold value.
					
					nitems += hunk.Left.Count;

					bool inSameRegion = false;
					
					for (int i = 0; i < hunk.Left.Count; i++) {
						object oleft = hunk.Left[i];
						object oright = hunk.Right[i];
						
						NodeInterface ileft = GetInterface(oleft);
						NodeInterface iright = GetInterface(oright);
						
						IList cleft = null, cright = null;
						cleft = ileft.GetChildren(oleft);
						cright = iright.GetChildren(oright);
						
						float comp = 0;
						if (ileft == iright)
							comp = ileft.Compare(oleft, oright, this);
						
						// If the nodes are equal, emit one node.
						if (ileft == iright && comp == 0) {
							if (output) {
								if (!inSameRegion) { WritePushSame(); inSameRegion = true; }
								WriteNodeSame(ileft, oleft, oright);
							}
							
						// Recurse into the lists of each node.
						} else if (ileft == iright && cleft != null && cright != null && cleft.Count > 0 && cright.Count > 0 && comp <= 1.0) {
							if (output && inSameRegion) { WritePopSame(); inSameRegion = false; }
							if (output) WritePushNode(ileft, oleft, oright);
							float d = CompareLists(cleft, cright, 0, output);
							d *= hunk.Left.Count;
							if (d < 1) d = 1;
							ndiffs += (int)d;
							if (output) WritePopNode();
							
						// The nodes are not equal, so emit removed and added nodes.
						} else {
							if (output && inSameRegion) { WritePopSame(); inSameRegion = false; }
							if (output) WriteNodeChange(ileft, oleft, iright, oright);
							ndiffs += hunk.Left.Count;
						}
					}
					
					if (output && inSameRegion) WritePopSame();
				} else {
					int ct = hunk.Left.Count + hunk.Right.Count;
					nitems += ct;
					ndiffs += ct;
					
					if (output) {
						bool noRecurse = comparer.minimumDifference >= 1;
						if (hunk.Right.Count == 0 || (hunk.Left.Count > 0 && noRecurse))
							WriteNodesRemoved(hunk.Left);
						if (hunk.Left.Count == 0 || (hunk.Right.Count > 0 && noRecurse))
							WriteNodesAdded(hunk.Right);
						if (hunk.Right.Count != 0 && hunk.Left.Count != 0 && !noRecurse)
							CompareLists(hunk.Left, hunk.Right, comparer.minimumDifference, output);
					}
				}
			}
			
			return (float)ndiffs / (float)nitems;
		}
		
		protected abstract void WritePushNode(NodeInterface nodeInterface, object left, object right);

		protected abstract void WritePushSame();
		
		protected abstract void WriteNodeSame(NodeInterface nodeInterface, object left, object right);

		protected abstract void WritePopSame();

		protected abstract void WriteNodeChange(NodeInterface leftInterface, object left, NodeInterface rightInterface, object right);

		protected abstract void WriteNodesRemoved(IList objects);

		protected abstract void WriteNodesAdded(IList objects);

		protected abstract void WritePopNode();
	}
		
	public class XmlOutputStructuredDiff : StructuredDiff {
		bool deepOutput, allContext;
		XmlWriter output;
		Hashtable contextNodes;
		
		public XmlOutputStructuredDiff(XmlWriter output, string context) {
			this.output = output;
			this.deepOutput = (context == null);
			this.allContext = (context != null && context == "*");
			
			if (!deepOutput && !allContext) {
				contextNodes = new Hashtable();
				foreach (string name in context.Split(','))
					contextNodes[name.Trim()] = contextNodes;
			}
		}
		
		public override void AddInterface(Type type, NodeInterface nodeInterface) {
			if (!(nodeInterface is XmlOutputNodeInterface))
				throw new ArgumentException("Node interfaces for the XmlOutputStructuredDiff must implement XmlOutputNodeInterface.");
			base.AddInterface(type, nodeInterface);
		}
		
		protected override void WritePushSame() {
		}
		
		protected override void WritePopSame() {
		}

		protected override void WriteNodeSame(NodeInterface nodeInterface, object left, object right) {
			bool deep = deepOutput;
			
			if (left is XmlNode && !deepOutput && !allContext) {
				if (!contextNodes.ContainsKey(((XmlNode)left).Name)) {
					return;
				} else {
					deep = true;
				}
			}

			((XmlOutputNodeInterface)nodeInterface).WriteBeginNode(left, left, (XmlWriter)output);
			output.WriteAttributeString("Status", "Same");
			if (deep)
				((XmlOutputNodeInterface)nodeInterface).WriteNodeChildren(left, (XmlWriter)output);
			output.WriteEndElement();
		}

		protected override void WriteNodeChange(NodeInterface leftInterface, object left, NodeInterface rightInterface, object right) {
			((XmlOutputNodeInterface)leftInterface).WriteBeginNode(left, right, (XmlWriter)output);
			output.WriteAttributeString("Status", "Changed");
				((XmlOutputNodeInterface)leftInterface).WriteBeginNode(left, left, (XmlWriter)output);
					((XmlOutputNodeInterface)leftInterface).WriteNodeChildren(left, (XmlWriter)output);
				output.WriteEndElement();
				((XmlOutputNodeInterface)rightInterface).WriteBeginNode(right, right, (XmlWriter)output);
					((XmlOutputNodeInterface)rightInterface).WriteNodeChildren(right, (XmlWriter)output);
				output.WriteEndElement();
			output.WriteEndElement();
		}

		protected override void WritePushNode(NodeInterface nodeInterface, object left, object right) {
			((XmlOutputNodeInterface)nodeInterface).WriteBeginNode(left, right, output);
		}

		protected override void WritePopNode() {
			output.WriteEndElement();
		}
		
		void AddRemove(IList objects, string status) {
			foreach (object obj in objects) {
				XmlOutputNodeInterface i = (XmlOutputNodeInterface)GetInterface(obj);
				((XmlOutputNodeInterface)i).WriteBeginNode(obj, obj, output);
				output.WriteAttributeString("Status", status);
				i.WriteBeginNode(obj, obj, output);
				i.WriteNodeChildren(obj, output);
				output.WriteEndElement();
				output.WriteEndElement();
			}
		}

		protected override void WriteNodesRemoved(IList objects) {
			AddRemove(objects, "Removed");
		}
		protected override void WriteNodesAdded(IList objects) {
			AddRemove(objects, "Added");
		}
	}
	
	public abstract class NodeInterface {
		public abstract IList GetChildren(object node);
		public abstract float Compare(object left, object right, StructuredDiff comparer);
		public virtual int GetHashCode(object node) {
			return node.GetHashCode();
		}
	}
	
	public interface XmlOutputNodeInterface {
		void WriteNodeChildren(object node, XmlWriter output);
		void WriteBeginNode(object left, object right, XmlWriter output);
	}
	
	internal class NodeComparerWrapper : IComparer, IEqualityComparer {
		float threshold;
		StructuredDiff differ;
		
		public float minimumDifference = 1;
		
		bool useCache = true;
		
		public NodeComparerWrapper(float threshold, StructuredDiff differ) {
			this.threshold = threshold;
			this.differ = differ;
		}
		
		private class Pair {
			object left, right;
			int code;
			public Pair(object a, object b, StructuredDiff differ) {
				left = a; right = b;
				code = unchecked(differ.GetInterface(left).GetHashCode(left) + differ.GetInterface(right).GetHashCode(right));
			}
			public override bool Equals(object o) {
				return ((Pair)o).left == left && ((Pair)o).right == right;
			}
			public override int GetHashCode() {
				return code;
			}
		}
		
		int IComparer.Compare(object left, object right) {
			float ret;
			
			Pair pair = new Pair(left, right, differ);
			
			if (left.GetType() != right.GetType()) {
				ret = 1;
			} else if (useCache && differ.comparisonCache.ContainsKey(pair)) {
				ret = (float)differ.comparisonCache[pair];
			} else {
				NodeInterface comparer = differ.GetInterface(left);
				ret = comparer.Compare(left, right, differ);
			}
			
			if (useCache)
				differ.comparisonCache[pair] = ret;
			
			if (ret < minimumDifference && ret > threshold)
				minimumDifference = ret;
			
			if (ret <= threshold)
				return 0;
			else
				return 1;
		}
		
		bool IEqualityComparer.Equals (object a, object b)
		{
			return ((IComparer)this).Compare (a, b) == 0;
		}
		
		int IEqualityComparer.GetHashCode (object obj) {
			return differ.GetInterface(obj).GetHashCode(obj);
		}
	}

}
