// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;

namespace Microsoft.AspNetCore.Razor.TagHelpers
{
    /// <summary>
    /// Class used to represent the output of an <see cref="ITagHelper"/>.
    /// </summary>
    public class TagHelperOutput : IHtmlContentContainer
    {
        private readonly Func<bool, HtmlEncoder, Task<TagHelperContent>> _getChildContentAsync;
        private TagHelperContent _preElement;
        private TagHelperContent _preContent;
        private TagHelperContent _content;
        private TagHelperContent _postContent;
        private TagHelperContent _postElement;
        private bool _wasSuppressOutputCalled;

        // Internal for testing
        internal TagHelperOutput(string tagName)
            : this(
                tagName,
                new TagHelperAttributeList(),
                (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()))
        {
        }

        /// <summary>
        /// Instantiates a new instance of <see cref="TagHelperOutput"/>.
        /// </summary>
        /// <param name="tagName">The HTML element's tag name.</param>
        /// <param name="attributes">The HTML attributes.</param>
        /// <param name="getChildContentAsync">
        /// A delegate used to execute children asynchronously with the given <see cref="HtmlEncoder"/> in scope and
        /// return their rendered content.
        /// </param>
        public TagHelperOutput(
            string tagName,
            TagHelperAttributeList attributes,
            Func<bool, HtmlEncoder, Task<TagHelperContent>> getChildContentAsync)
        {
            if (getChildContentAsync == null)
            {
                throw new ArgumentNullException(nameof(getChildContentAsync));
            }

            if (attributes == null)
            {
                throw new ArgumentNullException(nameof(attributes));
            }

            TagName = tagName;
            _getChildContentAsync = getChildContentAsync;
            Attributes = attributes;
        }

        /// <summary>
        /// The HTML element's tag name.
        /// </summary>
        /// <remarks>
        /// A whitespace or <c>null</c> value results in no start or end tag being rendered.
        /// </remarks>
        public string TagName { get; set; }

        /// <summary>
        /// Content that precedes the HTML element.
        /// </summary>
        /// <remarks>Value is rendered before the HTML element.</remarks>
        public TagHelperContent PreElement
        {
            get
            {
                if (_preElement == null)
                {
                    _preElement = new DefaultTagHelperContent();
                }

                return _preElement;
            }
        }

        /// <summary>
        /// The HTML element's pre content.
        /// </summary>
        /// <remarks>Value is prepended to the <see cref="ITagHelper"/>'s final output.</remarks>
        public TagHelperContent PreContent
        {
            get
            {
                if (_preContent == null)
                {
                    _preContent = new DefaultTagHelperContent();
                }

                return _preContent;
            }
        }

        /// <summary>
        /// Get or set the HTML element's main content.
        /// </summary>
        /// <remarks>Value occurs in the <see cref="ITagHelper"/>'s final output after <see cref="PreContent"/> and
        /// before <see cref="PostContent"/></remarks>
        public TagHelperContent Content
        {
            get
            {
                if (_content == null)
                {
                    _content = new DefaultTagHelperContent();
                }

                return _content;
            }
            set
            {
                if (value == null)
                {
                    throw new ArgumentNullException(nameof(value));
                }

                _content = value;
            }
        }

        /// <summary>
        /// The HTML element's post content.
        /// </summary>
        /// <remarks>Value is appended to the <see cref="ITagHelper"/>'s final output.</remarks>
        public TagHelperContent PostContent
        {
            get
            {
                if (_postContent == null)
                {
                    _postContent = new DefaultTagHelperContent();
                }

                return _postContent;
            }
        }

        /// <summary>
        /// Content that follows the HTML element.
        /// </summary>
        /// <remarks>Value is rendered after the HTML element.</remarks>
        public TagHelperContent PostElement
        {
            get
            {
                if (_postElement == null)
                {
                    _postElement = new DefaultTagHelperContent();
                }

                return _postElement;
            }
        }

        /// <summary>
        /// <c>true</c> if <see cref="Content"/> has been set, <c>false</c> otherwise.
        /// </summary>
        public bool IsContentModified
        {
            get
            {
                return _wasSuppressOutputCalled || _content?.IsModified == true;
            }
        }

        /// <summary>
        /// Syntax of the element in the generated HTML.
        /// </summary>
        public TagMode TagMode { get; set; }

        /// <summary>
        /// The HTML element's attributes.
        /// </summary>
        /// <remarks>
        /// MVC will HTML encode <see cref="string"/> values when generating the start tag. It will not HTML encode
        /// a <c>Microsoft.AspNetCore.Mvc.Rendering.HtmlString</c> instance. MVC converts most other types to a
        /// <see cref="string"/>, then HTML encodes the result.
        /// </remarks>
        public TagHelperAttributeList Attributes { get; }

        /// <summary>
        /// Clears the <see cref="TagHelperOutput"/> and updates its state with the provided values.
        /// </summary>
        /// <param name="tagName">The tag name to use.</param>
        /// <param name="tagMode">The <see cref="TagMode"/> to use.</param>
        public void Reinitialize(string tagName, TagMode tagMode)
        {
            TagName = tagName;
            TagMode = tagMode;
            Attributes.Clear();

            _preElement?.Reinitialize();
            _preContent?.Reinitialize();
            _content?.Reinitialize();
            _postContent?.Reinitialize();
            _postElement?.Reinitialize();

            _wasSuppressOutputCalled = false;
        }

        /// <summary>
        /// Changes <see cref="TagHelperOutput"/> to generate nothing.
        /// </summary>
        /// <remarks>
        /// Sets <see cref="TagName"/> to <c>null</c>, and clears <see cref="PreElement"/>, <see cref="PreContent"/>,
        /// <see cref="Content"/>, <see cref="PostContent"/>, and <see cref="PostElement"/> to suppress output.
        /// </remarks>
        public void SuppressOutput()
        {
            TagName = null;
            _wasSuppressOutputCalled = true;
            _preElement?.Clear();
            _preContent?.Clear();
            _content?.Clear();
            _postContent?.Clear();
            _postElement?.Clear();
        }

        /// <summary>
        /// Executes children asynchronously and returns their rendered content.
        /// </summary>
        /// <returns>A <see cref="Task"/> that on completion returns content rendered by children.</returns>
        /// <remarks>
        /// This method is memoized. Multiple calls will not cause children to re-execute with the page's original
        /// <see cref="HtmlEncoder"/>.
        /// </remarks>
        public Task<TagHelperContent> GetChildContentAsync()
        {
            return GetChildContentAsync(useCachedResult: true, encoder: null);
        }

        /// <summary>
        /// Executes children asynchronously and returns their rendered content.
        /// </summary>
        /// <param name="useCachedResult">
        /// If <c>true</c>, multiple calls will not cause children to re-execute with the page's original
        /// <see cref="HtmlEncoder"/>; returns cached content.
        /// </param>
        /// <returns>A <see cref="Task"/> that on completion returns content rendered by children.</returns>
        public Task<TagHelperContent> GetChildContentAsync(bool useCachedResult)
        {
            return GetChildContentAsync(useCachedResult, encoder: null);
        }

        /// <summary>
        /// Executes children asynchronously with the given <paramref name="encoder"/> in scope and returns their
        /// rendered content.
        /// </summary>
        /// <param name="encoder">
        /// The <see cref="HtmlEncoder"/> to use when the page handles non-<see cref="IHtmlContent"/> C# expressions.
        /// If <c>null</c>, executes children with the page's current <see cref="HtmlEncoder"/>.
        /// </param>
        /// <returns>A <see cref="Task"/> that on completion returns content rendered by children.</returns>
        /// <remarks>
        /// This method is memoized. Multiple calls with the same <see cref="HtmlEncoder"/> instance will not cause
        /// children to re-execute with that encoder in scope.
        /// </remarks>
        public Task<TagHelperContent> GetChildContentAsync(HtmlEncoder encoder)
        {
            return GetChildContentAsync(useCachedResult: true, encoder: encoder);
        }

        /// <summary>
        /// Executes children asynchronously with the given <paramref name="encoder"/> in scope and returns their
        /// rendered content.
        /// </summary>
        /// <param name="useCachedResult">
        /// If <c>true</c>, multiple calls with the same <see cref="HtmlEncoder"/> will not cause children to
        /// re-execute; returns cached content.
        /// </param>
        /// <param name="encoder">
        /// The <see cref="HtmlEncoder"/> to use when the page handles non-<see cref="IHtmlContent"/> C# expressions.
        /// If <c>null</c>, executes children with the page's current <see cref="HtmlEncoder"/>.
        /// </param>
        /// <returns>A <see cref="Task"/> that on completion returns content rendered by children.</returns>
        public Task<TagHelperContent> GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder)
        {
            return _getChildContentAsync(useCachedResult, encoder);
        }

        void IHtmlContentContainer.CopyTo(IHtmlContentBuilder destination)
        {
            if (destination == null)
            {
                throw new ArgumentNullException(nameof(destination));
            }

            _preElement?.CopyTo(destination);

            var isTagNameNullOrWhitespace = string.IsNullOrWhiteSpace(TagName);

            if (!isTagNameNullOrWhitespace)
            {
                destination.AppendHtml("<");
                destination.AppendHtml(TagName);

                // Perf: Avoid allocating enumerator, cache .Count as it goes via interface
                var count = Attributes.Count;
                for (var i = 0; i < count; i++)
                {
                    var attribute = Attributes[i];
                    destination.AppendHtml(" ");
                    attribute.CopyTo(destination);
                }

                if (TagMode == TagMode.SelfClosing)
                {
                    destination.AppendHtml(" /");
                }

                destination.AppendHtml(">");
            }

            if (isTagNameNullOrWhitespace || TagMode == TagMode.StartTagAndEndTag)
            {
                _preContent?.CopyTo(destination);

                _content?.CopyTo(destination);

                _postContent?.CopyTo(destination);
            }

            if (!isTagNameNullOrWhitespace && TagMode == TagMode.StartTagAndEndTag)
            {
                destination.AppendHtml("</");
                destination.AppendHtml(TagName);
                destination.AppendHtml(">");
            }

            _postElement?.CopyTo(destination);
        }

        void IHtmlContentContainer.MoveTo(IHtmlContentBuilder destination)
        {
            if (destination == null)
            {
                throw new ArgumentNullException(nameof(destination));
            }

            _preElement?.MoveTo(destination);

            var isTagNameNullOrWhitespace = string.IsNullOrWhiteSpace(TagName);

            if (!isTagNameNullOrWhitespace)
            {
                destination.AppendHtml("<");
                destination.AppendHtml(TagName);

                // Perf: Avoid allocating enumerator, cache .Count as it goes via interface
                var count = Attributes.Count;
                for (var i = 0; i < count; i++)
                {
                    var attribute = Attributes[i];
                    destination.AppendHtml(" ");
                    attribute.MoveTo(destination);
                }

                if (TagMode == TagMode.SelfClosing)
                {
                    destination.AppendHtml(" /");
                }

                destination.AppendHtml(">");
            }

            if (isTagNameNullOrWhitespace || TagMode == TagMode.StartTagAndEndTag)
            {
                _preContent?.MoveTo(destination);
                _content?.MoveTo(destination);
                _postContent?.MoveTo(destination);
            }

            if (!isTagNameNullOrWhitespace && TagMode == TagMode.StartTagAndEndTag)
            {
                destination.AppendHtml("</");
                destination.AppendHtml(TagName);
                destination.AppendHtml(">");
            }

            _postElement?.MoveTo(destination);

            // Depending on the code path we took, these might need to be cleared.
            _preContent?.Clear();
            _content?.Clear();
            _postContent?.Clear();
            Attributes.Clear();
        }

        public void WriteTo(TextWriter writer, HtmlEncoder encoder)
        {
            if (writer == null)
            {
                throw new ArgumentNullException(nameof(writer));
            }

            if (encoder == null)
            {
                throw new ArgumentNullException(nameof(encoder));
            }

            _preElement?.WriteTo(writer, encoder);

            var isTagNameNullOrWhitespace = string.IsNullOrWhiteSpace(TagName);

            if (!isTagNameNullOrWhitespace)
            {
                writer.Write("<");
                writer.Write(TagName);

                // Perf: Avoid allocating enumerator, cache .Count as it goes via interface
                var count = Attributes.Count;
                for (var i = 0; i < count; i++)
                {
                    var attribute = Attributes[i];
                    writer.Write(" ");
                    attribute.WriteTo(writer, encoder);
                }

                if (TagMode == TagMode.SelfClosing)
                {
                    writer.Write(" /");
                }

                writer.Write(">");
            }

            if (isTagNameNullOrWhitespace || TagMode == TagMode.StartTagAndEndTag)
            {
                _preContent?.WriteTo(writer, encoder);

                _content?.WriteTo(writer, encoder);

                _postContent?.WriteTo(writer, encoder);
            }

            if (!isTagNameNullOrWhitespace && TagMode == TagMode.StartTagAndEndTag)
            {
                writer.Write("</");
                writer.Write(TagName);
                writer.Write(">");
            }

            _postElement?.WriteTo(writer, encoder);
        }
    }
}
