//
// editing.cs
//
// Author:
//   Ben Maurer (bmaurer@users.sourceforge.net)
//
// (C) 2003 Ben Maurer
//

using System;
using System.Collections;
using System.Collections.Specialized;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Xml.XPath;
using System.Web;

namespace Monodoc {
	public class EditingUtils {
		
		public static string FormatEditUri (string document_identifier, string xpath)
		{
			return String.Format ("edit:{0}@{1}", HttpUtility.UrlEncode (document_identifier),
				HttpUtility.UrlEncode (xpath));
		}
		
		public static string GetXPath (XPathNavigator n)
		{
			switch (n.NodeType) {
				case XPathNodeType.Root: return "/";
				case XPathNodeType.Attribute: {
					string ret = "@" + n.Name;
					n.MoveToParent ();
					string s = GetXPath (n);
					return s + (s == "/" ? "" : "/") + ret;
				}
				break;
				case XPathNodeType.Element: {
					string ret = n.Name;
					int i = 1;
					while (n.MoveToPrevious ()) {
						if (n.NodeType == XPathNodeType.Element && n.Name == ret)
							i++;
					}
					ret += "[" + i + "]";
					if (n.MoveToParent ()) {
						string s = GetXPath (n);
						return s + (s == "/" ? "" : "/") + ret;
					}
				}
				break;
			}
			throw new Exception ("node type not supported for editing");
			
		}
		
		public static XmlNode GetNodeFromUrl (string url, RootTree tree)
		{
			string [] uSplit = ParseEditUrl (url);
			
			string xp = uSplit [2];
			string id =  uSplit [1];
			
			XmlDocument d;
			
			if (uSplit[0].StartsWith("monodoc:///")) {
				int prov = int.Parse (uSplit [0].Substring("monodoc:///".Length));
				d = tree.GetHelpSourceFromId (prov).GetHelpXmlWithChanges (id);
			} else if (uSplit[0].StartsWith("file:")) {
				d = new XmlDocument();
				d.PreserveWhitespace = true;
				d.Load(uSplit[0].Substring(5));
			} else {
				throw new NotImplementedException("Don't know how to load " + url); 
			}			
			
			return d.SelectSingleNode (xp);
				
		}
		
		public static void SaveChange (string url, RootTree tree, XmlNode node, string node_url)
		{
			string [] uSplit = ParseEditUrl (url);
		
			string xp = uSplit [2];
			string id =  uSplit [1];
						
			if (uSplit[0].StartsWith("monodoc:///")) {
				int prov = int.Parse (uSplit [0].Substring("monodoc:///".Length));
				HelpSource hs = tree.GetHelpSourceFromId (prov);
				
				changes.AddChange (hs.Name, hs.GetRealPath (id), xp, node, node_url);
				changes.Save ();
			} else if (uSplit[0].StartsWith("file:")) {
				uSplit[0] = uSplit[0].Substring(5);
				
				XmlDocument d = new XmlDocument();
				d.PreserveWhitespace = true;
				d.Load(uSplit[0]);
				
				XmlNode original = d.SelectSingleNode(xp);
				original.ParentNode.ReplaceChild(d.ImportNode(node, true), original);
				
				d.Save(uSplit[0]);
			} else {				
				throw new NotImplementedException("Don't know how to save to " + url); 
			}
		}

		public static void RemoveChange (string url, RootTree tree)
		{
			string [] uSplit = ParseEditUrl (url);
		
			string xp = uSplit [2];
			string id = uSplit [1];
						
			if (uSplit[0].StartsWith("monodoc:///")) {
				int prov = int.Parse (uSplit [0].Substring("monodoc:///".Length));
				HelpSource hs = tree.GetHelpSourceFromId (prov);
				
				changes.RemoveChange (hs.Name, hs.GetRealPath (id), xp);
				changes.Save ();
			} else if (uSplit[0].StartsWith("file:")) {
				//TODO: Not implemented
			}
		}
		
		public static void RenderEditPreview (string url, RootTree tree, XmlNode new_node, XmlWriter w)
		{
			string [] uSplit = ParseEditUrl (url);
		
			string xp = uSplit [2];
			string id =  uSplit [1];
						
			if (uSplit[0].StartsWith("monodoc:///")) {
				int prov = int.Parse (uSplit [0].Substring("monodoc:///".Length));
				HelpSource hs = tree.GetHelpSourceFromId (prov);
				hs.RenderPreviewDocs (new_node, w);
			} else {
				foreach (HelpSource hs in tree.HelpSources) {
					if (hs is EcmaUncompiledHelpSource) {
						// It doesn't matter which EcmaHelpSource is chosen.
						hs.RenderPreviewDocs (new_node, w);
						break;
					}
				}				
			}
		}
		
		public static string [] ParseEditUrl (string url)
		{
			if (!url.StartsWith ("edit:"))
				throw new Exception ("wtf");
			
			string [] parts = url.Split ('@');
			if (parts.Length != 2)
				throw new Exception ("invalid editing url");
			
			string xp = HttpUtility.UrlDecode (parts [1]);
			parts = HttpUtility.UrlDecode (parts [0]).Substring ("edit:".Length).Split ('@');
			if (parts.Length == 1) {
				string p = parts[0];
				parts = new string[2];
				parts[0] = p;
				parts[1] = "";
			}
			
			return new string [] {parts [0], parts [1], xp};
		}
		
		public static void AccountForChanges (XmlDocument d, string doc_set, string real_file)
		{
			try {
				FileChangeset fcs = changes.GetChangeset (doc_set, real_file);
				if (fcs == null)
					return;
				
				foreach (Change c in fcs.Changes) {
					// Filter out old changes
					if (c.FromVersion != RootTree.MonodocVersion)
						continue;
					
					XmlNode old = d.SelectSingleNode (c.XPath);
					if (old != null)
						old.ParentNode.ReplaceChild (d.ImportNode (c.NewNode, true), old);
				}
			} catch {
				return;
			}
		}
	
		public static GlobalChangeset changes = GlobalChangeset.Load ();

		static public GlobalChangeset GetChangesFrom (int starting_serial_id)
		{
			return changes.GetFrom (starting_serial_id);
		}
	}

#region Data Model
	public class GlobalChangeset {

		public static XmlSerializer serializer = new XmlSerializer (typeof (GlobalChangeset));
		static string changeset_file = Path.Combine (SettingsHandler.Path, "changeset.xml");
		static string changeset_backup_file = Path.Combine (SettingsHandler.Path, "changeset.xml~");
	
		public static GlobalChangeset Load ()
		{
			try {
				if (File.Exists (changeset_file))
					return LoadFromFile (changeset_file);
			} catch {}
			
			return new GlobalChangeset ();
		}
		
		public static GlobalChangeset LoadFromFile (string fileName)
		{
			using (Stream s = File.OpenRead (fileName)) {
				return (GlobalChangeset) serializer.Deserialize (s);
			}
		}			
		
		public void Save ()
		{
			SettingsHandler.EnsureSettingsDirectory ();

			try {    
				if (File.Exists(changeset_file))  // create backup copy
					File.Copy (changeset_file, changeset_backup_file, true);
           
				using (FileStream fs = File.Create (changeset_file)){
					serializer.Serialize (fs, this);
				}
			} catch (Exception e) {
				Console.WriteLine ("Error while saving changes. " + e);
				if (File.Exists(changeset_backup_file))  // if saving fails then use backup if we have one				
					File.Copy (changeset_backup_file, changeset_file, true);
				else
					File.Delete (changeset_file);   // if no backup, delete invalid changeset 
			}
		}
		
		static void VerifyDirectoryExists (DirectoryInfo d) {
			if (d.Exists)
				return;

			VerifyDirectoryExists (d.Parent);
			d.Create ();
		}

		[XmlElement ("DocSetChangeset", typeof (DocSetChangeset))]
		public ArrayList DocSetChangesets = new ArrayList ();

		public FileChangeset GetChangeset (string doc_set, string real_file)
		{
			foreach (DocSetChangeset dscs in DocSetChangesets) {
				if (dscs.DocSet != doc_set) 
					continue;
			
				foreach (FileChangeset fcs in dscs.FileChangesets) {
					if (fcs.RealFile == real_file)
						return fcs;
				}
			}
			
			return null;
		}

		public int Count {
			get {
				int count = 0;
				
				foreach (DocSetChangeset dscs in DocSetChangesets){
					foreach (FileChangeset fcs in dscs.FileChangesets){
						count += fcs.Changes.Count;
					}
				}

				return count;
			}
		}

		Change NewChange (string xpath, XmlNode new_node, string node_url)
		{
			Change new_change = new Change ();
			new_change.XPath = xpath;
			new_change.NewNode = new_node;
			new_change.NodeUrl = node_url;

			Console.WriteLine ("New serial:" + SettingsHandler.Settings.SerialNumber);
			new_change.Serial = SettingsHandler.Settings.SerialNumber;

			return new_change;
		}
		
		public void AddChange (string doc_set, string real_file, string xpath, XmlNode new_node, string node_url)
		{
			FileChangeset new_file_change_set;
			Change new_change = NewChange (xpath, new_node, node_url);
			
			if (real_file == null)
				throw new Exception ("Could not find real_file. Please talk to Miguel or Ben about this");
			
			foreach (DocSetChangeset dscs in DocSetChangesets) {
				if (dscs.DocSet != doc_set) 
					continue;

				foreach (FileChangeset fcs in dscs.FileChangesets) {
					if (fcs.RealFile != real_file)
						continue;
					
					foreach (Change c in fcs.Changes) {
						if (c.XPath == xpath) {
							c.NewNode = new_node;
							c.Serial = SettingsHandler.Settings.SerialNumber;
							return;
						}
					}

					fcs.Changes.Add (new_change);
					return;
					
				}
				
				new_file_change_set = new FileChangeset ();
				new_file_change_set.RealFile = real_file;
				new_file_change_set.Changes.Add (new_change);
				dscs.FileChangesets.Add (new_file_change_set);
				return;
					
			}
			
			DocSetChangeset new_dcs = new DocSetChangeset ();
			new_dcs.DocSet = doc_set;
			
			new_file_change_set = new FileChangeset ();
			new_file_change_set.RealFile = real_file;
			
			new_file_change_set.Changes.Add (new_change);
			new_dcs.FileChangesets.Add (new_file_change_set);
			DocSetChangesets.Add (new_dcs);
		}

		public void RemoveChange (string doc_set, string real_file, string xpath)
		{
			if (real_file == null)
				throw new Exception ("Could not find real_file. Please talk to Miguel or Ben about this");
			
			for (int i = 0; i < DocSetChangesets.Count; i++) {
				DocSetChangeset dscs = DocSetChangesets [i] as DocSetChangeset;
				if (dscs.DocSet != doc_set) 
					continue;

				for (int j = 0; j < dscs.FileChangesets.Count; j++) {
					FileChangeset fcs = dscs.FileChangesets [j] as FileChangeset;
					if (fcs.RealFile != real_file)
						continue;

					for (int k = 0; k < fcs.Changes.Count; k++) {
						Change c = fcs.Changes [k] as Change;
						if (c.XPath == xpath) {
							fcs.Changes.Remove (c);
							break;
						}
					}
					if (fcs.Changes.Count == 0)
						dscs.FileChangesets.Remove (fcs);
				}

				if (dscs.FileChangesets.Count == 0)
					DocSetChangesets.Remove (dscs);
			}
		}

		public GlobalChangeset GetFrom (int starting_serial_id)
		{
			GlobalChangeset s = null;
			
			foreach (DocSetChangeset dscs in DocSetChangesets){
				object o = dscs.GetFrom (starting_serial_id);
				if (o == null)
					continue;
				if (s == null)
					s = new GlobalChangeset ();
				s.DocSetChangesets.Add (o);
			}
			return s;
		}
	}
	
	public class DocSetChangeset {
		[XmlAttribute] public string DocSet;
		
		[XmlElement ("FileChangeset", typeof (FileChangeset))]
		public ArrayList FileChangesets = new ArrayList ();

		public DocSetChangeset GetFrom (int starting_serial_id)
		{
			DocSetChangeset dsc = null;
			
			foreach (FileChangeset fcs in FileChangesets){
				object o = fcs.GetFrom (starting_serial_id);
				if (o == null)
					continue;
				if (dsc == null){
					dsc = new DocSetChangeset ();
					dsc.DocSet = DocSet;
				}
				dsc.FileChangesets.Add (o);
			}
			return dsc;
		}
	}
	
	public class FileChangeset {
		[XmlAttribute] public string RealFile;
		
		[XmlElement ("Change", typeof (Change))]
		public ArrayList Changes = new ArrayList ();

		public FileChangeset GetFrom (int starting_serial_id)
		{
			FileChangeset fcs = null;

			foreach (Change c in Changes){
				if (c.Serial < starting_serial_id)
					continue;
				if (fcs == null){
					fcs = new FileChangeset ();
					fcs.RealFile = RealFile;
				}
				fcs.Changes.Add (c);
			}
			return fcs;
		}
	}
	
	public class Change {
		[XmlAttribute] public string XPath;
		[XmlAttribute] public int FromVersion = RootTree.MonodocVersion;
		[XmlAttribute] public string NodeUrl;
		
		public XmlNode NewNode;

		public int Serial;

		bool applied = false;
		
		//
		// These are not a property, because we dont want them serialized;
		// Only used by the Admin Client.
		//
		public bool Applied ()
		{
			return applied;
		}

		public void SetApplied (bool value)
		{
			applied = value;
		}
	}
#endregion
	
	public class EditMerger {
		GlobalChangeset changeset;
		ArrayList targetDirs;
		
		public EditMerger (GlobalChangeset changeset, ArrayList targetDirs)
		{
			this.changeset = changeset;
			this.targetDirs = targetDirs;
		}
		
		public void Merge ()
		{
			foreach (DocSetChangeset dsc in changeset.DocSetChangesets) {
				bool merged = false;
				foreach (string path in targetDirs) {
					if (File.Exists (Path.Combine (path, dsc.DocSet + ".source"))) {
						Merge (dsc, path);
						merged = true;
						break;
					}
				}
				if (!merged) Console.WriteLine ("Could not merge docset {0}", dsc.DocSet);
			}
		}
		
		void Merge (DocSetChangeset dsc, string path)
		{
			Console.WriteLine ("Merging changes in {0} ({1})", dsc.DocSet, path);
			
			foreach (FileChangeset fcs in dsc.FileChangesets) {
				if (File.Exists (Path.Combine (path, fcs.RealFile)))
					Merge (fcs, path);
				else
					Console.WriteLine ("\tCould not find file {0}", Path.Combine (path, fcs.RealFile));
			}
		}
		
		void Merge (FileChangeset fcs, string path)
		{
			XmlDocument d = new XmlDocument ();
			d.Load (Path.Combine (path, fcs.RealFile));
			
			foreach (Change c in fcs.Changes) {
				XmlNode old = d.SelectSingleNode (c.XPath);
				if (old != null)
					old.ParentNode.ReplaceChild (d.ImportNode (c.NewNode, true), old);
			}
			
			d.Save (Path.Combine (path, fcs.RealFile));
		}
	}
}
