//------------------------------------------------------------------------------
// <copyright company="Telligent Systems">
//     Copyright (c) Telligent Systems Corporation.  All rights reserved.
// </copyright> 
//------------------------------------------------------------------------------

using System;
using System.Text;
using System.Xml;
using System.Collections;
using System.Text.RegularExpressions;

namespace CommunityServer.Components
{
	public class HtmlNestingCorrectionModule : ICSModule
	{
		private Regex regex = new Regex( "<[^>]+>",RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline);
		private Hashtable closeBeforeNext = new Hashtable();
		private Hashtable selfContainedTags = new Hashtable();

		public HtmlNestingCorrectionModule()
		{
		}

		#region ICSModule Members

		public void Init(CSApplication csa, XmlNode node)
		{
			foreach (XmlNode childNode in node.ChildNodes)
			{
				if (childNode.Name == "CloseBeforeNext")
				{
					ArrayList parents = null;
					if (childNode.Attributes["Parents"] != null && !Globals.IsNullorEmpty(childNode.Attributes["Parents"].Value))
					{
						parents = new ArrayList();
						foreach (string tag in childNode.Attributes["Parents"].Value.Split(','))
						{
							parents.Add(tag);
						}
					}

					closeBeforeNext.Add(childNode.Attributes["Tag"].Value.ToLower(), parents);
				}
				else if (childNode.Name == "SelfContained")
				{
					selfContainedTags.Add(childNode.Attributes["Tag"].Value.ToLower(), true);
				}
			}

			csa.PrePostUpdate += new CSPostEventHandler(csa_PrePostUpdate);
		}

		#endregion

		private void csa_PrePostUpdate(IContent content, CSPostEventArgs e)
		{
			Post post = content as Post;
			if(post == null || post.PostType == PostContentType.Text || !(e.State == ObjectState.Create || e.State == ObjectState.Update))
				return;

			post.FormattedBody = CorrectNesting(post.FormattedBody);
			if (post.HasPoll)
				post.PollDescription = CorrectNesting(post.PollDescription);
		}

		public string CorrectNesting(string html)
		{
			Stack tags = new Stack();
			int lastEndIndex = 0;
			StringBuilder output = new StringBuilder();
			Match match = regex.Match(html);
			Tag tag;

			while(match.Value != string.Empty)
			{
				if (lastEndIndex != match.Index)
				{
					// add whatever text was between the tags
					output.Append(html.Substring(lastEndIndex, match.Index - lastEndIndex));
				}
				
				tag = new Tag(match.Value, selfContainedTags);
				switch (tag.Type)
				{
					case TagType.Start:

						// if this is the start of a new "close before next" tag, close any previous instance of this tag
						if (closeBeforeNext.ContainsKey(tag.TagName) && tags.Contains(tag.TagName) && RequiresClose(tags, tag.TagName))
						{
							while (tags.Peek().ToString() != tag.TagName)
							{
								output.Append("</");
								output.Append(tags.Pop().ToString());
								output.Append(">");
							}

							output.Append("</");
							output.Append(tags.Pop().ToString());
							output.Append(">");			
						}

						// note the tag's start
						tags.Push(tag.TagName);
						output.Append(tag.OuterHtml);
						break;

					case TagType.End:
						// if the end tag doesn't match a start tag, try to close it by looking at the currently unclosed tags
						if (tags.Count > 0 && tags.Peek().ToString() == tag.TagName)
						{
							tags.Pop();
							output.Append(tag.OuterHtml);
						}
						else if (tags.Contains(tag.TagName) && (!closeBeforeNext.ContainsKey(tag.TagName) || RequiresClose(tags, tag.TagName)))
						{
							while (tags.Peek().ToString() != tag.TagName)
							{
								output.Append("</");
								output.Append(tags.Pop().ToString());
								output.Append(">");
							}

							tags.Pop();
							output.Append(tag.OuterHtml);
						}

						break;

					case TagType.SelfContained:
						// self-contained tags are safe
						output.Append(tag.OuterHtml);
						break;
				}

				//Get the next match
				lastEndIndex = match.Index + match.Length;
				match = regex.Match(html, lastEndIndex);
			}

			// add whatever text exists after the last tag
			if (lastEndIndex < html.Length)
				output.Append(html.Substring(lastEndIndex));

			// close any tags left open
			while (tags.Count > 0)
			{
				output.Append("</");
				output.Append(tags.Pop().ToString());
				output.Append(">");
			}

			return output.ToString();
		}

		private bool RequiresClose(Stack tags, string tagName)
		{
			ArrayList parents = (ArrayList) closeBeforeNext[tagName];
			if (parents == null || parents.Count == 0)
				return true;

			tags = (Stack) tags.Clone();
			while (tags.Peek().ToString() != tagName)
			{
				if (parents.Contains(tags.Pop().ToString()))
					return false;
			}

			return true;
		}

		#region Helper classes

		class Tag
		{
			static Regex regex = new Regex( "[^< />]+",RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline);

			private TagType _type;
			private string _tagName;
			private string _tag;

			public Tag(string tag, Hashtable selfContainedTags)
			{
				_tag = tag;

				if (tag.Substring(0, 2) == "</")
					_type = TagType.End;
				else if (tag.Substring(tag.Length - 2, 2) == "/>")
					_type = TagType.SelfContained;
				else
					_type = TagType.Start;

				Match match = regex.Match(tag);
				_tagName = match.Value.ToLower();

				// convert the tag name to lower case (xhtml-friendly)
				if (match.Value != String.Empty)
					_tag = _tag.Substring(0, match.Index) + _tagName + _tag.Substring(match.Index + match.Length);

				// if this should be self-contained, make it so
				if (selfContainedTags.ContainsKey(_tagName) && _type != TagType.SelfContained)
				{
					_tag = _tag.Replace(">", " />");
					_type = TagType.SelfContained;
				}
			}

			public string TagName
			{
				get
				{
					return _tagName;
				}
			}

			public TagType Type
			{
				get
				{
					return _type;
				}
			}

			public string OuterHtml
			{
				get
				{
					return _tag;
				}
			}
		}

		enum TagType
		{
			Start,
			End,
			SelfContained
		}

		#endregion
	}
}
