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

using System.Diagnostics;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace Microsoft.AspNetCore.Razor.Runtime.TagHelpers;

/// <summary>
/// Class used to store information about a <see cref="ITagHelper"/>'s execution lifetime.
/// </summary>
public class TagHelperExecutionContext
{
    private readonly List<ITagHelper> _tagHelpers;
    private readonly Action<HtmlEncoder> _startTagHelperWritingScope;
    private readonly Func<TagHelperContent> _endTagHelperWritingScope;
    private TagHelperContent _childContent;
    private Func<Task> _executeChildContentAsync;
    private Dictionary<HtmlEncoder, TagHelperContent> _perEncoderChildContent;
    private readonly TagHelperAttributeList _allAttributes;

    /// <summary>
    /// Internal for testing purposes only.
    /// </summary>
    internal TagHelperExecutionContext(string tagName, TagMode tagMode)
        : this(tagName,
               tagMode,
               items: new Dictionary<object, object>(),
               uniqueId: string.Empty,
               executeChildContentAsync: () => Task.CompletedTask,
               startTagHelperWritingScope: _ => { },
               endTagHelperWritingScope: () => new DefaultTagHelperContent())
    {
    }

    /// <summary>
    /// Instantiates a new <see cref="TagHelperExecutionContext"/>.
    /// </summary>
    /// <param name="tagName">The HTML tag name in the Razor source.</param>
    /// <param name="tagMode">HTML syntax of the element in the Razor source.</param>
    /// <param name="items">The collection of items used to communicate with other
    /// <see cref="ITagHelper"/>s</param>
    /// <param name="uniqueId">An identifier unique to the HTML element this context is for.</param>
    /// <param name="executeChildContentAsync">A delegate used to execute the child content asynchronously.</param>
    /// <param name="startTagHelperWritingScope">
    /// A delegate used to start a writing scope in a Razor page and optionally override the page's
    /// <see cref="HtmlEncoder"/> within that scope.
    /// </param>
    /// <param name="endTagHelperWritingScope">A delegate used to end a writing scope in a Razor page.</param>
    public TagHelperExecutionContext(
        string tagName,
        TagMode tagMode,
        IDictionary<object, object> items,
        string uniqueId,
        Func<Task> executeChildContentAsync,
        Action<HtmlEncoder> startTagHelperWritingScope,
        Func<TagHelperContent> endTagHelperWritingScope)
    {
        ArgumentNullException.ThrowIfNull(startTagHelperWritingScope);
        ArgumentNullException.ThrowIfNull(endTagHelperWritingScope);

        _tagHelpers = new List<ITagHelper>();
        _allAttributes = new TagHelperAttributeList();

        Context = new TagHelperContext(tagName, _allAttributes, items, uniqueId);
        Output = new TagHelperOutput(tagName, new TagHelperAttributeList(), GetChildContentAsync)
        {
            TagMode = tagMode
        };

        Reinitialize(tagName, tagMode, items, uniqueId, executeChildContentAsync);

        _startTagHelperWritingScope = startTagHelperWritingScope;
        _endTagHelperWritingScope = endTagHelperWritingScope;
    }

    /// <summary>
    /// Indicates if <see cref="GetChildContentAsync"/> has been called.
    /// </summary>
    public bool ChildContentRetrieved => _childContent != null;

    /// <summary>
    /// Gets the collection of items used to communicate with other <see cref="ITagHelper"/>s.
    /// </summary>
    public IDictionary<object, object> Items { get; private set; }

    /// <summary>
    /// <see cref="ITagHelper"/>s that should be run.
    /// </summary>
    public IList<ITagHelper> TagHelpers => _tagHelpers;

    internal List<ITagHelper> TagHelperList => _tagHelpers;

    // Internal set for testing.
    /// <summary>
    /// The <see cref="ITagHelper"/>'s output.
    /// </summary>
    public TagHelperOutput Output { get; internal set; }

    /// <summary>
    /// The <see cref="ITagHelper"/>'s context.
    /// </summary>
    public TagHelperContext Context { get; }

    /// <summary>
    /// Tracks the given <paramref name="tagHelper"/>.
    /// </summary>
    /// <param name="tagHelper">The tag helper to track.</param>
    public void Add(ITagHelper tagHelper)
    {
        ArgumentNullException.ThrowIfNull(tagHelper);

        _tagHelpers.Add(tagHelper);
    }

    /// <summary>
    /// Tracks the HTML attribute.
    /// </summary>
    /// <param name="name">The HTML attribute name.</param>
    /// <param name="value">The HTML attribute value.</param>
    /// <param name="valueStyle">The value style of the attribute.</param>
    public void AddHtmlAttribute(string name, object value, HtmlAttributeValueStyle valueStyle)
    {
        ArgumentNullException.ThrowIfNull(name);

        var attribute = new TagHelperAttribute(name, value, valueStyle);
        AddHtmlAttribute(attribute);
    }

    /// <summary>
    /// Tracks the HTML attribute.
    /// </summary>
    /// <param name="attribute">The <see cref="TagHelperAttribute"/> to track.</param>
    public void AddHtmlAttribute(TagHelperAttribute attribute)
    {
        ArgumentNullException.ThrowIfNull(attribute);

        Output.Attributes.Add(attribute);
        _allAttributes.Add(attribute);
    }

    /// <summary>
    /// Tracks the <see cref="ITagHelper"/> bound attribute.
    /// </summary>
    /// <param name="name">The bound attribute name.</param>
    /// <param name="value">The attribute value.</param>
    /// <param name="valueStyle">The value style of the attribute.</param>
    public void AddTagHelperAttribute(string name, object value, HtmlAttributeValueStyle valueStyle)
    {
        ArgumentNullException.ThrowIfNull(name);

        var attribute = new TagHelperAttribute(name, value, valueStyle);
        _allAttributes.Add(attribute);
    }

    /// <summary>
    /// Tracks the <see cref="ITagHelper"/> bound attribute.
    /// </summary>
    /// <param name="attribute">The bound attribute.</param>
    public void AddTagHelperAttribute(TagHelperAttribute attribute)
    {
        ArgumentNullException.ThrowIfNull(attribute);

        _allAttributes.Add(attribute);
    }

    /// <summary>
    /// Clears the <see cref="TagHelperExecutionContext"/> 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>
    /// <param name="items">The <see cref="IDictionary{Object, Object}"/> to use.</param>
    /// <param name="uniqueId">The unique id to use.</param>
    /// <param name="executeChildContentAsync">The <see cref="Func{Task}"/> to use.</param>
    public void Reinitialize(
        string tagName,
        TagMode tagMode,
        IDictionary<object, object> items,
        string uniqueId,
        Func<Task> executeChildContentAsync)
    {
        ArgumentNullException.ThrowIfNull(tagName);
        ArgumentNullException.ThrowIfNull(items);
        ArgumentNullException.ThrowIfNull(uniqueId);
        ArgumentNullException.ThrowIfNull(executeChildContentAsync);

        Items = items;
        _executeChildContentAsync = executeChildContentAsync;
        _tagHelpers.Clear();
        _perEncoderChildContent?.Clear();
        _childContent = null;

        Context.Reinitialize(tagName, Items, uniqueId);
        Output.Reinitialize(tagName, tagMode);
    }

    /// <summary>
    /// Executes children asynchronously with the page's <see cref="HtmlEncoder" /> in scope and
    /// sets <see cref="Output"/>'s <see cref="TagHelperOutput.Content"/> to the rendered results.
    /// </summary>
    /// <returns>A <see cref="Task"/> that on completion sets <see cref="Output"/>'s
    /// <see cref="TagHelperOutput.Content"/> to the children's rendered content.</returns>
    public async Task SetOutputContentAsync()
    {
        var childContent = _childContent;

        if (childContent == null)
        {
            _startTagHelperWritingScope(null);
            try
            {
                await _executeChildContentAsync();
            }
            finally
            {
                childContent = _endTagHelperWritingScope();
            }
        }

        Debug.Assert(!Output.IsContentModified);

        Output.Content.SetHtmlContent(childContent);
    }

    // Internal for testing.
    internal async Task<TagHelperContent> GetChildContentAsync(bool useCachedResult, HtmlEncoder encoder)
    {
        // Get cached content for this encoder.
        TagHelperContent childContent;
        if (encoder == null)
        {
            childContent = _childContent;
        }
        else
        {
            if (_perEncoderChildContent == null)
            {
                childContent = null;
                _perEncoderChildContent = new Dictionary<HtmlEncoder, TagHelperContent>();
            }
            else
            {
                _perEncoderChildContent.TryGetValue(encoder, out childContent);
            }
        }

        if (!useCachedResult || childContent == null)
        {
            _startTagHelperWritingScope(encoder);
            try
            {
                await _executeChildContentAsync();
            }
            finally
            {
                childContent = _endTagHelperWritingScope();
            }

            if (encoder == null)
            {
                _childContent = childContent;
            }
            else
            {
                _perEncoderChildContent[encoder] = childContent;
            }
        }

        return new DefaultTagHelperContent().SetHtmlContent(childContent);
    }
}
