// WideMargin. Simple fast bible software.
// Copyright (C) 2011  Daniel Hughes
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Linq;
using Gtk;
using WideMargin.MVCInterfaces;
using System.Collections.Generic;
using WideMargin.Utilities;

namespace WideMargin.GUI
{
	/// <summary>
	/// TextView which supports basic XML style markup.
	/// </summary>
	[System.ComponentModel.ToolboxItem(true)]
	public partial class RichTextView : Gtk.Bin
	{
		private EventLocker _eventLocker = new EventLocker();
		private EventHandler<LinkClickedEventArgs<IVerseIdentifier>> _verseLinkClicked;
		private EventHandler<LinkClickedEventArgs<string>> _actionLinkClicked;
		private TextView _textView;
		private List<XmlTag> _xmlTags;
		private Dictionary<string, TextTag> _tags = new Dictionary<string, TextTag>();
		private GaplessScrolledWindow _scrolledWindow;
		
		/// <summary>
		/// Constructor
		/// </summary>
		public RichTextView ()
		{
			this.Build ();
			_scrolledWindow = new GaplessScrolledWindow();
			_textView = new TextView();
			_textView.WrapMode = WrapMode.Word;
			_textView.MotionNotifyEvent += HandleTextViewMotionNotifyEvent;
			_scrolledWindow.Add(_textView);
			_scrolledWindow.Vadjustment.ValueChanged += HandleScrollWindowsVadjustmentValueChanged;
			this.Add(_scrolledWindow);
		}
		
		public bool VScrollbarVisible
		{
			get
			{
				return _scrolledWindow.VScrollbar.Visible;	
			}
		}
		
		/// <summary>
		/// Handles the scroll windows vadjustment value changed.
		/// Determines if the scroll bar is at the bottom and requests more
		/// results if it is.
		/// </summary>
		private void HandleScrollWindowsVadjustmentValueChanged (object sender, EventArgs args)
		{
			if(_scrolledWindow.Vadjustment.Upper - _scrolledWindow.Vadjustment.PageSize == _scrolledWindow.Vadjustment.Value)
			{
				ScrollbarAtBottom.Fire(this, EventArgs.Empty);
			}
		}
		
		/// <summary>
		/// Occurs when the mouse moves over the text
		/// If its over a link show the hand cursor.
		/// </summary>
		/// <param name="o">sender</param>
		/// <param name="args">Movement arguments</param>
		private void HandleTextViewMotionNotifyEvent (object o, MotionNotifyEventArgs args)
		{
			int x;
			int	y;
			_textView.WindowToBufferCoords (Gtk.TextWindowType.Widget,
		                                    (int)args.Event.X,
		                                    (int)args.Event.Y,
		                                    out x,
		                                    out y);

			Gtk.TextIter iter = _textView.GetIterAtLocation (x, y);
			bool foundLink = false;
			foreach(TextTag tag in iter.Tags)
			{
				if(tag is LinkTag<IVerseIdentifier>
				   || tag is LinkTag<string>)
				{
					foundLink = true;
					break;
				}
			}
			
			Gdk.Window window = _textView.GetWindow (Gtk.TextWindowType.Text);
			if(!foundLink)
			{
				window.Cursor = new Gdk.Cursor (Gdk.CursorType.Xterm);
				return;
			}
			window.Cursor = new Gdk.Cursor (Gdk.CursorType.Hand2);
		}
		
		/// <summary>
		/// Text including markup
		/// </summary>
		public string Text
		{
			set
			{
				Clear();
				_textView.Buffer.Text = new string(ParseMarkup(value, 0).ToArray());
				
				ParseTags();
			}
		}
		
		public void Clear()
		{
			//clean up existing tags
			ClearTags(); 
			_textView.Buffer.Clear ();
		}
		
		private void ClearTags()
		{
			TextBuffer buffer = _textView.Buffer;
			buffer.RemoveAllTags(buffer.GetIterAtOffset (0),
		   						 buffer.GetIterAtOffset (buffer.Text.Length - 1));
			List<TextTag> tagsToRemove = new List<TextTag>();
			buffer.TagTable.Foreach(tag => tagsToRemove.Add(tag));
			foreach(TextTag tag in tagsToRemove)
			{
				buffer.TagTable.Remove(tag);
			}
			_tags.Clear ();
		}

		public void AppendText(string text)
		{
			int startupIndex = _textView.Buffer.Text.Length;
			var end = _textView.Buffer.EndIter;
			_textView.Buffer.Insert(ref end, new string(ParseMarkup(text, startupIndex).ToArray()));
			ParseTags();
		}
		
		/// <summary>
		/// Adds the tags to the TextView so to display the markup
		/// </summary>
		private void ParseTags()
		{
			foreach(XmlTag tag in _xmlTags)
			{
				if(tag.Name == "ActionLink")
				{
					string target = tag.Attributes["target"];
					TextTag textTag;
					if(!_tags.TryGetValue(target, out textTag))
					{
						LinkTag<string> linkTag = new LinkTag<string>(target, target);
						linkTag.LinkClicked += HandleActionLinkTagClicked;
						textTag = linkTag;
						AddTag(textTag, target);
					}
					
					ApplyTag(textTag, tag.StartingIndex, tag.EndIndex);
					
				}
				if(tag.Name == "VerseLink")
				{
					string target = tag.Attributes["target"];
					TextTag textTag;
					if(!_tags.TryGetValue(target, out textTag))
					{
						IVerseIdentifier verse = new VerseIdentifier(target);
						LinkTag<IVerseIdentifier> linkTag = new LinkTag<IVerseIdentifier>(target, verse);
						linkTag.LinkClicked += HandleTagLinkClicked;
						textTag = linkTag;
						AddTag(textTag, target);
					}
					
					ApplyTag(textTag, tag.StartingIndex, tag.EndIndex);
				}
				if(tag.Name == "Center")
				{
					TextTag textTag;
					if(!_tags.TryGetValue("Center", out textTag))
					{
						textTag = new TextTag("Center");
						textTag.Justification = Justification.Center;
						AddTag(textTag, "Center");
					}
					
					ApplyTag(textTag, tag.StartingIndex, tag.EndIndex);
				}
				if(tag.Name == "Size")
				{
					TextTag textTag;
					string val = tag.Attributes["value"];
					string name = String.Format("Size{0}", val);
					if(!_tags.TryGetValue(name, out textTag))
					{
						textTag = new TextTag(name);
						//textTag.Size = int.Parse(val);
						textTag.SizePoints = double.Parse(val);
						AddTag(textTag, name);
					}
					
					ApplyTag(textTag, tag.StartingIndex, tag.EndIndex);
				}
				if(tag.Name == "Bold")
				{
					TextTag textTag;
					if(!_tags.TryGetValue("Bold", out textTag))
					{
						textTag = new TextTag("Bold");
						textTag.Weight = Pango.Weight.Bold;
						AddTag(textTag, "Bold");
					}
					
					ApplyTag(textTag, tag.StartingIndex, tag.EndIndex);	
				}
			}
		}

		/// <summary>
		/// A verse link has been cliked, Passes the event up to the controller.
		/// </summary>
		private void HandleActionLinkTagClicked (object sender, LinkClickedEventArgs<string> e)
		{
			_actionLinkClicked.Fire(this, e);
		}

		/// <summary>
		/// A verse link has been cliked, Passes the event up to the controller.
		/// </summary>
		private void HandleTagLinkClicked (object sender, LinkClickedEventArgs<IVerseIdentifier> e)
		{
			_verseLinkClicked.Fire(this, e);
		}
		
		/// <summary>
		/// Parses the markup extracting the tags and returning the raw text
		/// </summary>
		/// <param name="markup">text containing markup</param>
		/// <returns>markup free text</returns>
		private IEnumerable<char> ParseMarkup(string markup, int startIndex)
		{
			_xmlTags = new List<XmlTag>();
			Stack<XmlTag> openTags = new Stack<XmlTag>();
			IEnumerator<char> enumerator = markup.GetEnumerator();
			int index = startIndex -1;
			while(enumerator.MoveNext())
			{
				
				if(enumerator.Current != '<')
				{
					index++;
					yield return enumerator.Current;	
				}
				else
				{
					string rawTag = ReadTag(enumerator);
					if(rawTag[0] == '/')
					{
						XmlTag tagToClose = openTags.Pop();
						tagToClose.EndIndex = index + 1;	
					}
					else
					{
						var tag = new XmlTag(rawTag);
						_xmlTags.Add(tag);
						tag.StartingIndex = index + 1;
						openTags.Push(tag);
					}
				}
			}
		}
		
		/// <summary>
		/// Adds a tag
		/// </summary>
		/// <param name="tag">tag to add</param>
		/// <param name="name">unique name for tag</param>
		private void AddTag(TextTag tag, string name)
		{
			_tags.Add(name, tag);
			_textView.Buffer.TagTable.Add(tag);
		}
		
		/// <summary>
		/// Applies a tag at the given index
		/// </summary>
		/// <param name="tag">tag to apply</param>
		/// <param name="startIndex">start index to apply at</param>
		/// <param name="endIndex">end index to apply at</param>
		private void ApplyTag(TextTag tag, int startIndex, int endIndex)
		{
			TextIter iter1 = _textView.Buffer.GetIterAtOffset (startIndex); 
			TextIter iter2 = _textView.Buffer.GetIterAtOffset (endIndex); 
			_textView.Buffer.ApplyTag(tag, iter1, iter2);
		}
		
		/// <summary>
		/// Reads an xml tag from the enumeration and returns in with the '<' and '>'
		/// </summary>
		/// <param name="enumerator">chacters starting with and opening '<'</param>
		/// <returns>Tag, stuff between '<' and '>'</returns>
		private string ReadTag(IEnumerator<char> enumerator)
		{
			List<char> list = new List<char>();
			while(enumerator.MoveNext())
			{
				char current = enumerator.Current;
				if(current != '>')
				{
					list.Add(current);
				}
				else
				{
					return	new string(list.ToArray());
				}
			}
			throw new FormatException("Tag doesn't end");
		}
		
		/// <summary>
		/// Occurs when a verse link is clicked.
		/// </summary>
		public event EventHandler<LinkClickedEventArgs<IVerseIdentifier>> VerseLinkClicked
		{
			add
			{
				_eventLocker.Add(ref _verseLinkClicked, value);
			}
			remove
			{
				_eventLocker.Remove(ref _verseLinkClicked, value);
			}
		}
		
		/// <summary>
		/// Occurs when a action link is clicked.
		/// </summary>
		public event EventHandler<LinkClickedEventArgs<string>> ActionLinkClicked
		{
			add
			{
				_eventLocker.Add(ref _actionLinkClicked, value);
			}
			remove
			{
				_eventLocker.Remove(ref _actionLinkClicked, value);
			}
		}
		
		public event EventHandler<EventArgs> ScrollbarAtBottom;
	}
}

