using System;
using System.Linq;
using System.Linq.Expressions;
using System.Collections.Generic;
namespace System.Web.Mvc.Html {
	public static class HtmlTagExtensions {
		/// <summary>
		/// Create an <see cref="System.Web.Mvc.Html.HtmlTag"/> for a given model property.
		/// </summary>
		/// <typeparam name="TModel"></typeparam>
		/// <typeparam name="TProperty"></typeparam>
		/// <param name="htmlHelper"></param>
		/// <param name="model"></param>
		/// <returns></returns>
		public static HtmlTag<TModel, TProperty> TagFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> model) {
			return new HtmlTag<TModel, TProperty>(htmlHelper, model);
		}

		public static HtmlTag Tag(this HtmlHelper htmlHelper, string name) {
			return new HtmlTag(name);
		}

		public static MarkdownEditor<TModel, TProperty> MarkdownEditorFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> model) {
			return new MarkdownEditor<TModel, TProperty>(htmlHelper, model);
		}

		/// <summary>
		/// Splits a list of tags apart based on a common delimiter.
		/// </summary>
		/// <param name="content"></param>
		/// <param name="delimiter"></param>
		/// <returns></returns>
		public static string[] ToDelimitedArray(this string content, params char[] delimiter) {
			return content.Split(delimiter).Select(x => x.Trim()).ToArray();
		}

		public static string Join(this IEnumerable<string> strings, string separator) {
			return string.Join(separator, strings);
		}

		public static MvcHtmlString ToHtml(this HtmlTag tag) {
			return MvcHtmlString.Create(tag.ToString());
		}
	}

	public class HtmlText {

		/// <summary>
		/// Constructs a new HtmlText
		/// </summary>
		public HtmlText() {
			Encode = true;
		}

		/// <summary>
		/// Constructs a new HtmlText with the given value.
		/// </summary>
		/// <param name="text"></param>
		public HtmlText(string text) : this() {
			Text = text;
		}

		/// <summary>
		/// Construct a new HtmlText with the given encoding and value.
		/// </summary>
		/// <param name="text"></param>
		/// <param name="encode"></param>
		public HtmlText(string text, bool encode) : this(text) {
			Encode = encode;
		}

		/// <summary>
		/// The actual text to write.
		/// </summary>
		public string Text { get; set; }

		/// <summary>
		/// Whether or not to encode the text.
		/// </summary>
		public bool Encode { get; set; }
	}
	/// <summary>
	/// Represents a typesafe html tag that can be used to build complicated html models with
	/// intellisense and encoding, including validation attributes.
	/// </summary>
	public class HtmlTag {
		/// <summary>
		/// The name of the html tag being created
		/// </summary>
		public string Tag { get; set; }

		/// <summary>
		/// The inner text of the Html Tag
		/// </summary>
		public HtmlText HtmlText { get; set; }

		/// <summary>
		/// Specifies whether or not there is a closing tag
		/// </summary>
		public Boolean HasClosingTag { get; set; }

		/// <summary>
		/// Construct a new Html Tag.
		/// </summary>
		public HtmlTag() {
			// default the closing tag value
			HasClosingTag = true;
			// construct the list of children tags
			Children = new List<HtmlTag>();
			// construct the styles list
			Styles = new Dictionary<string, string>();
			// construct the classes list
			Classes = new List<string>();
			// construct the attributes 
			Attributes = new Dictionary<string, string>();
			// construct the meta data
			MetaData = new Dictionary<string, string>();
			// construct the data binding collection
			DataBind = new Dictionary<string, string>();
		}

		/// <summary>
		/// Construct a new Html Tag of the given type.
		/// </summary>
		/// <param name="tag">The html tag to create.</param>
		public HtmlTag(string tag)
			: this() {
			// set the html tag property, make sure to always
			// use the lowercase variant just for consistency.
			this.Tag = tag.ToLower();
		}

		/// <summary>
		/// Construct a new Html Tag as a child of the given tag.
		/// </summary>
		/// <param name="tag"></param>
		/// <param name="parent"></param>
		public HtmlTag(string tag, HtmlTag parent)
			: this(tag) {
			if (parent != null) parent.Append(this);
		}

		/// <summary>
		/// Construct a new Html Tag with the given validation attributes.
		/// </summary>
		/// <param name="validationAttributes">
		/// The validation attributes to include.
		/// </param>
		public HtmlTag(IDictionary<string, object> validationAttributes)
			: this() {
			this.ValidationAttributes = validationAttributes;
		}

		/// <summary>
		/// Construct a new Html Tag with the given validation attributes and model metadata.
		/// </summary>
		/// <param name="validationAttributes">
		/// The validation attributes to include.
		/// </param>
		/// <param name="metadata">
		/// The model metadata to include
		/// </param>
		public HtmlTag(ModelMetadata metadata, IDictionary<string, object> validationAttributes)
			: this(validationAttributes) {
			this.ModelMetadata = metadata;
		}

		/// <summary>
		/// Set the html tag type.
		/// </summary>
		/// <param name="tag"></param>
		/// <returns></returns>
		public HtmlTag Of(string tag) {
			Tag = tag;
			return this;
		}

		/// <summary>
		/// Sets the 'type' Html Attribute.
		/// </summary>
		/// <param name="type"></param>
		/// <returns></returns>
		public HtmlTag Type(string type) {
			return Attribute("type", type);
		}

		/// <summary>
		/// Sets the 'Id' Html Attribute.
		/// </summary>
		/// <param name="id">
		/// The value of the id
		/// </param>
		/// <returns></returns>
		public HtmlTag Id(string id) {
			return Attribute("id", id);
		}

		/// <summary>
		/// Sets the 'Name' Html Attribute.
		/// </summary>
		/// <param name="name">
		/// The value of the name.
		/// </param>
		/// <returns></returns>
		public HtmlTag Name(string name) {
			return Attribute("name", name);
		}

		/// <summary>
		/// Specify the inner Html text
		/// </summary>
		/// <param name="text"></param>
		/// <returns></returns>
		public HtmlTag Text(string text) {
			this.HtmlText = new HtmlText(text); return this;
		}

		/// <summary>
		/// Specify the inner html text and encoding.
		/// </summary>
		/// <param name="text"></param>
		/// <param name="encode"></param>
		/// <returns></returns>
		public HtmlTag Text(string text, bool encode) {
			this.HtmlText = new HtmlText(text, encode); return this;
		}

		/// <summary>
		/// Specifies whether or not the tag closes itself.
		/// </summary>
		/// <param name="value"></param>
		/// <returns></returns>
		public HtmlTag Closes(bool value) {
			this.HasClosingTag = value; return this;
		}

		/// <summary>
		/// Set a specific cascading stylesheet style value.
		/// </summary>
		/// <param name="key">The style to set.</param>
		/// <param name="value">The value of the style.</param>
		/// <returns></returns>
		public HtmlTag Style(string key, string value) {
			Styles[key] = value;
			return this;
		}

		/// <summary>
		/// Set a specific html attribute for the tag.
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		/// <returns></returns>
		public HtmlTag Attribute(string key, string value) {
			Attributes[key] = value;
			return this;
		}

		/// <summary>
		/// Adds a cascading stylesheet class to the tag.
		/// </summary>
		/// <param name="css"></param>
		/// <returns></returns>
		public HtmlTag Class(string css) {
			if (!Classes.Contains(css))
				Classes.Add(css);
			return this;
		}

		/// <summary>
		/// Adds a data-binding attribute.
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		/// <returns></returns>
		public HtmlTag Bind(string key, string value) {
			DataBind[key] = value; return this;
		}

		/// <summary>
		/// Inserts a sibling tag immediately after the current tag. Any existing sibling will follow the inserted tag.
		/// </summary>
		/// <param name="tag">The tag to add as a sibling</param>
		/// <returns>The original tag</returns>
		public HtmlTag After(HtmlTag tag) {
			Next = tag; return this;
		}

		/// <summary>
		/// Inserts a sibling tag immediately after the current tag. Any existing sibling will follow the inserted tag.
		/// </summary>
		/// <param name="configuration"></param>
		/// <returns></returns>
		public HtmlTag After(Action<HtmlTag> configuration) {
			Next = new HtmlTag(); configuration(Next); return this;
		}

		/// <summary>
		/// Inserts a parent tag immediately before the current tag.
		/// </summary>
		/// <param name="configuration"></param>
		/// <returns></returns>
		public HtmlTag Before(Action<HtmlTag> configuration) {
			Previous = new HtmlTag(); configuration(Previous); return this;
		}

		/// <summary>
		/// Creates nested child tags and returns the innermost tag. Use <see cref="Append(string)"/> if you want to return the parent tag.
		/// </summary>
		/// <param name="tagNames">One or more HTML element names separated by a <c>/</c> or <c>></c></param>
		/// <returns>The innermost tag that was newly added</returns>
		public HtmlTag Add(string tags) {
			return tags
				.ToDelimitedArray('/', '>')
				.Aggregate(this, (parent, tag) => new HtmlTag(tag, parent));
		}

		/// <summary>
		/// Creates nested child tags and returns the innermost tag after running <paramref name="configuration"/> on it. Use <see cref="Append(string, Action{HtmlTag})"/> if you want to return the parent tag.
		/// </summary>
		/// <param name="tags">One or more HTML element names separated by a <c>/</c> or <c>></c></param>
		/// <param name="configuration">Modifications to perform on the newly added innermost tag</param>
		/// <returns>The innermost tag that was newly added</returns>
		public HtmlTag Add(string tags, Action<HtmlTag> configuration) {
			var element = Add(tags); configuration(element); return this;
		}

		/// <summary>
		/// Creates a tag of <typeparamref name="T"/> and adds it as a child. Returns the created child tag.
		/// </summary>
		/// <typeparam name="T">The type of <see cref="HtmlTag"/> to create</typeparam>
		/// <returns>The created child tag</returns>
		public T Add<T>() where T : HtmlTag, new() {
			var child = new T(); Children.Add(child); return child;
		}

		/// <summary>
		/// Adds the given tag as the last child of the parent, and returns the parent.
		/// </summary>
		/// <param name="child">The tag to add as a child of the parent.</param>
		/// <returns>The parent tag</returns>
		public HtmlTag Append(HtmlTag child) {
			Children.Add(child); return this;
		}

		/// <summary>
		/// Creates nested child tags and returns the tag on which the method was called. Use <see cref="Add(string)"/> if you want to return the innermost tag.
		/// </summary>
		/// <param name="tags">One or more HTML element names separated by a <c>/</c> or <c>></c></param>
		/// <returns>The instance on which the method was called (the parent of the new tags)</returns>
		public HtmlTag Append(string tags) {
			Add(tags); return this;
		}

		/// <summary>
		/// Creates nested child tags, runs <paramref name="configuration"/> on the innermost tag, and returns the tag on which the method was called. Use <see cref="Add(string, Action{HtmlTag})"/> if you want to return the innermost tag.
		/// </summary>
		/// <param name="tags"></param>
		/// <param name="configuration"></param>
		/// <returns>The parent tag</returns>
		public HtmlTag Append(string tags, Action<HtmlTag> configuration) {
			Add(tags, configuration); return this;
		}

		/// <summary>
		/// Adds a sequence of tags as children of the current tag. Returns the parent tag.
		/// </summary>
		/// <param name="tags">A sequence of tags to add as children.</param>
		/// <returns>The parent tag</returns>
		public HtmlTag Append(IEnumerable<HtmlTag> tags) {
			Children.AddRange(tags); return this;
		}

		public override string ToString() {
			return ToString(new System.Web.UI.HtmlTextWriter(new System.IO.StringWriter(), string.Empty) { NewLine = string.Empty });
		}

		public string ToString(System.Web.UI.HtmlTextWriter html) {
			WriteHtml(html); return html.InnerWriter.ToString();
		}

		internal virtual void WriteHtml(System.Web.UI.HtmlTextWriter html) {
			if (Previous != null)
				Previous.WriteHtml(html);

			// first, construct a tag builder
			var htmlTag = new TagBuilder(Tag);

			// assemble data bindings if neccessary.
			if (!DataBind.IsEmpty()) {
				// build the data-binding
				var bindings = new System.Text.StringBuilder();
				// build a full string of all data bindings
				DataBind.Each(data => {
					// insert each binding, separated by a comma, parsed by a colon
					bool last = data.Equals(DataBind.Last()) ? true : false;
					bindings.AppendFormat("{0}:{1}{2}", data.Key, data.Value, last ? null : ",");
				});

				// add the data binding attribute with all of the appropriate bindings
				Attribute("data-bind", bindings.ToString());
			}

			// build the html attributes for the tag
			htmlTag.MergeAttributes(Attributes);
			// build the data attributes for the tag
			htmlTag.MergeAttributes(ValidationAttributes);
			// build the stylesheet data
			htmlTag.MergeAttributes(Styles);

			// we have used the tag builder to do our merging and formatting, 
			// now we can use it to begin drawing actual html.
			htmlTag.Attributes.Each(attribute => {
				html.AddAttribute(attribute.Key, attribute.Value);
			});

			// add all of the css classes to the html writer.
			if (Classes.Count > 0) {
				var classValue = Classes.Join(" ");
				html.AddAttribute("class", classValue);
			}

			// Draw the Beginning Tag
			html.RenderBeginTag(Tag);

			// if there is any inner text, write it now
			WriteInnerText(html);

			// Draw each Child Tag
			Children.Each(child => child.WriteHtml(html));

			// if there is a closing tag to be written, draw it
			// now
			if (HasClosingTag)
				html.RenderEndTag();

			if (Next != null)
				Next.WriteHtml(html);
		}

		private void WriteInnerText(System.Web.UI.HtmlTextWriter html) {
			// if there is no inner text, just exit.
			if (HtmlText == null)
				return;

			if (String.IsNullOrEmpty(HtmlText.Text))
				return;

			if (HtmlText.Encode)
				html.WriteEncodedText(HtmlText.Text);
			else
				html.Write(HtmlText.Text);
		}
		/// <summary>
		/// The unobtrusive validation attributes that apply to this html tag, from the
		/// model metadata.
		/// </summary>
		protected IDictionary<string, object> ValidationAttributes { get; set; }
		/// <summary>
		/// The various cascading stylesheet styles to include.
		/// </summary>
		protected IDictionary<string, string> Styles { get; set; }
		/// <summary>
		/// The various cascading stylesheet classes to include.
		/// </summary>
		protected List<string> Classes { get; set; }
		/// <summary>
		/// The html attributes that belong to the tag.
		/// </summary>
		protected IDictionary<string, string> Attributes { get; set; }
		/// <summary>
		/// The html5 meta data that is attached to the tag.
		/// </summary>
		protected IDictionary<string, string> MetaData { get; set; }
		/// <summary>
		/// The html5 data-binding
		/// </summary>
		protected IDictionary<string, string> DataBind { get; set; }

		/// <summary>
		/// The html children of this tag that should be rendered with it.
		/// </summary>
		protected List<HtmlTag> Children { get; set; }

		/// <summary>
		/// The Mvc Model Meta Data.
		/// </summary>
		protected ModelMetadata ModelMetadata { get; set; }

		/// <summary>
		/// The next Html Tag in the sequence of rendering, if applicable.
		/// </summary>
		protected HtmlTag Next { get; set; }

		/// <summary>
		/// The tag to render before this one.
		/// </summary>
		protected HtmlTag Previous { get; set; }
	}
	/// <summary>
	/// Represents a typesafe html tag that can be used to build complicated html models with
	/// intellisense and encoding, including validation attributes.
	/// </summary>
	public class HtmlTag<TModel, TProperty> : HtmlTag {
		/// <summary>
		/// Construct a new Html Tag of the given type.
		/// </summary>
		/// <param name="tag">The html tag to create.</param>
		public HtmlTag(string tag): base(tag) {
		}
		/// <summary>
		/// Construct a new Html Tag with the given HtmlHelper.
		/// </summary>
		/// <param name="helper">The html helper</param>
		public HtmlTag(HtmlHelper<TModel> helper)
			: base() {
			this.Helper = helper;
		}

		/// <summary>
		/// Construct a new Html Tag with the given HtmlHelper from a Model property.
		/// </summary>
		/// <param name="helper">The html helper</param>
		/// <param name="model">The html model</param>
		public HtmlTag(HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> model)
			: this(helper) {
			this.Model = model;
		}

		/// <summary>
		/// Construct a new Html Tag with the given validation attributes and model metadata and view data
		/// </summary>
		/// <param name="validationAttributes">
		/// The validation attributes to include.
		/// </param>
		/// <param name="metadata">
		/// The model metadata to include
		/// </param>
		public HtmlTag(ModelMetadata metadata, IDictionary<string, object> validationAttributes, ViewDataDictionary<TModel> viewData)
			: base(metadata, validationAttributes) {
			this.ViewData = viewData;
		}

		/// <summary>
		/// Construct a new Html Tag with the given validation attributes and model metadata and view data
		/// </summary>
		/// <param name="validationAttributes">
		/// The validation attributes to include.
		/// </param>
		/// <param name="metadata">
		/// The model metadata to include
		/// </param>
		public HtmlTag(ModelMetadata metadata, IDictionary<string, object> validationAttributes, HtmlHelper<TModel> helper)
			: base(metadata, validationAttributes) {
			this.Helper = helper;
			this.ViewData = helper.ViewData;
		}

		/// <summary>
		/// Set the html tag type.
		/// </summary>
		/// <param name="tag"></param>
		/// <returns></returns>
		public HtmlTag<TModel, TProperty> Of(string tag) {
			Tag = tag; return this;
		}

		/// <summary>
		/// Sets the 'type' Html Attribute.
		/// </summary>
		/// <param name="type"></param>
		/// <returns></returns>
		public HtmlTag<TModel, TProperty> Type(string type) {
			return Attribute("type", type);
		}

		/// <summary>
		/// Sets the 'Id' Html Attribute.
		/// </summary>
		/// <param name="id">
		/// The value of the id
		/// </param>
		/// <returns></returns>
		public HtmlTag<TModel, TProperty> Id(string id) {
			return Attribute("id", id);
		}

		/// <summary>
		/// Sets the 'Name' Html Attribute.
		/// </summary>
		/// <param name="name">
		/// The value of the name.
		/// </param>
		/// <returns></returns>
		public HtmlTag<TModel, TProperty> Name(string name) {
			return Attribute("name", name);
		}

		/// <summary>
		/// Set a specific cascading stylesheet style value.
		/// </summary>
		/// <param name="key">The style to set.</param>
		/// <param name="value">The value of the style.</param>
		/// <returns></returns>
		public HtmlTag<TModel, TProperty> Style(string key, string value) {
			Styles[key] = value;
			return this;
		}

		/// <summary>
		/// Set a specific html attribute for the tag.
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		/// <returns></returns>
		public HtmlTag<TModel, TProperty> Attribute(string key, string value) {
			Attributes[key] = value;
			return this;
		}

		/// <summary>
		/// Adds a cascading stylesheet class to the tag.
		/// </summary>
		/// <param name="css"></param>
		/// <returns></returns>
		public HtmlTag<TModel, TProperty> Class(string css) {
			if (!Classes.Contains(css))
				Classes.Add(css);
			return this;
		}

		/// <summary>
		/// Adds a data-binding attribute.
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		/// <returns></returns>
		public HtmlTag<TModel, TProperty> Bind(string key, string value) {
			DataBind[key] = value;
			return this;
		}

		public HtmlTag<TModel, TProperty> Validation() {
			// get the field name of the model property
			string fieldName = ExpressionHelper.GetExpressionText(Model);
			// decompile the model metadata so that we can have access to the attributes.
			ModelMetadata metadata = ModelMetadata.FromLambdaExpression(Model, Helper.ViewData);
			// retrieve the unobtrusive validation attributes
			this.ValidationAttributes = Helper.GetUnobtrusiveValidationAttributes(fieldName, metadata);
			// return the html tag
			return this;
		}

		public HtmlTag<TModel, TProperty> ValidationFor(Expression<Func<TModel, TProperty>> model) {
			// get the field name of the model property
			string fieldName = ExpressionHelper.GetExpressionText(model);
			// decompile the model metadata so that we can have access to the attributes.
			ModelMetadata metadata = ModelMetadata.FromLambdaExpression(model, Helper.ViewData);
			// retrieve the unobtrusive validation attributes
			this.ValidationAttributes = Helper.GetUnobtrusiveValidationAttributes(fieldName, metadata);
			// return the html tag
			return this;
		}

		/// <summary>Gets the strongly typed view data dictionary.</summary>
		/// <returns>The strongly typed view data dictionary.</returns>
		protected ViewDataDictionary<TModel> ViewData { get; set; }

		/// <summary>
		/// The HtmlHelper that created this tag.
		/// </summary>
		protected HtmlHelper<TModel> Helper { get; set; }

		/// <summary>
		/// The Html Model used to Associate this Tag.
		/// </summary>
		protected Expression<Func<TModel, TProperty>> Model { get; set; }
	}

	public class MarkdownEditor<TModel, TProperty> : HtmlTag<TModel, TProperty> {

		public MarkdownEditor(ModelMetadata metadata, IDictionary<string, object> validationAttributes, HtmlHelper<TModel> helper)
			: base(metadata, validationAttributes, helper) {
		}

		/// <summary>
		/// Sets the <strong>Id</strong> of the Markdown preview pane.
		/// </summary>
		/// <param name="name">The html element id of the preview pane to create.</param>
		/// <returns>
		/// The preview pane will be accessed using the <em>#(selector)</em> behavior by <em>jQuery</em>.
		/// </returns>
		public HtmlTag<TModel, TProperty> Preview(string name)  {
			this.PreviewPane = name; return this;
		}

		/// <summary>
		/// Construct a new Html Tag with the given HtmlHelper from a Model property.
		/// </summary>
		/// <param name="helper">The html helper</param>
		/// <param name="model">The html model</param>
		public MarkdownEditor(HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> model): base(helper, model) {
		}

		internal override void WriteHtml(System.Web.UI.HtmlTextWriter html) {
			// ensure that this textarea will be an editor
			Of("textarea");
			Class("mdd_editor");
			Attribute("data-mdd-preview", String.Format("#{0}", this.PreviewPane));

			// add the extra data that has to enclose the editor
			Before(tag => tag.Of("div").Class("mdd_toolbar"))
				.After(tag => tag.Of("div").Class("mdd_resizer"))
				.After(tag => tag.Of("div").Id(PreviewPane));

			base.WriteHtml(html);
		}

		public string PreviewPane { get; set; }
	}

	public class HtmlSelectTag<TModel, TProperty> : HtmlTag<TModel, TProperty> {
		public HtmlSelectTag():base("select") {
		}
		/// <summary>
		/// Construct a new Html Tag with the given HtmlHelper from a Model property.
		/// </summary>
		/// <param name="helper">The html helper</param>
		/// <param name="model">The html model</param>
		public HtmlSelectTag(HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> model): base(helper, model) {
			Of("select");
		}

		public HtmlSelectTag<TModel, TProperty> Options(IEnumerable<SelectListItem> options) {
			options.Each(option => {
							 Option(option.Text, option.Value);
						 });
			return this;
		}
		public HtmlSelectTag<TModel, TProperty> Option(string display, string value) {
			Append(MakeOption(display, value)); return this;
		}

		private static HtmlTag MakeOption(string display, string value) {
			return new HtmlTag("option")
				.Text(display)
				.Attribute("value", value);
		}
	}
}