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

using System.ComponentModel;
using System.Diagnostics;
using System.Security.Claims;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Razor.Internal;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.AspNetCore.Razor.Runtime.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Mvc.Razor;

/// <summary>
/// Represents properties and methods that are needed in order to render a view that uses Razor syntax.
/// </summary>
public abstract class RazorPageBase : IRazorPage
{
    private readonly Stack<TextWriter> _textWriterStack = new Stack<TextWriter>();
    private readonly IDictionary<string, RenderAsyncDelegate> _sectionWriters = new Dictionary<string, RenderAsyncDelegate>(StringComparer.OrdinalIgnoreCase);
    private StringWriter? _valueBuffer;
    private ITagHelperFactory? _tagHelperFactory;
    private IViewBufferScope? _bufferScope;
    private TextWriter? _pageWriter;
    private AttributeInfo _attributeInfo;
    private TagHelperAttributeInfo _tagHelperAttributeInfo;
    private IUrlHelper? _urlHelper;

    // These fields back properties that are hidden from debugging with DebuggerBrowsableState.Never.
    // Using a field instead of an auto-property allows the value to be seen in the debugger by expanding the "Non-Public members" option.
    private bool _isLayoutBeingRendered;
    private IHtmlContent? _bodyContent;
    private IDictionary<string, RenderAsyncDelegate> _previousSectionWriters = default!;
    private DiagnosticSource _diagnosticSource = default!;
    private HtmlEncoder _htmlEncoder = default!;

    /// <inheritdoc/>
    public virtual ViewContext ViewContext { get; set; } = default!;

    /// <inheritdoc/>
    public string? Layout { get; set; }

    /// <summary>
    /// Gets the <see cref="TextWriter"/> that the page is writing output to.
    /// </summary>
    /// <summary>
    /// Gets the <see cref="TextWriter"/> that the page is writing output to.
    /// </summary>
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public virtual TextWriter Output
    {
        get
        {
            var viewContext = ViewContext;
            if (viewContext == null)
            {
                throw new InvalidOperationException(Resources.FormatViewContextMustBeSet(nameof(ViewContext), nameof(Output)));
            }

            return viewContext.Writer;
        }
    }

    /// <inheritdoc />
    public string Path { get; set; } = default!;

    /// <inheritdoc />
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public IDictionary<string, RenderAsyncDelegate> SectionWriters => _sectionWriters;

    /// <summary>
    /// Gets the dynamic view data dictionary.
    /// </summary>
    public dynamic ViewBag => ViewContext?.ViewBag!;

    /// <inheritdoc />
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public bool IsLayoutBeingRendered
    {
        get => _isLayoutBeingRendered;
        set => _isLayoutBeingRendered = value;
    }

    /// <inheritdoc />
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public IHtmlContent? BodyContent
    {
        get => _bodyContent;
        set => _bodyContent = value;
    }

    /// <inheritdoc />
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public IDictionary<string, RenderAsyncDelegate> PreviousSectionWriters
    {
        get => _previousSectionWriters;
        set => _previousSectionWriters = value;
    }

    /// <summary>
    /// Gets or sets a <see cref="System.Diagnostics.DiagnosticSource"/> instance used to instrument the page execution.
    /// </summary>
    [RazorInject]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public DiagnosticSource DiagnosticSource
    {
        get => _diagnosticSource;
        set => _diagnosticSource = value;
    }

    /// <summary>
    /// Gets the <see cref="System.Text.Encodings.Web.HtmlEncoder"/> to use when this <see cref="RazorPage"/>
    /// handles non-<see cref="IHtmlContent"/> C# expressions.
    /// </summary>
    [RazorInject]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public HtmlEncoder HtmlEncoder
    {
        get => _htmlEncoder;
        set => _htmlEncoder = value;
    }

    /// <summary>
    /// Gets the <see cref="ClaimsPrincipal"/> of the current logged in user.
    /// </summary>
    public virtual ClaimsPrincipal User => ViewContext.HttpContext.User;

    /// <summary>
    /// Gets the <see cref="ITempDataDictionary"/> from the <see cref="ViewContext"/>.
    /// </summary>
    /// <remarks>Returns null if <see cref="ViewContext"/> is null.</remarks>
    public ITempDataDictionary TempData => ViewContext?.TempData!;

    private Stack<TagHelperScopeInfo> TagHelperScopes { get; } = new Stack<TagHelperScopeInfo>();

    private ITagHelperFactory TagHelperFactory
    {
        get
        {
            if (_tagHelperFactory == null)
            {
                var services = ViewContext.HttpContext.RequestServices;
                _tagHelperFactory = services.GetRequiredService<ITagHelperFactory>();
            }

            return _tagHelperFactory;
        }
    }

    private IViewBufferScope BufferScope
    {
        get
        {
            if (_bufferScope == null)
            {
                var services = ViewContext.HttpContext.RequestServices;
                _bufferScope = services.GetRequiredService<IViewBufferScope>();
            }

            return _bufferScope;
        }
    }

    /// <inheritdoc/>
    public abstract Task ExecuteAsync();

    /// <summary>
    /// Format an error message about using an indexer when the tag helper property is <c>null</c>.
    /// </summary>
    /// <param name="attributeName">Name of the HTML attribute associated with the indexer.</param>
    /// <param name="tagHelperTypeName">Full name of the tag helper <see cref="Type"/>.</param>
    /// <param name="propertyName">Dictionary property in the tag helper.</param>
    /// <returns>An error message about using an indexer when the tag helper property is <c>null</c>.</returns>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public string InvalidTagHelperIndexerAssignment(
        string attributeName,
        string tagHelperTypeName,
        string propertyName)
    {
        return Resources.FormatRazorPage_InvalidTagHelperIndexerAssignment(
            attributeName,
            tagHelperTypeName,
            propertyName);
    }

    /// <summary>
    /// Creates and activates a <see cref="ITagHelper"/>.
    /// </summary>
    /// <typeparam name="TTagHelper">A <see cref="ITagHelper"/> type.</typeparam>
    /// <returns>The activated <see cref="ITagHelper"/>.</returns>
    /// <remarks>
    /// <typeparamref name="TTagHelper"/> must have a parameterless constructor.
    /// </remarks>
    public TTagHelper CreateTagHelper<TTagHelper>() where TTagHelper : ITagHelper
    {
        return TagHelperFactory.CreateTagHelper<TTagHelper>(ViewContext);
    }

    /// <summary>
    /// Starts a new writing scope and optionally overrides <see cref="HtmlEncoder"/> within that scope.
    /// </summary>
    /// <param name="encoder">
    /// The <see cref="System.Text.Encodings.Web.HtmlEncoder"/> to use when this <see cref="RazorPage"/> handles
    /// non-<see cref="IHtmlContent"/> C# expressions. If <c>null</c>, does not change <see cref="HtmlEncoder"/>.
    /// </param>
    /// <remarks>
    /// All writes to the <see cref="Output"/> or <see cref="ViewContext.Writer"/> after calling this method will
    /// be buffered until <see cref="EndTagHelperWritingScope"/> is called.
    /// </remarks>
    public void StartTagHelperWritingScope(HtmlEncoder encoder)
    {
        var viewContext = ViewContext;
        var buffer = new ViewBuffer(BufferScope, Path, ViewBuffer.TagHelperPageSize);
        TagHelperScopes.Push(new TagHelperScopeInfo(buffer, HtmlEncoder, viewContext.Writer));

        // If passed an HtmlEncoder, override the property.
        if (encoder != null)
        {
            HtmlEncoder = encoder;
        }

        // We need to replace the ViewContext's Writer to ensure that all content (including content written
        // from HTML helpers) is redirected.
        viewContext.Writer = new ViewBufferTextWriter(buffer, viewContext.Writer.Encoding);
    }

    /// <summary>
    /// Ends the current writing scope that was started by calling <see cref="StartTagHelperWritingScope"/>.
    /// </summary>
    /// <returns>The buffered <see cref="TagHelperContent"/>.</returns>
    public TagHelperContent EndTagHelperWritingScope()
    {
        if (TagHelperScopes.Count == 0)
        {
            throw new InvalidOperationException(Resources.RazorPage_ThereIsNoActiveWritingScopeToEnd);
        }

        var scopeInfo = TagHelperScopes.Pop();

        // Get the content written during the current scope.
        var tagHelperContent = new DefaultTagHelperContent();
        tagHelperContent.AppendHtml(scopeInfo.Buffer);

        // Restore previous scope.
        HtmlEncoder = scopeInfo.HtmlEncoder;
        ViewContext.Writer = scopeInfo.Writer;

        return tagHelperContent;
    }

    /// <summary>
    /// Starts a new scope for writing <see cref="ITagHelper"/> attribute values.
    /// </summary>
    /// <remarks>
    /// All writes to the <see cref="Output"/> or <see cref="ViewContext.Writer"/> after calling this method will
    /// be buffered until <see cref="EndWriteTagHelperAttribute"/> is called.
    /// The content will be buffered using a shared <see cref="StringWriter"/> within this <see cref="RazorPage"/>
    /// Nesting of <see cref="BeginWriteTagHelperAttribute"/> and <see cref="EndWriteTagHelperAttribute"/> method calls
    /// is not supported.
    /// </remarks>
    public void BeginWriteTagHelperAttribute()
    {
        if (_pageWriter != null)
        {
            throw new InvalidOperationException(Resources.RazorPage_NestingAttributeWritingScopesNotSupported);
        }

        var viewContext = ViewContext;
        _pageWriter = viewContext.Writer;

        if (_valueBuffer == null)
        {
            _valueBuffer = new StringWriter();
        }

        // We need to replace the ViewContext's Writer to ensure that all content (including content written
        // from HTML helpers) is redirected.
        viewContext.Writer = _valueBuffer;
    }

    /// <summary>
    /// Ends the current writing scope that was started by calling <see cref="BeginWriteTagHelperAttribute"/>.
    /// </summary>
    /// <returns>The content buffered by the shared <see cref="StringWriter"/> of this <see cref="RazorPage"/>.</returns>
    /// <remarks>
    /// This method assumes that there will be no nesting of <see cref="BeginWriteTagHelperAttribute"/>
    /// and <see cref="EndWriteTagHelperAttribute"/> method calls.
    /// </remarks>
    public string EndWriteTagHelperAttribute()
    {
        if (_pageWriter == null)
        {
            throw new InvalidOperationException(Resources.RazorPage_ThereIsNoActiveWritingScopeToEnd);
        }

        Debug.Assert(_valueBuffer is not null);

        var content = _valueBuffer.ToString();
        _valueBuffer.GetStringBuilder().Clear();

        // Restore previous writer.
        ViewContext.Writer = _pageWriter;
        _pageWriter = null;

        return content;
    }

    /// <summary>
    /// Puts a text writer on the stack.
    /// </summary>
    /// <param name="writer"></param>
    // Internal for unit testing.
    protected internal virtual void PushWriter(TextWriter writer)
    {
        ArgumentNullException.ThrowIfNull(writer);

        var viewContext = ViewContext;
        _textWriterStack.Push(viewContext.Writer);
        viewContext.Writer = writer;
    }

    /// <summary>
    /// Return a text writer from the stack.
    /// </summary>
    /// <returns>The text writer.</returns>
    // Internal for unit testing.
    protected internal virtual TextWriter PopWriter()
    {
        var viewContext = ViewContext;
        var writer = _textWriterStack.Pop();
        viewContext.Writer = writer;
        return writer;
    }

    /// <summary>
    /// Returns a href for the given content path.
    /// </summary>
    /// <param name="contentPath">The content path.</param>
    /// <returns>The href for the contentPath.</returns>
    public virtual string Href(string contentPath)
    {
        ArgumentNullException.ThrowIfNull(contentPath);

        if (_urlHelper == null)
        {
            var viewContext = ViewContext;
            var services = viewContext.HttpContext.RequestServices;
            var factory = services.GetRequiredService<IUrlHelperFactory>();
            _urlHelper = factory.GetUrlHelper(viewContext);
        }

        return _urlHelper.Content(contentPath);
    }

    /// <summary>
    /// Creates a named content section in the page that can be invoked in a Layout page using
    /// <c>RenderSection</c> or <c>RenderSectionAsync</c>
    /// </summary>
    /// <param name="name">The name of the section to create.</param>
    /// <param name="section">The delegate to execute when rendering the section.</param>
    /// <remarks>This is a temporary placeholder method to support ASP.NET Core 2.0.0 editor code generation.</remarks>
    [EditorBrowsable(EditorBrowsableState.Never)]
    protected void DefineSection(string name, Func<object?, Task> section)
        => DefineSection(name, () => section(null /* writer */));

    /// <summary>
    /// Creates a named content section in the page that can be invoked in a Layout page using
    /// <c>RenderSection</c> or <c>RenderSectionAsync</c>
    /// </summary>
    /// <param name="name">The name of the section to create.</param>
    /// <param name="section">The <see cref="RenderAsyncDelegate"/> to execute when rendering the section.</param>
    public virtual void DefineSection(string name, RenderAsyncDelegate section)
    {
        ArgumentNullException.ThrowIfNull(name);
        ArgumentNullException.ThrowIfNull(section);

        if (SectionWriters.ContainsKey(name))
        {
            throw new InvalidOperationException(Resources.FormatSectionAlreadyDefined(name));
        }
        SectionWriters[name] = section;
    }

    /// <summary>
    /// Writes the specified <paramref name="value"/> with HTML encoding to <see cref="Output"/>.
    /// </summary>
    /// <param name="value">The <see cref="object"/> to write.</param>
    public virtual void Write(object? value)
    {
        if (value is null || value == HtmlString.Empty)
        {
            return;
        }

        var writer = Output;
        var encoder = HtmlEncoder;
        if (value is IHtmlContent htmlContent)
        {
            if (writer is ViewBufferTextWriter bufferedWriter)
            {
                if (value is IHtmlContentContainer htmlContentContainer)
                {
                    // This is likely another ViewBuffer.
                    htmlContentContainer.MoveTo(bufferedWriter.Buffer);
                }
                else
                {
                    // Perf: This is the common case for IHtmlContent, ViewBufferTextWriter is inefficient
                    // for writing character by character.
                    bufferedWriter.Buffer.AppendHtml(htmlContent);
                }
            }
            else
            {
                htmlContent.WriteTo(writer, encoder);
            }

            return;
        }

        Write(value.ToString());
    }

    /// <summary>
    /// Writes the specified <paramref name="value"/> with HTML encoding to <see cref="Output"/>.
    /// </summary>
    /// <param name="value">The <see cref="string"/> to write.</param>
    public virtual void Write(string? value)
    {
        var writer = Output;
        var encoder = HtmlEncoder;
        if (!string.IsNullOrEmpty(value))
        {
            // Perf: Encode right away instead of writing it character-by-character.
            // character-by-character isn't efficient when using a writer backed by a ViewBuffer.
            var encoded = encoder.Encode(value);
            writer.Write(encoded);
        }
    }

    /// <summary>
    /// Writes the specified <paramref name="value"/> without HTML encoding to <see cref="Output"/>.
    /// </summary>
    /// <param name="value">The <see cref="object"/> to write.</param>
    public virtual void WriteLiteral(object? value)
    {
        if (value is null)
        {
            return;
        }

        WriteLiteral(value.ToString());
    }

    /// <summary>
    /// Writes the specified <paramref name="value"/> without HTML encoding to <see cref="Output"/>.
    /// </summary>
    /// <param name="value">The <see cref="string"/> to write.</param>
    public virtual void WriteLiteral(string? value)
    {
        if (!string.IsNullOrEmpty(value))
        {
            Output.Write(value);
        }
    }

    /// <summary>
    /// Begins writing out an attribute.
    /// </summary>
    /// <param name="name">The name.</param>
    /// <param name="prefix">The prefix.</param>
    /// <param name="prefixOffset">The prefix offset.</param>
    /// <param name="suffix">The suffix.</param>
    /// <param name="suffixOffset">The suffix offset.</param>
    /// <param name="attributeValuesCount">The attribute values count.</param>
    public virtual void BeginWriteAttribute(
        string name,
        string prefix,
        int prefixOffset,
        string suffix,
        int suffixOffset,
        int attributeValuesCount)
    {
        ArgumentNullException.ThrowIfNull(prefix);
        ArgumentNullException.ThrowIfNull(suffix);

        _attributeInfo = new AttributeInfo(name, prefix, prefixOffset, suffix, suffixOffset, attributeValuesCount);

        // Single valued attributes might be omitted in entirety if it the attribute value strictly evaluates to
        // null  or false. Consequently defer the prefix generation until we encounter the attribute value.
        if (attributeValuesCount != 1)
        {
            WritePositionTaggedLiteral(prefix, prefixOffset);
        }
    }

    /// <summary>
    /// Writes out an attribute value.
    /// </summary>
    /// <param name="prefix">The prefix.</param>
    /// <param name="prefixOffset">The prefix offset.</param>
    /// <param name="value">The value.</param>
    /// <param name="valueOffset">The value offset.</param>
    /// <param name="valueLength">The value length.</param>
    /// <param name="isLiteral">Whether the attribute is a literal.</param>
    public void WriteAttributeValue(
        string prefix,
        int prefixOffset,
        object? value,
        int valueOffset,
        int valueLength,
        bool isLiteral)
    {
        if (_attributeInfo.AttributeValuesCount == 1)
        {
            if (IsBoolFalseOrNullValue(prefix, value))
            {
                // Value is either null or the bool 'false' with no prefix; don't render the attribute.
                _attributeInfo.Suppressed = true;
                return;
            }

            // We are not omitting the attribute. Write the prefix.
            WritePositionTaggedLiteral(_attributeInfo.Prefix, _attributeInfo.PrefixOffset);

            if (IsBoolTrueWithEmptyPrefixValue(prefix, value))
            {
                // The value is just the bool 'true', write the attribute name instead of the string 'True'.
                value = _attributeInfo.Name;
            }
        }

        // This block handles two cases.
        // 1. Single value with prefix.
        // 2. Multiple values with or without prefix.
        if (value != null)
        {
            if (!string.IsNullOrEmpty(prefix))
            {
                WritePositionTaggedLiteral(prefix, prefixOffset);
            }

            BeginContext(valueOffset, valueLength, isLiteral);

            WriteUnprefixedAttributeValue(value, isLiteral);

            EndContext();
        }
    }

    /// <summary>
    /// Ends writing an attribute.
    /// </summary>
    public virtual void EndWriteAttribute()
    {
        if (!_attributeInfo.Suppressed)
        {
            WritePositionTaggedLiteral(_attributeInfo.Suffix, _attributeInfo.SuffixOffset);
        }
    }

    /// <summary>
    /// Begins adding html attribute values.
    /// </summary>
    /// <param name="executionContext">The <see cref="TagHelperExecutionContext"/>.</param>
    /// <param name="attributeName">The name of the attribute.</param>
    /// <param name="attributeValuesCount">The number of attribute values.</param>
    /// <param name="attributeValueStyle">The <see cref="HtmlAttributeValueStyle"/>.</param>
    public void BeginAddHtmlAttributeValues(
        TagHelperExecutionContext executionContext,
        string attributeName,
        int attributeValuesCount,
        HtmlAttributeValueStyle attributeValueStyle)
    {
        _tagHelperAttributeInfo = new TagHelperAttributeInfo(
            executionContext,
            attributeName,
            attributeValuesCount,
            attributeValueStyle);
    }

    /// <summary>
    /// Add an html attribute value.
    /// </summary>
    /// <param name="prefix">The prefix.</param>
    /// <param name="prefixOffset">The prefix offset.</param>
    /// <param name="value">The attribute value.</param>
    /// <param name="valueOffset">The value offset.</param>
    /// <param name="valueLength">The value length.</param>
    /// <param name="isLiteral">Whether the attribute is a literal.</param>
    public void AddHtmlAttributeValue(
        string? prefix,
        int prefixOffset,
        object? value,
        int valueOffset,
        int valueLength,
        bool isLiteral)
    {
        Debug.Assert(_tagHelperAttributeInfo.ExecutionContext != null);
        if (_tagHelperAttributeInfo.AttributeValuesCount == 1)
        {
            if (IsBoolFalseOrNullValue(prefix, value))
            {
                // The first value was 'null' or 'false' indicating that we shouldn't render the attribute. The
                // attribute is treated as a TagHelper attribute so it's only available in
                // TagHelperContext.AllAttributes for TagHelper authors to see (if they want to see why the
                // attribute was removed from TagHelperOutput.Attributes).
                _tagHelperAttributeInfo.ExecutionContext.AddTagHelperAttribute(
                    _tagHelperAttributeInfo.Name,
                    value?.ToString() ?? string.Empty,
                    _tagHelperAttributeInfo.AttributeValueStyle);
                _tagHelperAttributeInfo.Suppressed = true;
                return;
            }
            else if (IsBoolTrueWithEmptyPrefixValue(prefix, value))
            {
                _tagHelperAttributeInfo.ExecutionContext.AddHtmlAttribute(
                    _tagHelperAttributeInfo.Name,
                    _tagHelperAttributeInfo.Name,
                    _tagHelperAttributeInfo.AttributeValueStyle);
                _tagHelperAttributeInfo.Suppressed = true;
                return;
            }
        }

        if (value != null)
        {
            // Perf: We'll use this buffer for all of the attribute values and then clear it to
            // reduce allocations.
            if (_valueBuffer == null)
            {
                _valueBuffer = new StringWriter();
            }

            PushWriter(_valueBuffer);
            if (!string.IsNullOrEmpty(prefix))
            {
                WriteLiteral(prefix);
            }

            WriteUnprefixedAttributeValue(value, isLiteral);
            PopWriter();
        }
    }

    /// <summary>
    /// Ends adding html attribute values.
    /// </summary>
    /// <param name="executionContext">The <see cref="TagHelperExecutionContext"/>.</param>
    public void EndAddHtmlAttributeValues(TagHelperExecutionContext executionContext)
    {
        if (!_tagHelperAttributeInfo.Suppressed)
        {
            // Perf: _valueBuffer might be null if nothing was written. If it is set, clear it so
            // it is reset for the next value.
            var content = _valueBuffer == null ? HtmlString.Empty : new HtmlString(_valueBuffer.ToString());
            _valueBuffer?.GetStringBuilder().Clear();

            executionContext.AddHtmlAttribute(_tagHelperAttributeInfo.Name, content, _tagHelperAttributeInfo.AttributeValueStyle);
        }
    }

    /// <summary>
    /// Invokes <see cref="TextWriter.FlushAsync()"/> on <see cref="Output"/> and <see cref="m:Stream.FlushAsync"/>
    /// on the response stream, writing out any buffered content to the <see cref="HttpResponse.Body"/>.
    /// </summary>
    /// <returns>A <see cref="Task{HtmlString}"/> that represents the asynchronous flush operation and on
    /// completion returns an empty <see cref="IHtmlContent"/>.</returns>
    /// <remarks>The value returned is a token value that allows FlushAsync to work directly in an HTML
    /// section. However the value does not represent the rendered content.
    /// This method also writes out headers, so any modifications to headers must be done before
    /// <see cref="FlushAsync"/> is called. For example, call <see cref="SetAntiforgeryCookieAndHeader"/> to send
    /// antiforgery cookie token and X-Frame-Options header to client before this method flushes headers out.
    /// </remarks>
    public virtual async Task<HtmlString> FlushAsync()
    {
        // If there are active scopes, then we should throw. Cannot flush content that has the potential to change.
        if (TagHelperScopes.Count > 0)
        {
            throw new InvalidOperationException(
                Resources.FormatRazorPage_CannotFlushWhileInAWritingScope(nameof(FlushAsync), Path));
        }

        // Calls to Flush are allowed if the page does not specify a Layout or if it is executing a section in the
        // Layout.
        if (!IsLayoutBeingRendered && !string.IsNullOrEmpty(Layout))
        {
            var message = Resources.FormatLayoutCannotBeRendered(Path, nameof(FlushAsync));
            throw new InvalidOperationException(message);
        }

        await Output.FlushAsync();
        await ViewContext.HttpContext.Response.Body.FlushAsync();
        return HtmlString.Empty;
    }

    /// <summary>
    /// Sets antiforgery cookie and X-Frame-Options header on the response.
    /// </summary>
    /// <returns>An empty <see cref="IHtmlContent"/>.</returns>
    /// <remarks> Call this method to send antiforgery cookie token and X-Frame-Options header to client
    /// before <see cref="RazorPageBase.FlushAsync"/> flushes the headers. </remarks>
    public virtual HtmlString SetAntiforgeryCookieAndHeader()
    {
        var viewContext = ViewContext;
        if (viewContext != null)
        {
            var antiforgery = viewContext.HttpContext.RequestServices.GetRequiredService<IAntiforgery>();
            antiforgery.SetCookieTokenAndHeader(viewContext.HttpContext);
        }
        return HtmlString.Empty;
    }

    private void WriteUnprefixedAttributeValue(object? value, bool isLiteral)
    {
        // The extra branching here is to ensure that we call the Write*To(string) overload where possible.
        if (value is string stringValue)
        {
            if (isLiteral)
            {
                WriteLiteral(stringValue);
            }
            else
            {
                Write(stringValue);
            }
        }
        else if (isLiteral)
        {
            WriteLiteral(value);
        }
        else
        {
            Write(value);
        }
    }

    private void WritePositionTaggedLiteral(string value, int position)
    {
        BeginContext(position, value.Length, isLiteral: true);
        WriteLiteral(value);
        EndContext();
    }

    /// <inheritdoc />
    public abstract void BeginContext(int position, int length, bool isLiteral);

    /// <inheritdoc />
    public abstract void EndContext();

    private static bool IsBoolFalseOrNullValue(string? prefix, object? value)
    {
        return string.IsNullOrEmpty(prefix) &&
            (value is null ||
            (value is bool boolValue && !boolValue));
    }

    private static bool IsBoolTrueWithEmptyPrefixValue(string? prefix, object? value)
    {
        // If the value is just the bool 'true', use the attribute name as the value.
        return string.IsNullOrEmpty(prefix) &&
            (value is bool boolValue && boolValue);
    }

    /// <inheritdoc />
    public abstract void EnsureRenderedBodyOrSections();

    private struct AttributeInfo
    {
        public AttributeInfo(
            string name,
            string prefix,
            int prefixOffset,
            string suffix,
            int suffixOffset,
            int attributeValuesCount)
        {
            Name = name;
            Prefix = prefix;
            PrefixOffset = prefixOffset;
            Suffix = suffix;
            SuffixOffset = suffixOffset;
            AttributeValuesCount = attributeValuesCount;

            Suppressed = false;
        }

        public int AttributeValuesCount { get; }

        public string Name { get; }

        public string Prefix { get; }

        public int PrefixOffset { get; }

        public string Suffix { get; }

        public int SuffixOffset { get; }

        public bool Suppressed { get; set; }
    }

    private struct TagHelperAttributeInfo
    {
        public TagHelperAttributeInfo(
            TagHelperExecutionContext tagHelperExecutionContext,
            string name,
            int attributeValuesCount,
            HtmlAttributeValueStyle attributeValueStyle)
        {
            ExecutionContext = tagHelperExecutionContext;
            Name = name;
            AttributeValuesCount = attributeValuesCount;
            AttributeValueStyle = attributeValueStyle;

            Suppressed = false;
        }

        public string Name { get; }

        public TagHelperExecutionContext ExecutionContext { get; }

        public int AttributeValuesCount { get; }

        public HtmlAttributeValueStyle AttributeValueStyle { get; }

        public bool Suppressed { get; set; }
    }

    private readonly struct TagHelperScopeInfo
    {
        public TagHelperScopeInfo(ViewBuffer buffer, HtmlEncoder encoder, TextWriter writer)
        {
            Buffer = buffer;
            HtmlEncoder = encoder;
            Writer = writer;
        }

        public ViewBuffer Buffer { get; }

        public HtmlEncoder HtmlEncoder { get; }

        public TextWriter Writer { get; }
    }
}
