context: using System;
  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);
  
  	public enum ChangeType
  	{
  		NoChange,
  		ContentChanged,
  		OtherDataChanged
  	}
  
  	// 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;
  		DateTime metadata_change_date;
  
  		int cursor_pos, selection_bound_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;
  			selection_bound_pos = noPosition;
  
  			tags = new Dictionary<string, Tag> ();
  
  			create_date = DateTime.MinValue;
  			change_date = DateTime.MinValue;
  			metadata_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;
  			}
  		}
  
  		/// <summary>
  		/// Indicates the last time note content data changed.
  		/// Does not include tag/notebook changes (see MetadataChangeDate).
  		/// </summary>
  		public DateTime ChangeDate
  		{
  			get {
  				return change_date;
  			}
  			set {
  				change_date = value;
  				metadata_change_date = value;
  			}
  		}
  
  		/// <summary>
  		/// Indicates the last time non-content note data changed.
  		/// This currently only applies to tags/notebooks.
  		/// </summary>
  		public DateTime MetadataChangeDate
  		{
  			get {
  				return metadata_change_date;
  			}
  			set {
  				metadata_change_date = value;
  			}
  		}
  		
  
  		// FIXME: the next six 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 SelectionBoundPosition
  		{
  			get {
  				return selection_bound_pos;
  			}
  			set {
  				selection_bound_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)
  		{
  			if (x < 0 || y < 0)
  				return;
  			if (width <= 0 || height <= 0)
  				return;
  
  			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 += OnBufferChanged;
  				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);
  				
  				if (data.SelectionBoundPosition >= 0) {
  					// Move selection bound to last-saved position
  					Gtk.TextIter selection_bound;
  					selection_bound = buffer.GetIterAtOffset (data.SelectionBoundPosition);
  					buffer.MoveMark (buffer.SelectionBound.Name, selection_bound);
  				}
  
  				// New events should create Undo actions
  				buffer.Undoer.ThawUndo ();
  			}
  		}
  
  		// Callbacks
  
  		void OnBufferChanged (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;
  		bool enabled = true;
  
  		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;
  		}
  		/// <summary>
  		/// Returns a Tomboy URL from the given path.
  		/// </summary>
  		/// <param name="filepath">
  		/// A <see cref="System.String"/>
  		/// </param>
  		/// <returns>
  		/// A <see cref="System.String"/>
  		/// </returns>
  		static string UrlFromPath (string filepath)
  		{
  			return "note://tomboy/" +
  			       Path.GetFileNameWithoutExtension (filepath);
  		}
  
  		public override int GetHashCode ()
  		{
  			return this.Title.GetHashCode();
  		}
  		/// <summary>
  		/// Creates a New Note with the given values.
  		/// </summary>
  		/// <param name="title">
  		/// A <see cref="System.String"/>
  		/// </param>
  		/// <param name="filepath">
  		/// A <see cref="System.String"/>
  		/// </param>
  		/// <param name="manager">
  		/// A <see cref="NoteManager"/>
  		/// </param>
  		/// <returns>
  		/// A <see cref="Note"/>
  		/// </returns>
  		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;
  
  			string new_note_pattern = String.Format (Catalog.GetString ("New Note {0}"), @"\d+");
  			Note template_note = manager.GetOrCreateTemplateNote ();
  			string template_content = template_note.TextContent.Replace (template_note.Title, Title);
  
  			// Do nothing if this note contains the unchanged template content
  			// and if the title matches the new note title template to prevent
  			// lots of unwanted "New Note NNN" notes: Bug #545252
  			if (Regex.IsMatch (Title, new_note_pattern) && TextContent.Equals (template_content))
  				return;
  
  			Logger.Debug ("Saving '{0}'...", data.Data.Title);
  
  			try {
  				NoteArchiver.Write (filepath, data.GetDataSynchronized ());
  			} catch (Exception e) {
  				// Probably IOException or UnauthorizedAccessException?
  				Logger.Error ("Exception while saving note: " + e.ToString ());
  				NoteUtils.ShowIOErrorDialog (window);
  			}
  
  			if (Saved != null)
  				Saved (this);
  		}
  
  		//
  		// Buffer change signals.  These queue saves and invalidate the serialized text
  		// depending on the change...
  		//
  
  		void OnBufferChanged (object sender, EventArgs args)
  		{
  			DebugSave ("OnBufferChanged queueing save");
  			QueueSave (ChangeType.ContentChanged);
  			if (BufferChanged != null)
  				BufferChanged (this);
  		}
  
  		void BufferTagApplied (object sender, Gtk.TagAppliedArgs args)
  		{
  			if (NoteTagTable.TagIsSerializable (args.Tag)) {
  				DebugSave ("BufferTagApplied queueing save: {0}", args.Tag.Name);
  				QueueSave (TagTable.GetChangeType (args.Tag));
  			}
  		}
  
  		void BufferTagRemoved (object sender, Gtk.TagRemovedArgs args)
  		{
  			if (NoteTagTable.TagIsSerializable (args.Tag)) {
  				DebugSave ("BufferTagRemoved queueing save: {0}", args.Tag.Name);
  				QueueSave (TagTable.GetChangeType (args.Tag));
  			}
  		}
  
  		void OnBufferMarkSet (object sender, Gtk.MarkSetArgs args)
  		{
  			if (args.Mark == buffer.InsertMark)
  				data.Data.CursorPosition = args.Location.Offset;
  			else if (args.Mark == buffer.SelectionBound)
  				data.Data.SelectionBoundPosition = args.Location.Offset;
  			else
  				return;
  
  			DebugSave ("OnBufferSetMark queueing save");
  			QueueSave (ChangeType.NoChange);
  		}
  
  		//
  		// 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 (ChangeType.NoChange);
  		}
  
  		[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 (ChangeType changeType)
  		{
  			DebugSave ("Got QueueSave");
  
  			// Replace the existing save timeout.  Wait 4 seconds
  			// before saving...
  			save_timeout.Reset (4000);
  			if (!is_deleting)
  				save_needed = true;
  			
  			switch (changeType)
  			{
  			case ChangeType.ContentChanged:
  				// NOTE: Updating ChangeDate automatically updates MetdataChangeDate to match.
  				data.Data.ChangeDate = DateTime.Now;
  				break;
  			case ChangeType.OtherDataChanged:
  				// Only update MetadataChangeDate.  Used by sync/etc
  				// to know when non-content note data has changed,
  				// but order of notes in menu and search UI is
  				// unaffected.
  				data.Data.MetadataChangeDate = DateTime.Now;
  				break;
  			default:
  				break;
  			}
  		}
  
  		// 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.Error ("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 (ChangeType.OtherDataChanged);
  			}
  		}
  
  		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 (ChangeType.OtherDataChanged);
  		}
  		
  		public bool ContainsTag (Tag tag)
  		{
  			if (tag != null && data.Data.Tags.ContainsKey (tag.NormalizedName))
  				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 {
  				SetTitle (value, false);
  			}
  		}
  
  		public void SetTitle (string new_title, bool from_user_action)
  		{
  			if (data.Data.Title != new_title) {
  				if (window != null)
  					window.Title = new_title;
  
  				string old_title = data.Data.Title;
  				data.Data.Title = new_title;
  
  				if (from_user_action)
  					ProcessRenameLinkUpdate (old_title);
  
  				if (Renamed != null)
  					Renamed (this, old_title);
  
  				QueueSave (ChangeType.ContentChanged); // TODO: Right place for this?
  			}
  		}
  
  		private void ProcessRenameLinkUpdate (string old_title)
  		{
  			List<Note> linkingNotes = new List<Note> ();
  			foreach (Note note in manager.Notes) {
  				// Technically, containing text does not imply linking,
  				// but this is less work
  				if (note != this && note.ContainsText (old_title))
  					linkingNotes.Add (note);
  			}
  
  			if (linkingNotes.Count > 0) {
  				NoteRenameBehavior behavior = (NoteRenameBehavior)
  					Preferences.Get (Preferences.NOTE_RENAME_BEHAVIOR);
  				if (behavior == NoteRenameBehavior.AlwaysShowDialog) {
  					var dlg = new NoteRenameDialog (linkingNotes, old_title, this);
  					Gtk.ResponseType response = (Gtk.ResponseType) dlg.Run ();
  					if (response != Gtk.ResponseType.Cancel &&
  					    dlg.SelectedBehavior != NoteRenameBehavior.AlwaysShowDialog)
  						Preferences.Set (Preferences.NOTE_RENAME_BEHAVIOR, (int) dlg.SelectedBehavior);
  					foreach (var pair in dlg.Notes) {
  						if (pair.Value && response == Gtk.ResponseType.Yes) // Rename
  							pair.Key.RenameLinks (old_title, this);
  						else
  							pair.Key.RemoveLinks (old_title, this);
  					}
  					dlg.Destroy ();
  				} else if (behavior == NoteRenameBehavior.AlwaysRemoveLinks)
  					foreach (var note in linkingNotes)
  						note.RemoveLinks (old_title, this);
  				else if (behavior == NoteRenameBehavior.AlwaysRenameLinks)
  					foreach (var note in linkingNotes)
  						note.RenameLinks (old_title, this);
  			}
  		}
  
  		private bool ContainsText (string text)
  		{
  			return TextContent.IndexOf (text, StringComparison.InvariantCultureIgnoreCase) > -1;
  		}
  
  		private void RenameLinks (string old_title, Note renamed)
  		{
  			HandleLinkRename (old_title, renamed, true);
  		}
  
  		private void RemoveLinks (string old_title, Note renamed)
  		{
  			HandleLinkRename (old_title, renamed, false);
  		}
  
  		private void HandleLinkRename (string old_title, Note renamed, bool rename_links)
  		{
  			// Check again, things may have changed
  			if (!ContainsText (old_title))
  				return;
  
  			string old_title_lower = old_title.ToLower ();
  
  			NoteTag link_tag = TagTable.LinkTag;
  
  			// Replace existing links with the new title.
  			TextTagEnumerator enumerator = new TextTagEnumerator (Buffer, link_tag);
  			foreach (TextRange range in enumerator) {
  				if (range.Text.ToLower () != old_title_lower)
  					continue;
  
  				if (!rename_links) {
  					Logger.Debug ("Removing link tag from text '{0}'",
  					              range.Text);
  					Buffer.RemoveTag (link_tag, range.Start, range.End);
  				} else {
  					Logger.Debug ("Replacing '{0}' with '{1}'",
  					              range.Text,
  					              renamed.Title);
  					Gtk.TextIter start_iter = range.Start;
  					Gtk.TextIter end_iter = range.End;
  					Buffer.Delete (ref start_iter, ref end_iter);
  					start_iter = range.Start;
  					Buffer.InsertWithTags (ref start_iter, renamed.Title, link_tag);
  				}
  			}
  		}
  
  		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 (ChangeType.ContentChanged); // TODO: Right place for this?
  			}
  		}
  
  		public string XmlContent
  		{
  			get {
  				return data.Text;
  			}
  			set {
  				if (buffer != null) {
  					buffer.Text = string.Empty;
  					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 ()
  		{
  			return NoteArchiver.WriteString (data.GetDataSynchronized ());
  		}
  
  		// Reload note data from a complete note XML string
  		// Should referesh note window, too
  		public void LoadForeignNoteXml (string foreignNoteXml, ChangeType changeType)
  		{
  			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;
  
  			// Remove tags now, since a note with no tags has
  			// no "tags" element in the XML
  			List<Tag> newTags = new List<Tag> ();
  
  			using (var xml = new XmlTextReader (new StringReader (foreignNoteXml)) {Namespaces = false}) {
  				DateTime date;
  	
  				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":
  							if (DateTime.TryParse (xml.ReadString (), out date))
  								data.Data.ChangeDate = date;
  							else
  								data.Data.ChangeDate = DateTime.Now;
  							break;
  						case "last-metadata-change-date":
  							if (DateTime.TryParse (xml.ReadString (), out date))
  								data.Data.MetadataChangeDate = date;
  							else
  								data.Data.MetadataChangeDate = DateTime.Now;
  							break;
  						case "create-date":
  							if (DateTime.TryParse (xml.ReadString (), out date))
  								data.Data.CreateDate = date;
  							else
  								data.Data.CreateDate = DateTime.Now;
  							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);
  								newTags.Add (tag);
  							}
  							break;
  						case "open-on-startup":
  							bool isStartup;
  							if (bool.TryParse (xml.ReadString (), out isStartup))
  								IsOpenOnStartup = isStartup;
  							break;
  						}
  						break;
  					}
  				}
  			}
  
  			foreach (Tag oldTag in Tags)
  				if (!newTags.Contains (oldTag))
  				    RemoveTag (oldTag);
  			foreach (Tag newTag in newTags)
  				AddTag (newTag);
  
  			// Allow method caller to specify ChangeType (mostly needed by sync)
  			QueueSave (changeType);
  		}
  
  		// 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.Text = value;
  				else
  					Logger.Error ("Setting text content for closed notes not supported");
  			}
  
  		}
  
  		public NoteData Data
  		{
  			get {
  				return data.GetDataSynchronized ();
  			}
  		}
  
  		public DateTime CreateDate
  		{
  			get {
  				return data.Data.CreateDate;
  			}
  		}
  
  		/// <summary>
  		/// Indicates the last time note content data changed.
  		/// Does not include tag/notebook changes (see MetadataChangeDate).
  		/// </summary>
  		public DateTime ChangeDate
  		{
  			get {
  				return data.Data.ChangeDate;
  			}
  		}
  		
  		/// <summary>
  		/// Indicates the last time non-content note data changed.
  		/// This currently only applies to tags/notebooks.
  		/// </summary>
  		public DateTime MetadataChangeDate
  		{
  			get {
  				return data.Data.MetadataChangeDate;
  			}
  		}
  
  		public NoteManager Manager
  		{
  			get {
  				return manager;
  			}
  			set {
  				manager = value;
  			}
  		}
  
  		public NoteTagTable TagTable
  		{
  			get {
  				if (tag_table == null) {
  					// NOTE: Sharing the same TagTable means
  					// that formatting is duplicated between
  					// buffers.
  					tag_table = NoteTagTable.Instance;
  				}
  				return tag_table;
  			}
  		}
  
  		public bool HasBuffer
  		{
  			get {
  				return null != buffer;
  			}
  		}
  
  		public NoteBuffer Buffer
  		{
  			get {
  				if (buffer == null) {
  					Logger.Debug ("Creating Buffer for '{0}'...",
  					data.Data.Title);
  
  					buffer = new NoteBuffer (TagTable, this);
  					data.Buffer = buffer;
  
  					// Listen for further changed signals
  					buffer.Changed += OnBufferChanged;
  					buffer.TagApplied += BufferTagApplied;
  					buffer.TagRemoved += BufferTagRemoved;
  					buffer.MarkSet += OnBufferMarkSet;
  				}
  				return buffer;
  			}
  		}
  
  		public bool HasWindow
  		{
  			get {
  				return null != window;
  			}
  		}
  
  		private Gtk.Widget focusWidget;
  		public bool Enabled
  		{
  			get { return enabled; }
  			set {
  				enabled = value;
  				if (window != null) {
  					if (!enabled)
  						focusWidget = window.Focus;
  					window.Sensitive = enabled;
  					if (enabled)
  						window.Focus = focusWidget;
  				}
  			}
  		}
  
  		public NoteWindow Window
  		{
  			get {
  				if (window == null) {
  					window = new NoteWindow (this);
  					window.Destroyed += WindowDestroyed;
  					window.ConfigureEvent += WindowConfigureEvent;
  					// TODO: What about a disabled set where you can still copy text?
  					window.Editor.Sensitive = Enabled;
  
  					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) {
  
gt: 						if (pin != "" && pin != Uri) {