// 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.Linq;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.Extensions.DependencyInjection;

namespace Microsoft.AspNetCore.Mvc.Razor;

/// <summary>
/// Default implementation for <see cref="IView"/> that executes one or more <see cref="IRazorPage"/>
/// as parts of its execution.
/// </summary>
[DebuggerDisplay("{Path,nq}")]
public class RazorView : IView
{
    private readonly IRazorViewEngine _viewEngine;
    private readonly IRazorPageActivator _pageActivator;
    private readonly HtmlEncoder _htmlEncoder;
    private readonly DiagnosticListener _diagnosticListener;
    private IViewBufferScope? _bufferScope;

    /// <summary>
    /// Initializes a new instance of <see cref="RazorView"/>
    /// </summary>
    /// <param name="viewEngine">The <see cref="IRazorViewEngine"/> used to locate Layout pages.</param>
    /// <param name="pageActivator">The <see cref="IRazorPageActivator"/> used to activate pages.</param>
    /// <param name="viewStartPages">The sequence of <see cref="IRazorPage" /> instances executed as _ViewStarts.
    /// </param>
    /// <param name="razorPage">The <see cref="IRazorPage"/> instance to execute.</param>
    /// <param name="htmlEncoder">The HTML encoder.</param>
    /// <param name="diagnosticListener">The <see cref="DiagnosticListener"/>.</param>
    public RazorView(
        IRazorViewEngine viewEngine,
        IRazorPageActivator pageActivator,
        IReadOnlyList<IRazorPage> viewStartPages,
        IRazorPage razorPage,
        HtmlEncoder htmlEncoder,
        DiagnosticListener diagnosticListener)
    {
        ArgumentNullException.ThrowIfNull(viewEngine);
        ArgumentNullException.ThrowIfNull(pageActivator);
        ArgumentNullException.ThrowIfNull(viewStartPages);
        ArgumentNullException.ThrowIfNull(razorPage);
        ArgumentNullException.ThrowIfNull(htmlEncoder);
        ArgumentNullException.ThrowIfNull(diagnosticListener);

        _viewEngine = viewEngine;
        _pageActivator = pageActivator;
        ViewStartPages = viewStartPages;
        RazorPage = razorPage;
        _htmlEncoder = htmlEncoder;
        _diagnosticListener = diagnosticListener;
    }

    /// <inheritdoc />
    public string Path => RazorPage.Path;

    /// <summary>
    /// Gets <see cref="IRazorPage"/> instance that the views executes on.
    /// </summary>
    public IRazorPage RazorPage { get; }

    /// <summary>
    /// Gets the sequence of _ViewStart <see cref="IRazorPage"/> instances that are executed by this view.
    /// </summary>
    public IReadOnlyList<IRazorPage> ViewStartPages { get; }

    internal Action<IRazorPage, ViewContext>? OnAfterPageActivated { get; set; }

    /// <inheritdoc />
    public virtual async Task RenderAsync(ViewContext context)
    {
        ArgumentNullException.ThrowIfNull(context);

        // This GetRequiredService call is by design. ViewBufferScope is a scoped service, RazorViewEngine
        // is the component responsible for creating RazorViews and it is a Singleton service. It doesn't
        // have access to the RequestServices so requiring the service when we render the page is the best
        // we can do.
        _bufferScope = context.HttpContext.RequestServices.GetRequiredService<IViewBufferScope>();
        var bodyWriter = await RenderPageAsync(RazorPage, context, invokeViewStarts: true);
        await RenderLayoutAsync(context, bodyWriter);
    }

    private async Task<ViewBufferTextWriter> RenderPageAsync(
        IRazorPage page,
        ViewContext context,
        bool invokeViewStarts)
    {
        var writer = context.Writer as ViewBufferTextWriter;
        if (writer == null)
        {
            Debug.Assert(_bufferScope != null);

            // If we get here, this is likely the top-level page (not a partial) - this means
            // that context.Writer is wrapping the output stream. We need to buffer, so create a buffered writer.
            var buffer = new ViewBuffer(_bufferScope, page.Path, ViewBuffer.ViewPageSize);
            writer = new ViewBufferTextWriter(buffer, context.Writer.Encoding, _htmlEncoder, context.Writer);
        }
        else
        {
            // This means we're writing something like a partial, where the output needs to be buffered.
            // Create a new buffer, but without the ability to flush.
            var buffer = new ViewBuffer(_bufferScope, page.Path, ViewBuffer.ViewPageSize);
            writer = new ViewBufferTextWriter(buffer, context.Writer.Encoding);
        }

        // The writer for the body is passed through the ViewContext, allowing things like HtmlHelpers
        // and ViewComponents to reference it.
        var oldWriter = context.Writer;
        var oldFilePath = context.ExecutingFilePath;

        context.Writer = writer;
        context.ExecutingFilePath = page.Path;

        try
        {
            if (invokeViewStarts)
            {
                // Execute view starts using the same context + writer as the page to render.
                await RenderViewStartsAsync(context);
            }

            await RenderPageCoreAsync(page, context);
            return writer;
        }
        finally
        {
            context.Writer = oldWriter;
            context.ExecutingFilePath = oldFilePath;
        }
    }

    private async Task RenderPageCoreAsync(IRazorPage page, ViewContext context)
    {
        page.ViewContext = context;
        _pageActivator.Activate(page, context);

        OnAfterPageActivated?.Invoke(page, context);

        _diagnosticListener.BeforeViewPage(page, context);

        try
        {
            await page.ExecuteAsync();
        }
        finally
        {
            _diagnosticListener.AfterViewPage(page, context);
        }
    }

    private async Task RenderViewStartsAsync(ViewContext context)
    {
        string? layout = null;
        var oldFilePath = context.ExecutingFilePath;
        try
        {
            for (var i = 0; i < ViewStartPages.Count; i++)
            {
                var viewStart = ViewStartPages[i];
                context.ExecutingFilePath = viewStart.Path;

                // If non-null, copy the layout value from the previous view start to the current. Otherwise leave
                // Layout default alone.
                if (layout != null)
                {
                    viewStart.Layout = layout;
                }

                await RenderPageCoreAsync(viewStart, context);

                // Pass correct absolute path to next layout or the entry page if this view start set Layout to a
                // relative path.
                layout = _viewEngine.GetAbsolutePath(viewStart.Path, viewStart.Layout);
            }
        }
        finally
        {
            context.ExecutingFilePath = oldFilePath;
        }

        // If non-null, copy the layout value from the view start page(s) to the entry page.
        if (layout != null)
        {
            RazorPage.Layout = layout;
        }
    }

    private async Task RenderLayoutAsync(
        ViewContext context,
        ViewBufferTextWriter bodyWriter)
    {
        // A layout page can specify another layout page. We'll need to continue
        // looking for layout pages until they're no longer specified.
        var previousPage = RazorPage;
        var renderedLayouts = new List<IRazorPage>();

        // This loop will execute Layout pages from the inside to the outside. With each
        // iteration, bodyWriter is replaced with the aggregate of all the "body" content
        // (including the layout page we just rendered).
        while (!string.IsNullOrEmpty(previousPage.Layout))
        {
            if (bodyWriter.Flushed)
            {
                // Once a call to RazorPage.FlushAsync is made, we can no longer render Layout pages - content has
                // already been written to the client and the layout content would be appended rather than surround
                // the body content. Throwing this exception wouldn't return a 500 (since content has already been
                // written), but a diagnostic component should be able to capture it.

                var message = Resources.FormatLayoutCannotBeRendered(Path, nameof(Razor.RazorPage.FlushAsync));
                throw new InvalidOperationException(message);
            }

            var layoutPage = GetLayoutPage(context, previousPage.Path, previousPage.Layout);

            if (renderedLayouts.Count > 0 &&
                renderedLayouts.Any(l => string.Equals(l.Path, layoutPage.Path, StringComparison.Ordinal)))
            {
                // If the layout has been previously rendered as part of this view, we're potentially in a layout
                // rendering cycle.
                throw new InvalidOperationException(
                    Resources.FormatLayoutHasCircularReference(previousPage.Path, layoutPage.Path));
            }

            // Notify the previous page that any writes that are performed on it are part of sections being written
            // in the layout.
            previousPage.IsLayoutBeingRendered = true;
            layoutPage.PreviousSectionWriters = previousPage.SectionWriters;
            layoutPage.BodyContent = bodyWriter.Buffer;
            bodyWriter = await RenderPageAsync(layoutPage, context, invokeViewStarts: false);

            renderedLayouts.Add(layoutPage);
            previousPage = layoutPage;
        }

        // Now we've reached and rendered the outer-most layout page. Nothing left to execute.

        // Ensure all defined sections were rendered or RenderBody was invoked for page without defined sections.
        foreach (var layoutPage in renderedLayouts)
        {
            layoutPage.EnsureRenderedBodyOrSections();
        }

        // We've got a bunch of content in the view buffer. How to best deal with it
        // really depends on whether or not we're writing directly to the output or if we're writing to
        // another buffer.
        if (context.Writer is ViewBufferTextWriter viewBufferTextWriter)
        {
            // This means we're writing to another buffer. Use MoveTo to combine them.
            bodyWriter.Buffer.MoveTo(viewBufferTextWriter.Buffer);
        }
        else
        {
            // This means we're writing to a 'real' writer, probably to the actual output stream.
            // We're using PagedBufferedTextWriter here to 'smooth' synchronous writes of IHtmlContent values.
            await using (var writer = _bufferScope!.CreateWriter(context.Writer))
            {
                await bodyWriter.Buffer.WriteToAsync(writer, _htmlEncoder);
                await writer.FlushAsync();
            }
        }
    }

    private IRazorPage GetLayoutPage(ViewContext context, string executingFilePath, string layoutPath)
    {
        var layoutPageResult = _viewEngine.GetPage(executingFilePath, layoutPath);
        var originalLocations = layoutPageResult.SearchedLocations;
        if (layoutPageResult.Page == null)
        {
            layoutPageResult = _viewEngine.FindPage(context, layoutPath);
        }

        if (layoutPageResult.Page == null)
        {
            Debug.Assert(originalLocations is not null && layoutPageResult.SearchedLocations is not null);

            var locations = string.Empty;
            if (originalLocations!.Any())
            {
                locations = Environment.NewLine + string.Join(Environment.NewLine, originalLocations);
            }

            if (layoutPageResult.SearchedLocations.Any())
            {
                locations +=
                    Environment.NewLine + string.Join(Environment.NewLine, layoutPageResult.SearchedLocations);
            }

            throw new InvalidOperationException(Resources.FormatLayoutCannotBeLocated(layoutPath, locations));
        }

        var layoutPage = layoutPageResult.Page;
        return layoutPage;
    }
}
