using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Xml;
using System.Text.RegularExpressions;
using Mono.Unix;

namespace Tomboy
{
	public delegate void NoteRenameHandler (Note sender, string old_title);
	public delegate void NoteSavedHandler (Note note);
	public delegate void TagAddedHandler (Note note, Tag tag);
	public delegate void TagRemovingHandler (Note note, Tag tag);
	public delegate void TagRemovedHandler (Note note, string tag_name);

	// Contains all pure note data, like the note title and note text.
	public class NoteData
	{
		readonly string uri;
		string title;
		string text;
		DateTime create_date;
		DateTime change_date;

		int cursor_pos;
		int width, height;
		int x, y;
		bool open_on_startup;

		Dictionary<string, Tag> tags;

		const int noPosition = -1;

		public NoteData (string uri)
		{
			this.uri = uri;
			this.text = "";
			x = noPosition;
			y = noPosition;

			tags = new Dictionary<string, Tag> ();

			create_date = DateTime.MinValue;
			change_date = DateTime.MinValue;
		}

		public string Uri
		{
			get {
				return uri;
			}
		}

		public string Title
		{
			get {
				return title;
			}
			set {
				title = value;
			}
		}

		public string Text
		{
			get {
				return text;
			}
			set {
				text = value;
			}
		}

		public DateTime CreateDate
		{
			get {
				return create_date;
			}
			set {
				create_date = value;
			}
		}

		public DateTime ChangeDate
		{
			get {
				return change_date;
			}
			set {
				change_date = value;
			}
		}

		// FIXME: the next five attributes don't belong here (the data
		// model), but belong into the view; for now they are kept here
		// for backwards compatibility

		public int CursorPosition
		{
			get {
				return cursor_pos;
			}
			set {
				cursor_pos = value;
			}
		}

		public int Width
		{
			get {
				return width;
			}
			set {
				width = value;
			}
		}

		public int Height
		{
			get {
				return height;
			}
			set {
				height = value;
			}
		}

		public int X
		{
			get {
				return x;
			}
			set {
				x = value;
			}
		}

		public int Y
		{
			get {
				return y;
			}
			set {
				y = value;
			}
		}

		public Dictionary<string, Tag> Tags
		{
			get {
				return tags;
			}
		}

		public bool IsOpenOnStartup
		{
			get {
				return open_on_startup;
			}
			set {
				open_on_startup = value;
			}
		}

		public void SetPositionExtent (int x, int y, int width, int height)
		{
			Debug.Assert (x >= 0 && y >= 0);
			Debug.Assert (width > 0 && height > 0);

			this.x = x;
			this.y = y;
			this.width = width;
			this.height = height;
		}

		public bool HasPosition ()
		{
			return x != noPosition && y != noPosition;
		}

		public bool HasExtent ()
		{
			return width != 0 && height != 0;
		}
	}

	// This class wraps a NoteData instance. Most method calls are
	// forwarded to the wrapped instance, but there is special behaviour
	// for the Text attribute. This class takes care that this attribute
	// is synchronized with the contents of a NoteBuffer instance.
	public class NoteDataBufferSynchronizer
	{
		readonly NoteData data;
		NoteBuffer buffer;

		public NoteDataBufferSynchronizer (NoteData data)
		{
			this.data = data;
		}

		public NoteData GetDataSynchronized ()
		{
			// Assert that Data.Text returns the current
			// text from the text buffer.
			SynchronizeText ();
			return data;
		}

		public NoteData Data
		{
			get {
				return data;
			}
		}

		public NoteBuffer Buffer
		{
			get {
				return buffer;
			}
			set {
				buffer = value;
				buffer.Changed += BufferChanged;
				buffer.TagApplied += BufferTagApplied;
				buffer.TagRemoved += BufferTagRemoved;

				SynchronizeBuffer ();

				InvalidateText ();
			}
		}

		//Text is actually an Xml formatted string
		public string Text
		{
			get {
				SynchronizeText ();
				return data.Text;
			}
			set {
				data.Text = value;
				SynchronizeBuffer ();
			}
		}

		// Custom Methods

		void InvalidateText ()
		{
			data.Text = "";
		}

		bool TextInvalid ()
		{
			return data.Text == "";
		}

		void SynchronizeText ()
		{
			if (TextInvalid () && buffer != null) {
				data.Text = NoteBufferArchiver.Serialize (buffer);
			}
		}

		void SynchronizeBuffer ()
		{
			if (!TextInvalid () && buffer != null) {
				// Don't create Undo actions during load
				buffer.Undoer.FreezeUndo ();

				buffer.Clear ();

				// Load the stored xml text
				NoteBufferArchiver.Deserialize (buffer,
				                                buffer.StartIter,
				                                data.Text);
				buffer.Modified = false;

				Gtk.TextIter cursor;
				if (data.CursorPosition != 0) {
					// Move cursor to last-saved position
					cursor = buffer.GetIterAtOffset (data.CursorPosition);
				} else {
					// Avoid title line
					cursor = buffer.GetIterAtLine (2);
				}
				buffer.PlaceCursor (cursor);

				// New events should create Undo actions
				buffer.Undoer.ThawUndo ();
			}
		}

		// Callbacks

		void BufferChanged (object sender, EventArgs args)
		{
			InvalidateText ();
		}

		void BufferTagApplied (object sender, Gtk.TagAppliedArgs args)
		{
			if (NoteTagTable.TagIsSerializable (args.Tag)) {
				InvalidateText ();
			}
		}

		void BufferTagRemoved (object sender, Gtk.TagRemovedArgs args)
		{
			if (NoteTagTable.TagIsSerializable (args.Tag)) {
				InvalidateText ();
			}
		}
	}

	public class Note
	{
		readonly NoteDataBufferSynchronizer data;

		string filepath;

		bool save_needed;
		bool is_deleting;

		NoteManager manager;
		NoteWindow window;
		NoteBuffer buffer;
		NoteTagTable tag_table;

		InterruptableTimeout save_timeout;

		struct ChildWidgetData
		{
			public Gtk.TextChildAnchor anchor;
			public Gtk.Widget widget;
		};

		Queue <ChildWidgetData> childWidgetQueue;

		[System.Diagnostics.Conditional ("DEBUG_SAVE")]
		static void DebugSave (string format, params object[] args)
		{
			Console.WriteLine (format, args);
		}

		Note (NoteData data, string filepath, NoteManager manager)
		{
			this.data = new NoteDataBufferSynchronizer (data);
			this.filepath = filepath;
			this.manager = manager;

			// Make sure each of the tags that NoteData found point to the
			// instance of this note.
			foreach (Tag tag in data.Tags.Values) {
				AddTag (tag);
			}

			save_timeout = new InterruptableTimeout ();
			save_timeout.Timeout += SaveTimeout;

			childWidgetQueue = new Queue <ChildWidgetData> ();
			
			is_deleting = false;
		}

		static string UrlFromPath (string filepath)
		{
			return "note://tomboy/" +
			       Path.GetFileNameWithoutExtension (filepath);
		}

		public static Note CreateNewNote (string title,
		                                  string filepath,
		                                  NoteManager manager)
		{
			NoteData data = new NoteData (UrlFromPath (filepath));
			data.Title = title;
			data.CreateDate = DateTime.Now;
			data.ChangeDate = data.CreateDate;
			return new Note (data, filepath, manager);
		}

		public static Note CreateExistingNote (NoteData data,
		                                       string filepath,
		                                       NoteManager manager)
		{
			if (data.CreateDate == DateTime.MinValue)
				data.CreateDate = File.GetCreationTime (filepath);
			if (data.ChangeDate == DateTime.MinValue)
				data.ChangeDate = File.GetLastWriteTime (filepath);
			return new Note (data, filepath, manager);
		}

		public void Delete ()
		{
			is_deleting = true;
			save_timeout.Cancel ();

			// Remove the note from all the tags
			foreach (Tag tag in Tags) {
				RemoveTag (tag);
			}

			if (window != null) {
				window.Hide ();
				window.Destroy ();
			}

			// Remove note URI from GConf entry menu_pinned_notes
			IsPinned = false;
		}

		// Load from an existing Note...
		public static Note Load (string read_file, NoteManager manager)
		{
			NoteData data = NoteArchiver.Read (read_file, UrlFromPath (read_file));
			Note note = CreateExistingNote (data, read_file, manager);

			return note;
		}

		public void Save ()
		{
			// Prevent any other condition forcing a save on the note
			// if Delete has been called.
			if (is_deleting)
				return;
			
			// Do nothing if we don't need to save.  Avoids unneccessary saves
			// e.g on forced quit when we call save for every note.
			if (!save_needed)
				return;

			Logger.Log ("Saving '{0}'...", data.Data.Title);

			NoteArchiver.Write (filepath, data.GetDataSynchronized ());

			if (Saved != null)
				Saved (this);
		}

		//
		// Buffer change signals.  These queue saves and invalidate the serialized text
		// depending on the change...
		//

		void BufferChanged (object sender, EventArgs args)
		{
			DebugSave ("BufferChanged queueing save");
			QueueSave (true);
		}

		void BufferTagApplied (object sender, Gtk.TagAppliedArgs args)
		{
			if (NoteTagTable.TagIsSerializable (args.Tag)) {
				DebugSave ("BufferTagApplied queueing save: {0}", args.Tag.Name);
				QueueSave (true);
			}
		}

		void BufferTagRemoved (object sender, Gtk.TagRemovedArgs args)
		{
			if (NoteTagTable.TagIsSerializable (args.Tag)) {
				DebugSave ("BufferTagRemoved queueing save: {0}", args.Tag.Name);
				QueueSave (true);
			}
		}

		void BufferInsertMarkSet (object sender, Gtk.MarkSetArgs args)
		{
			if (args.Mark != buffer.InsertMark)
				return;

			data.Data.CursorPosition = args.Location.Offset;

			DebugSave ("BufferInsertSetMark queueing save");
			QueueSave (false);
		}

		//
		// Window events.  Queue a save when the window location/size has changed, and set
		// our window to null on delete, and fire the Opened event on window realize...
		//

		[GLib.ConnectBefore]
		void WindowConfigureEvent (object sender, Gtk.ConfigureEventArgs args)
		{
			int cur_x, cur_y, cur_width, cur_height;

			// Ignore events when maximized.  We don't want notes
			// popping up maximized the next run.
			if ((window.GdkWindow.State & Gdk.WindowState.Maximized) > 0)
				return;

			window.GetPosition (out cur_x, out cur_y);
			window.GetSize (out cur_width, out cur_height);

			if (data.Data.X == cur_x &&
			                data.Data.Y == cur_y &&
			                data.Data.Width == cur_width &&
			                data.Data.Height == cur_height)
				return;

			data.Data.SetPositionExtent (cur_x, cur_y, cur_width, cur_height);

			DebugSave ("WindowConfigureEvent queueing save");
			QueueSave (false);
		}

		[GLib.ConnectBefore]
		void WindowDestroyed (object sender, EventArgs args)
		{
			window = null;
		}

		/// <summary>
		/// Set a 4 second timeout to execute the save.  Possibly
		/// invalidate the text, which causes a re-serialize when the
		/// timeout is called...
		/// </summary>
		/// <param name="content_changed">Indicates whether or not
		/// to update the note's last change date</param>
		public void QueueSave (bool content_changed)
		{
			DebugSave ("Got QueueSave");

			// Replace the existing save timeout.  Wait 4 seconds
			// before saving...
			save_timeout.Reset (4000);
			save_needed = true;

			if (content_changed) {
				data.Data.ChangeDate = DateTime.Now;
			}
		}

		// Save timeout to avoid constanly resaving.  Called every 4 seconds.
		void SaveTimeout (object sender, EventArgs args)
		{
			try {
				Save ();
				save_needed = false;
			} catch (Exception e) {
				// FIXME: Present a nice dialog here that interprets the
				// error message correctly.
				Logger.Log ("Error while saving: {0}", e);
			}
		}

		public void AddTag (Tag tag)
		{
			if (tag == null)
				throw new ArgumentNullException ("Note.AddTag () called with a null tag.");

			tag.AddNote (this);

			if (!data.Data.Tags.ContainsKey (tag.NormalizedName)) {
				data.Data.Tags [tag.NormalizedName] = tag;

				if (TagAdded != null)
					TagAdded (this, tag);

				DebugSave ("Tag added, queueing save");
				QueueSave (true);
			}
		}

		public void RemoveTag (Tag tag)
		{
			if (tag == null)
				throw new ArgumentException ("Note.RemoveTag () called with a null tag.");

			if (!data.Data.Tags.ContainsKey (tag.NormalizedName))
				return;

			if (TagRemoving != null)
				TagRemoving (this, tag);

			data.Data.Tags.Remove (tag.NormalizedName);
			tag.RemoveNote (this);

			if (TagRemoved != null)
				TagRemoved (this, tag.NormalizedName);

			DebugSave ("Tag removed, queueing save");
			QueueSave (true);
		}
		
		public bool ContainsTag (Tag tag)
		{
			if (data.Data.Tags.ContainsKey (tag.NormalizedName) == true)
				return true;
			
			return false;
		}

		public void AddChildWidget (Gtk.TextChildAnchor childAnchor, Gtk.Widget widget)
		{
			ChildWidgetData data = new ChildWidgetData ();
			data.anchor = childAnchor;
			data.widget = widget;

			childWidgetQueue.Enqueue (data);

			if (HasWindow)
				ProcessChildWidgetQueue ();
		}

		private void ProcessChildWidgetQueue ()
		{
			// Insert widgets in the childWidgetQueue into the NoteEditor
			if (!HasWindow)
				return; // can't do anything without a window

			foreach (ChildWidgetData data in childWidgetQueue) {
				data.widget.Show();
				Window.Editor.AddChildAtAnchor (data.widget, data.anchor);
			}

			childWidgetQueue.Clear ();
		}

		public string Uri
		{
			get {
				return data.Data.Uri;
			}
		}

		public string Id
		{
			get {
				return data.Data.Uri.Replace ("note://tomboy/","");        // TODO: Store on Note instantiation
			}
		}

		public string FilePath
		{
			get {
				return filepath;
			}
			set {
				filepath = value;
			}
		}

		public string Title
		{
			get {
				return data.Data.Title;
			}
			set {
				if (data.Data.Title != value) {
					if (window != null)
						window.Title = value;

					string old_title = data.Data.Title;
					data.Data.Title = value;

					if (Renamed != null)
						Renamed (this, old_title);

					QueueSave (true); // TODO: Right place for this?
				}
			}
		}

		public void RenameWithoutLinkUpdate (string newTitle)
		{
			if (data.Data.Title != newTitle) {
				if (window != null)
					window.Title = newTitle;

				data.Data.Title = newTitle;

				// HACK:
				if (Renamed != null)
					Renamed (this, newTitle);

				QueueSave (true); // TODO: Right place for this?
			}
		}

		public string XmlContent
		{
			get {
				return data.Text;
			}
			set {
				if (buffer != null) {
					buffer.SetText("");
					NoteBufferArchiver.Deserialize (buffer, value);
				} else
					data.Text = value;
			}
		}

		/// <summary>
		/// Return the complete contents of this note's .note XML file
		/// In case of any error, null is returned.
		/// </summary>
		public string GetCompleteNoteXml ()
		{
			if (!File.Exists (filepath))
				return null;

			// Make sure file contents are up to date
			save_needed = true; // HACK: Catches newly created notes
			Save ();

			StreamReader reader = null;
			try {
				reader = new StreamReader (filepath);
				return reader.ReadToEnd ();
			} catch (Exception e) {
				Logger.Error ("Error received while attempting to read " +
				              filepath + ": " + e.Message);
				return null;
			} finally {
				if (reader != null)
					reader.Close ();
			}
		}

		// Reload note data from a complete note XML string
		// Should referesh note window, too
		public void LoadForeignNoteXml (string foreignNoteXml)
		{
			if (foreignNoteXml == null)
				throw new ArgumentNullException ("foreignNoteXml");

			// Arguments to this method cannot be trusted.  If this method
			// were to throw an XmlException in the middle of processing,
			// a note could be damaged.  Therefore, we check for parseability
			// ahead of time, and throw early.
			XmlDocument xmlDoc = new XmlDocument ();
			// This will throw an XmlException if foreignNoteXml is not parseable
			xmlDoc.LoadXml (foreignNoteXml);
			xmlDoc = null;

			StringReader reader = new StringReader (foreignNoteXml);
			XmlTextReader xml = new XmlTextReader (reader);
			xml.Namespaces = false;

			// Remove tags now, since a note with no tags has
			// no "tags" element in the XML
			foreach (Tag tag in Tags)
			RemoveTag (tag);

			while (xml.Read ()) {
				switch (xml.NodeType) {
				case XmlNodeType.Element:
					switch (xml.Name) {
					case "title":
						Title = xml.ReadString ();
						break;
					case "text":
						XmlContent = xml.ReadInnerXml ();
						break;
					case "last-change-date":
						data.Data.ChangeDate =
						        XmlConvert.ToDateTime (xml.ReadString (), NoteArchiver.DATE_TIME_FORMAT);
						break;
					case "create-date":
						data.Data.CreateDate =
						        XmlConvert.ToDateTime (xml.ReadString (), NoteArchiver.DATE_TIME_FORMAT);
						break;
					case "tags":
						XmlDocument doc = new XmlDocument ();
						List<string> tag_strings = ParseTags (doc.ReadNode (xml.ReadSubtree ()));
						foreach (string tag_str in tag_strings) {
							Tag tag = TagManager.GetOrCreateTag (tag_str);
							AddTag (tag);
						}
						break;
					case "open-on-startup":
						IsOpenOnStartup = bool.Parse (xml.ReadString ());
						break;
					}
					break;
				}
			}

			xml.Close ();

			// TODO: Any reason to queue a save here?  Maybe not for sync but for others?
		}

		// TODO: CODE DUPLICATION SUCKS
		List<string> ParseTags (XmlNode tagNodes)
		{
			List<string> tags = new List<string> ();

			foreach (XmlNode node in tagNodes.SelectNodes ("//tag")) {
				string tag = node.InnerText;
				tags.Add (tag);
			}

			return tags;
		}

		public string TextContent
		{
			get {
				if (buffer != null)
					return buffer.GetSlice (buffer.StartIter,
					buffer.EndIter,
					false /* hidden_chars */);
				else
					return XmlDecoder.Decode (XmlContent);
			}
			set {
				if (buffer != null)
					buffer.SetText (value);
				else
					Logger.Log ("Setting text content for closed notes not supported");
			}

		}

		public NoteData Data
		{
			get {
				return data.GetDataSynchronized ();
			}
		}

		public DateTime CreateDate
		{
			get {
				return data.Data.CreateDate;
			}
		}

		public DateTime ChangeDate
		{
			get {
				return data.Data.ChangeDate;
			}
		}

		public NoteManager Manager
		{
			get {
				return manager;
			}
			set {
				manager = value;
			}
		}

		public NoteTagTable TagTable
		{
			get {
				if (tag_table == null) {
					#if FIXED_GTKSPELL
					// NOTE: Sharing the same TagTable means
					// that formatting is duplicated between
					// buffers.
					tag_table = NoteTagTable.Instance;
					#else
					// NOTE: GtkSpell chokes on shared
					// TagTables because it blindly tries to
					// create a new "gtkspell-misspelling"
					// tag, which fails if one already
					// exists in the table.
					tag_table = new NoteTagTable ();
					#endif
				}
				return tag_table;
			}
		}

		public bool HasBuffer
		{
			get {
				return null != buffer;
			}
		}

		public NoteBuffer Buffer
		{
			get {
				if (buffer == null) {
					Logger.Log ("Creating Buffer for '{0}'...",
					data.Data.Title);

					buffer = new NoteBuffer (TagTable, this);
					data.Buffer = buffer;

					// Listen for further changed signals
					buffer.Changed += BufferChanged;
					buffer.TagApplied += BufferTagApplied;
					buffer.TagRemoved += BufferTagRemoved;
					buffer.MarkSet += BufferInsertMarkSet;
				}
				return buffer;
			}
		}

		public bool HasWindow
		{
			get {
				return null != window;
			}
		}

		public NoteWindow Window
		{
			get {
				if (window == null) {
					window = new NoteWindow (this);
					window.Destroyed += WindowDestroyed;
					window.ConfigureEvent += WindowConfigureEvent;

					if (data.Data.HasExtent ())
						window.SetDefaultSize (data.Data.Width,
						                       data.Data.Height);

					if (data.Data.HasPosition ())
						window.Move (data.Data.X, data.Data.Y);

					// This is here because emiting inside
					// OnRealized causes segfaults.
					if (Opened != null)
						Opened (this, new EventArgs ());

					// Add any child widgets if any exist now that
					// the window is showing.
					ProcessChildWidgetQueue ();
				}
				return window;
			}
		}

		public bool IsSpecial
		{
			get {
				return NoteManager.StartNoteUri == data.Data.Uri;
			}
		}

		public bool IsNew
		{
			get {
				// Note is new if created in the last 24 hours.
				return data.Data.CreateDate > DateTime.Now.AddHours (-24);
			}
		}

		public bool IsLoaded
		{
			get {
				return buffer != null;
			}
		}

		public bool IsOpened
		{
			get {
				return window != null;
			}
		}

		public bool IsPinned
		{
			get {
				string pinned_uris = (string)
				Preferences.Get (Preferences.MENU_PINNED_NOTES);
				return pinned_uris.IndexOf (Uri) > -1;
			}
			set {
				string new_pinned = "";
				string old_pinned = (string)
				                    Preferences.Get (Preferences.MENU_PINNED_NOTES);
				bool pinned = old_pinned.IndexOf (Uri) > -1;

				if (value == pinned)
					return;

				if (value) {
					new_pinned = Uri + " " + old_pinned;
				} else {
					string [] pinned_split = old_pinned.Split (' ', '\t', '\n');
					foreach (string pin in pinned_split) {
						if (pin != "" && pin != Uri) {
							new_pinned += pin + " ";
						}
					}
				}

				Preferences.Set (Preferences.MENU_PINNED_NOTES, new_pinned);
			}
		}

		public bool IsOpenOnStartup
		{
			get {
				return Data.IsOpenOnStartup;
			}
			set {
				if (Data.IsOpenOnStartup != value) {
					Data.IsOpenOnStartup = value;
					save_needed = true;
				}
			}
		}

		public List<Tag> Tags
		{
			get {
				return new List<Tag> (data.Data.Tags.Values);
			}
		}

		public event EventHandler Opened;
		public event NoteRenameHandler Renamed;
		public event NoteSavedHandler Saved;
		public event TagAddedHandler TagAdded;
		public event TagRemovingHandler TagRemoving;
		public event TagRemovedHandler TagRemoved;
	}

	// Singleton - allow overriding the instance for easy sensing in
	// test classes - we're not bothering with double-check locking,
	// since this class is only seldomly used
	public class NoteArchiver
	{
		public const string CURRENT_VERSION = "0.2";

		public const string DATE_TIME_FORMAT = "yyyy-MM-ddTHH:mm:ss.fffffffzzz";

		static NoteArchiver instance = null;
		static readonly object lock_ = new object();

		protected NoteArchiver ()
		{
		}

		public static NoteArchiver Instance
		{
			get
			{
				lock (lock_)
				{
					if (instance == null)
						instance = new NoteArchiver ();
					return instance;
				}
			}
			set {
				lock (lock_)
				{
					instance = value;
				}
			}
		}

		public static NoteData Read (string read_file, string uri)
		{
			return Instance.ReadFile (read_file, uri);
		}

		public virtual NoteData ReadFile (string read_file, string uri)
		{
			NoteData note = new NoteData (uri);
			string version = "";

			StreamReader reader = new StreamReader (read_file,
			                                        System.Text.Encoding.UTF8);
			XmlTextReader xml = new XmlTextReader (reader);
			xml.Namespaces = false;

			while (xml.Read ()) {
				switch (xml.NodeType) {
				case XmlNodeType.Element:
					switch (xml.Name) {
					case "note":
						version = xml.GetAttribute ("version");
						break;
					case "title":
						note.Title = xml.ReadString ();
						break;
					case "text":
						// <text> is just a wrapper around <note-content>
						// NOTE: Use .text here to avoid triggering a save.
						note.Text = xml.ReadInnerXml ();
						break;
					case "last-change-date":
						note.ChangeDate =
						        XmlConvert.ToDateTime (xml.ReadString (), DATE_TIME_FORMAT);
						break;
					case "create-date":
						note.CreateDate =
						        XmlConvert.ToDateTime (xml.ReadString (), DATE_TIME_FORMAT);
						break;
					case "cursor-position":
						note.CursorPosition = int.Parse (xml.ReadString ());
						break;
					case "width":
						note.Width = int.Parse (xml.ReadString ());
						break;
					case "height":
						note.Height = int.Parse (xml.ReadString ());
						break;
					case "x":
						note.X = int.Parse (xml.ReadString ());
						break;
					case "y":
						note.Y = int.Parse (xml.ReadString ());
						break;
					case "tags":
						XmlDocument doc = new XmlDocument ();
						List<string> tag_strings = ParseTags (doc.ReadNode (xml.ReadSubtree ()));
						foreach (string tag_str in tag_strings) {
							Tag tag = TagManager.GetOrCreateTag (tag_str);
							note.Tags [tag.NormalizedName] = tag;
						}
						break;
					case "open-on-startup":
						note.IsOpenOnStartup = bool.Parse (xml.ReadString ());
						break;
					}
					break;
				}
			}

			xml.Close ();

			if (version != NoteArchiver.CURRENT_VERSION) {
				// Note has old format, so rewrite it.  No need
				// to reread, since we are not adding anything.
				Logger.Log ("Updating note XML to newest format...");
				NoteArchiver.Write (read_file, note);
			}

			return note;
		}

		public static void Write (string write_file, NoteData note)
		{
			Instance.WriteFile (write_file, note);
		}

		public virtual void WriteFile (string write_file, NoteData note)
		{
			string tmp_file = write_file + ".tmp";

			XmlTextWriter xml = new XmlTextWriter (tmp_file, System.Text.Encoding.UTF8);
			Write (xml, note);
			xml.Close ();

			if (File.Exists (write_file)) {
				string backup_path = write_file + "~";
				if (File.Exists (backup_path))
					File.Delete (backup_path);

				// Backup the to a ~ file, just in case
				File.Move (write_file, backup_path);

				// Move the temp file to write_file
				File.Move (tmp_file, write_file);

				// Delete the ~ file
				File.Delete (backup_path);
			} else {
				// Move the temp file to write_file
				File.Move (tmp_file, write_file);
			}
		}

		public static void Write (TextWriter writer, NoteData note)
		{
			Instance.WriteFile (writer, note);
		}

		public void WriteFile (TextWriter writer, NoteData note)
		{
			XmlTextWriter xml = new XmlTextWriter (writer);
			Write (xml, note);
			xml.Close ();
		}

		void Write (XmlTextWriter xml, NoteData note)
		{
			xml.Formatting = Formatting.Indented;

			xml.WriteStartDocument ();
			xml.WriteStartElement (null, "note", "http://beatniksoftware.com/tomboy");
			xml.WriteAttributeString(null,
			                         "version",
			                         null,
			                         CURRENT_VERSION);
			xml.WriteAttributeString("xmlns",
			                         "link",
			                         null,
			                         "http://beatniksoftware.com/tomboy/link");
			xml.WriteAttributeString("xmlns",
			                         "size",
			                         null,
			                         "http://beatniksoftware.com/tomboy/size");

			xml.WriteStartElement (null, "title", null);
			xml.WriteString (note.Title);
			xml.WriteEndElement ();

			xml.WriteStartElement (null, "text", null);
			xml.WriteAttributeString ("xml", "space", null, "preserve");
			// Insert <note-content> blob...
			xml.WriteRaw (note.Text);
			xml.WriteEndElement ();

			xml.WriteStartElement (null, "last-change-date", null);
			xml.WriteString (
			        XmlConvert.ToString (note.ChangeDate, DATE_TIME_FORMAT));
			xml.WriteEndElement ();

			if (note.CreateDate != DateTime.MinValue) {
				xml.WriteStartElement (null, "create-date", null);
				xml.WriteString (
				        XmlConvert.ToString (note.CreateDate, DATE_TIME_FORMAT));
				xml.WriteEndElement ();
			}

			xml.WriteStartElement (null, "cursor-position", null);
			xml.WriteString (note.CursorPosition.ToString ());
			xml.WriteEndElement ();

			xml.WriteStartElement (null, "width", null);
			xml.WriteString (note.Width.ToString ());
			xml.WriteEndElement ();

			xml.WriteStartElement (null, "height", null);
			xml.WriteString (note.Height.ToString ());
			xml.WriteEndElement ();

			xml.WriteStartElement (null, "x", null);
			xml.WriteString (note.X.ToString ());
			xml.WriteEndElement ();

			xml.WriteStartElement (null, "y", null);
			xml.WriteString (note.Y.ToString ());
			xml.WriteEndElement ();

			if (note.Tags.Count > 0) {
				xml.WriteStartElement (null, "tags", null);
				foreach (Tag tag in note.Tags.Values) {
					xml.WriteStartElement (null, "tag", null);
					xml.WriteString (tag.Name);
					xml.WriteEndElement ();
				}
				xml.WriteEndElement ();
			}

			xml.WriteStartElement (null, "open-on-startup", null);
			xml.WriteString (note.IsOpenOnStartup.ToString ());
			xml.WriteEndElement ();

			xml.WriteEndElement (); // Note
			xml.WriteEndDocument ();
		}

		// <summary>
		// Parse the tags from the <tags> element
		// </summary>
		List<string> ParseTags (XmlNode tagNodes)
		{
			List<string> tags = new List<string> ();

			foreach (XmlNode node in tagNodes.SelectNodes ("//tag")) {
				string tag = node.InnerText;
				tags.Add (tag);
			}

			return tags;
		}

		public virtual string GetRenamedNoteXml (string noteXml, string oldTitle, string newTitle)
		{
			string updatedXml;

			// Replace occurences of oldTitle with newTitle in noteXml
			string titleTagPattern =
			        string.Format ("<title>{0}</title>", oldTitle);
			string titleTagReplacement =
			        string.Format ("<title>{0}</title>", newTitle);
			updatedXml = Regex.Replace (noteXml, titleTagPattern, titleTagReplacement);

			string titleContentPattern =
			        string.Format ("<note-content([^>]*)>\\s*{0}", oldTitle);
			string titleContentReplacement =
			        string.Format ("<note-content$1>{0}", newTitle);
			updatedXml = Regex.Replace (updatedXml, titleContentPattern, titleContentReplacement);

			return updatedXml;
		}

		public virtual string GetTitleFromNoteXml (string noteXml)
		{
			if (noteXml != null && noteXml.Length > 0) {
				XmlTextReader xml = new XmlTextReader (new StringReader (noteXml));
				xml.Namespaces = false;

				while (xml.Read ()) {
					switch (xml.NodeType) {
					case XmlNodeType.Element:
						switch (xml.Name) {
						case "title":
							return xml.ReadString ();
							break;
						}
						break;
					}
				}
			}

			return null;
		}
	}

	public class NoteUtils
	{
		public static void ShowDeletionDialog (Note note, Gtk.Window parent)
		{
			HIGMessageDialog dialog =
			        new HIGMessageDialog (
			        parent,
			        Gtk.DialogFlags.DestroyWithParent,
			        Gtk.MessageType.Question,
			        Gtk.ButtonsType.None,
			        Catalog.GetString ("Really delete this note?"),
			        Catalog.GetString ("If you delete a note it is " +
			                           "permanently lost."));

			Gtk.Button button;

			button = new Gtk.Button (Gtk.Stock.Cancel);
			button.CanDefault = true;
			button.Show ();
			dialog.AddActionWidget (button, Gtk.ResponseType.Cancel);
			dialog.DefaultResponse = Gtk.ResponseType.Cancel;

			button = new Gtk.Button (Gtk.Stock.Delete);
			button.CanDefault = true;
			button.Show ();
			dialog.AddActionWidget (button, 666);

			int result = dialog.Run ();
			if (result == 666) {
				note.Manager.Delete (note);
			}

			dialog.Destroy();
		}
	}
}
