// 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.Runtime.ExceptionServices;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure;

internal sealed class PageActionInvoker : ResourceInvoker, IActionInvoker
{
    private readonly IPageHandlerMethodSelector _selector;
    private readonly PageContext _pageContext;
    private readonly ITempDataDictionaryFactory _tempDataFactory;
    private readonly HtmlHelperOptions _htmlHelperOptions;
    private readonly CompiledPageActionDescriptor _actionDescriptor;

    private Dictionary<string, object?>? _arguments;
    private HandlerMethodDescriptor? _handler;
    private PageBase? _page;
    private object? _pageModel;
    private ViewContext? _viewContext;

    private PageHandlerSelectedContext? _handlerSelectedContext;
    private PageHandlerExecutingContext? _handlerExecutingContext;
    private PageHandlerExecutedContext? _handlerExecutedContext;

    public PageActionInvoker(
        IPageHandlerMethodSelector handlerMethodSelector,
        DiagnosticListener diagnosticListener,
        ILogger logger,
        IActionContextAccessor actionContextAccessor,
        IActionResultTypeMapper mapper,
        PageContext pageContext,
        IFilterMetadata[] filterMetadata,
        PageActionInvokerCacheEntry cacheEntry,
        ITempDataDictionaryFactory tempDataFactory,
        HtmlHelperOptions htmlHelperOptions)
        : base(
              diagnosticListener,
              logger,
              actionContextAccessor,
              mapper,
              pageContext,
              filterMetadata,
              pageContext.ValueProviderFactories)
    {
        _selector = handlerMethodSelector;
        _pageContext = pageContext;
        CacheEntry = cacheEntry;
        _tempDataFactory = tempDataFactory;
        _htmlHelperOptions = htmlHelperOptions;

        _actionDescriptor = pageContext.ActionDescriptor;
    }

    // Internal for testing
    internal PageActionInvokerCacheEntry CacheEntry { get; }

    private bool HasPageModel => _actionDescriptor.HandlerTypeInfo != _actionDescriptor.PageTypeInfo;

    // Internal for testing
    internal PageContext PageContext => _pageContext;

    /// <remarks>
    /// <see cref="ResourceInvoker"/> for details on what the variables in this method represent.
    /// </remarks>
    protected override async Task InvokeInnerFilterAsync()
    {
        var next = State.PageBegin;
        var scope = Scope.Invoker;
        var state = (object?)null;
        var isCompleted = false;

        while (!isCompleted)
        {
            await Next(ref next, ref scope, ref state, ref isCompleted);
        }
    }

    protected override ValueTask ReleaseResources()
    {
        if (_pageModel != null && CacheEntry.ReleaseModel != null)
        {
            return CacheEntry.ReleaseModel(_pageContext, _pageModel);
        }

        if (_page != null && CacheEntry.ReleasePage != null)
        {
            return CacheEntry.ReleasePage(_pageContext, _viewContext!, _page);
        }

        return default;
    }

    protected override Task InvokeResultAsync(IActionResult result)
    {
        // We also have some special initialization we need to do for PageResult.
        if (result is PageResult pageResult)
        {
            // If we used a PageModel then the Page isn't initialized yet.
            if (_viewContext == null)
            {
                _viewContext = new ViewContext(
                    _pageContext,
                    NullView.Instance,
                    _pageContext.ViewData,
                    _tempDataFactory.GetTempData(_pageContext.HttpContext),
                    TextWriter.Null,
                    _htmlHelperOptions);
                _viewContext.ExecutingFilePath = _pageContext.ActionDescriptor.RelativePath;
            }

            if (_page == null)
            {
                _page = (PageBase)CacheEntry.PageFactory(_pageContext, _viewContext);
            }
            pageResult.Page = _page;
            pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData;
        }

        return base.InvokeResultAsync(result);
    }

    private object CreateInstance()
    {
        if (HasPageModel)
        {
            _logger.ExecutingPageModelFactory(_pageContext);

            // Since this is a PageModel, we need to activate it, and then run a handler method on the model.
            _pageModel = CacheEntry.ModelFactory!(_pageContext);

            _logger.ExecutedPageModelFactory(_pageContext);

            _pageContext.ViewData.Model = _pageModel;

            return _pageModel;
        }
        else
        {
            // Since this is a Page without a PageModel, we need to create the Page before running a handler method.
            _viewContext = new ViewContext(
                _pageContext,
                NullView.Instance,
                _pageContext.ViewData,
                _tempDataFactory.GetTempData(_pageContext.HttpContext),
                TextWriter.Null,
                _htmlHelperOptions);
            _viewContext.ExecutingFilePath = _pageContext.ActionDescriptor.RelativePath;

            _logger.ExecutingPageFactory(_pageContext);

            _page = (PageBase)CacheEntry.PageFactory(_pageContext, _viewContext);

            _logger.ExecutedPageFactory(_pageContext);

            if (_actionDescriptor.ModelTypeInfo == _actionDescriptor.PageTypeInfo)
            {
                _pageContext.ViewData.Model = _page;
            }

            return _page;
        }
    }

    private HandlerMethodDescriptor? SelectHandler()
    {
        return _selector.Select(_pageContext);
    }

    private Task BindArgumentsAsync()
    {
        // Perf: Avoid allocating async state machines where possible. We only need the state
        // machine if you need to bind properties or arguments.
        if (_actionDescriptor.BoundProperties.Count == 0 && (_handler == null || _handler.Parameters.Count == 0))
        {
            return Task.CompletedTask;
        }

        return BindArgumentsCoreAsync();
    }

    private async Task BindArgumentsCoreAsync()
    {
        Debug.Assert(_instance is not null);

        await CacheEntry.PropertyBinder(_pageContext, _instance);

        if (_handler == null)
        {
            return;
        }

        // We do two separate cache lookups, once for the binder and once for the executor.
        // Reducing it to a single lookup requires a lot of code change with little value.
        PageHandlerBinderDelegate? handlerBinder = null;
        for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++)
        {
            if (object.ReferenceEquals(_handler, _actionDescriptor.HandlerMethods[i]))
            {
                handlerBinder = CacheEntry.HandlerBinders[i];
                break;
            }
        }

        Debug.Assert(handlerBinder is not null);
        Debug.Assert(_arguments is not null);
        await handlerBinder(_pageContext, _arguments);
    }

    private static object?[]? PrepareArguments(
        Dictionary<string, object?> argumentsInDictionary,
        HandlerMethodDescriptor handler)
    {
        if (handler.Parameters.Count == 0)
        {
            return null;
        }

        var arguments = new object?[handler.Parameters.Count];
        for (var i = 0; i < arguments.Length; i++)
        {
            var parameter = handler.Parameters[i];

            if (argumentsInDictionary.TryGetValue(parameter.ParameterInfo.Name!, out var value))
            {
                // Do nothing, already set the value.
            }
            else if (!ParameterDefaultValue.TryGetDefaultValue(parameter.ParameterInfo, out value) &&
                parameter.ParameterInfo.ParameterType.IsValueType)
            {
                value = Activator.CreateInstance(parameter.ParameterInfo.ParameterType);
            }

            arguments[i] = value;
        }

        return arguments;
    }

    private async Task InvokeHandlerMethodAsync()
    {
        var handler = _handler;
        if (handler != null)
        {
            Debug.Assert(_instance is not null);
            Debug.Assert(_arguments is not null);
            var arguments = PrepareArguments(_arguments, handler);

            PageHandlerExecutorDelegate? executor = null;
            for (var i = 0; i < _actionDescriptor.HandlerMethods.Count; i++)
            {
                if (object.ReferenceEquals(handler, _actionDescriptor.HandlerMethods[i]))
                {
                    executor = CacheEntry.HandlerExecutors[i];
                    break;
                }
            }

            Debug.Assert(executor != null, "We should always find a executor for a handler");

            _diagnosticListener.BeforeHandlerMethod(_pageContext, handler, _arguments, _instance);
            _logger.ExecutingHandlerMethod(_pageContext, handler, arguments);

            try
            {
                _result = await executor(_instance, arguments);
                _logger.ExecutedHandlerMethod(handler, _result);
            }
            finally
            {
                _diagnosticListener.AfterHandlerMethod(_pageContext, handler, _arguments, _instance, _result);
            }
        }

        // Pages have an implicit 'return Page()' even without a handler method.
        if (_result == null)
        {
            _logger.ExecutingImplicitHandlerMethod(_pageContext);
            _result = new PageResult();
            _logger.ExecutedImplicitHandlerMethod(_result);
        }

        // Ensure ViewData is set on PageResult for backwards compatibility (For example, Identity UI accesses
        // ViewData in a PageFilter's PageHandlerExecutedMethod)
        if (_result is PageResult pageResult)
        {
            pageResult.ViewData = pageResult.ViewData ?? _pageContext.ViewData;
        }
    }

    private Task Next(ref State next, ref Scope scope, ref object? state, ref bool isCompleted)
    {
        switch (next)
        {
            case State.PageBegin:
                {
                    _instance = CreateInstance();

                    goto case State.PageSelectHandlerBegin;
                }

            case State.PageSelectHandlerBegin:
                {
                    _cursor.Reset();

                    _handler = SelectHandler();

                    goto case State.PageSelectHandlerNext;
                }

            case State.PageSelectHandlerNext:

                Debug.Assert(_instance is not null);

                var currentSelector = _cursor.GetNextFilter<IPageFilter, IAsyncPageFilter>();
                if (currentSelector.FilterAsync != null)
                {
                    if (_handlerSelectedContext == null)
                    {
                        _handlerSelectedContext = new PageHandlerSelectedContext(_pageContext, _filters, _instance)
                        {
                            HandlerMethod = _handler,
                        };
                    }

                    state = currentSelector.FilterAsync;
                    goto case State.PageSelectHandlerAsyncBegin;
                }
                else if (currentSelector.Filter != null)
                {
                    if (_handlerSelectedContext == null)
                    {
                        _handlerSelectedContext = new PageHandlerSelectedContext(_pageContext, _filters, _instance)
                        {
                            HandlerMethod = _handler,
                        };
                    }

                    state = currentSelector.Filter;
                    goto case State.PageSelectHandlerSync;
                }
                else
                {
                    goto case State.PageSelectHandlerEnd;
                }

            case State.PageSelectHandlerAsyncBegin:
                {
                    Debug.Assert(state != null);
                    Debug.Assert(_handlerSelectedContext != null);

                    var filter = (IAsyncPageFilter)state;
                    var handlerSelectedContext = _handlerSelectedContext;

                    _diagnosticListener.BeforeOnPageHandlerSelection(handlerSelectedContext, filter);
                    _logger.BeforeExecutingMethodOnFilter(
                        PageLoggerExtensions.PageFilter,
                        nameof(IAsyncPageFilter.OnPageHandlerSelectionAsync),
                        filter);

                    var task = filter.OnPageHandlerSelectionAsync(handlerSelectedContext);
                    if (task.Status != TaskStatus.RanToCompletion)
                    {
                        next = State.PageSelectHandlerAsyncEnd;
                        return task;
                    }

                    goto case State.PageSelectHandlerAsyncEnd;
                }

            case State.PageSelectHandlerAsyncEnd:
                {
                    Debug.Assert(state != null);
                    Debug.Assert(_handlerSelectedContext != null);

                    var filter = (IAsyncPageFilter)state;

                    _diagnosticListener.AfterOnPageHandlerSelection(_handlerSelectedContext, filter);
                    _logger.AfterExecutingMethodOnFilter(
                        PageLoggerExtensions.PageFilter,
                        nameof(IAsyncPageFilter.OnPageHandlerSelectionAsync),
                        filter);

                    goto case State.PageSelectHandlerNext;
                }

            case State.PageSelectHandlerSync:
                {
                    Debug.Assert(state != null);
                    Debug.Assert(_handlerSelectedContext != null);

                    var filter = (IPageFilter)state;
                    var handlerSelectedContext = _handlerSelectedContext;

                    _diagnosticListener.BeforeOnPageHandlerSelected(handlerSelectedContext, filter);
                    _logger.BeforeExecutingMethodOnFilter(
                        PageLoggerExtensions.PageFilter,
                        nameof(IPageFilter.OnPageHandlerSelected),
                        filter);

                    filter.OnPageHandlerSelected(handlerSelectedContext);

                    _diagnosticListener.AfterOnPageHandlerSelected(handlerSelectedContext, filter);

                    goto case State.PageSelectHandlerNext;
                }

            case State.PageSelectHandlerEnd:
                {
                    if (_handlerSelectedContext != null)
                    {
                        _handler = _handlerSelectedContext.HandlerMethod;
                    }

                    _arguments = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);

                    _cursor.Reset();

                    var task = BindArgumentsAsync();
                    if (task.Status != TaskStatus.RanToCompletion)
                    {
                        next = State.PageNext;
                        return task;
                    }

                    goto case State.PageNext;
                }

            case State.PageNext:
                {
                    Debug.Assert(_instance is not null);
                    Debug.Assert(_arguments is not null);

                    var current = _cursor.GetNextFilter<IPageFilter, IAsyncPageFilter>();
                    if (current.FilterAsync != null)
                    {
                        if (_handlerExecutingContext == null)
                        {
                            _handlerExecutingContext = new PageHandlerExecutingContext(_pageContext, _filters, _handler, _arguments, _instance);
                        }

                        state = current.FilterAsync;
                        goto case State.PageAsyncBegin;
                    }
                    else if (current.Filter != null)
                    {
                        if (_handlerExecutingContext == null)
                        {
                            _handlerExecutingContext = new PageHandlerExecutingContext(_pageContext, _filters, _handler, _arguments, _instance);
                        }

                        state = current.Filter;
                        goto case State.PageSyncBegin;
                    }
                    else
                    {
                        goto case State.PageInside;
                    }
                }

            case State.PageAsyncBegin:
                {
                    Debug.Assert(state != null);
                    Debug.Assert(_handlerExecutingContext != null);

                    var filter = (IAsyncPageFilter)state;
                    var handlerExecutingContext = _handlerExecutingContext;

                    _diagnosticListener.BeforeOnPageHandlerExecution(handlerExecutingContext, filter);
                    _logger.BeforeExecutingMethodOnFilter(
                        PageLoggerExtensions.PageFilter,
                        nameof(IAsyncPageFilter.OnPageHandlerExecutionAsync),
                        filter);

                    var task = filter.OnPageHandlerExecutionAsync(handlerExecutingContext, InvokeNextPageFilterAwaitedAsync);
                    if (task.Status != TaskStatus.RanToCompletion)
                    {
                        next = State.PageAsyncEnd;
                        return task;
                    }

                    goto case State.PageAsyncEnd;
                }

            case State.PageAsyncEnd:
                {
                    Debug.Assert(state != null);
                    Debug.Assert(_handlerExecutingContext != null);

                    Debug.Assert(_instance is not null);

                    var filter = (IAsyncPageFilter)state;

                    if (_handlerExecutedContext == null)
                    {
                        // If we get here then the filter didn't call 'next' indicating a short circuit.
                        _logger.PageFilterShortCircuited(filter);

                        _handlerExecutedContext = new PageHandlerExecutedContext(
                            _pageContext,
                            _filters,
                            _handler,
                            _instance)
                        {
                            Canceled = true,
                            Result = _handlerExecutingContext.Result,
                        };
                    }

                    _diagnosticListener.AfterOnPageHandlerExecution(_handlerExecutedContext, filter);
                    _logger.AfterExecutingMethodOnFilter(
                       PageLoggerExtensions.PageFilter,
                       nameof(IAsyncPageFilter.OnPageHandlerExecutionAsync),
                       filter);

                    goto case State.PageEnd;
                }

            case State.PageSyncBegin:
                {
                    Debug.Assert(state != null);
                    Debug.Assert(_handlerExecutingContext != null);
                    Debug.Assert(_instance is not null);

                    var filter = (IPageFilter)state;
                    var handlerExecutingContext = _handlerExecutingContext;

                    _diagnosticListener.BeforeOnPageHandlerExecuting(handlerExecutingContext, filter);
                    _logger.BeforeExecutingMethodOnFilter(
                       PageLoggerExtensions.PageFilter,
                       nameof(IPageFilter.OnPageHandlerExecuting),
                       filter);

                    filter.OnPageHandlerExecuting(handlerExecutingContext);

                    _diagnosticListener.AfterOnPageHandlerExecuting(handlerExecutingContext, filter);
                    _logger.AfterExecutingMethodOnFilter(
                       PageLoggerExtensions.PageFilter,
                       nameof(IPageFilter.OnPageHandlerExecuting),
                       filter);

                    if (handlerExecutingContext.Result != null)
                    {
                        // Short-circuited by setting a result.
                        _logger.PageFilterShortCircuited(filter);

                        _handlerExecutedContext = new PageHandlerExecutedContext(
                            _pageContext,
                            _filters,
                            _handler,
                            _instance)
                        {
                            Canceled = true,
                            Result = _handlerExecutingContext.Result,
                        };

                        goto case State.PageEnd;
                    }

                    var task = InvokeNextPageFilterAsync();
                    if (task.Status != TaskStatus.RanToCompletion)
                    {
                        next = State.PageSyncEnd;
                        return task;
                    }

                    goto case State.PageSyncEnd;
                }

            case State.PageSyncEnd:
                {
                    Debug.Assert(state != null);
                    Debug.Assert(_handlerExecutingContext != null);
                    Debug.Assert(_handlerExecutedContext != null);

                    var filter = (IPageFilter)state;
                    var handlerExecutedContext = _handlerExecutedContext;

                    _diagnosticListener.BeforeOnPageHandlerExecuted(handlerExecutedContext, filter);
                    _logger.BeforeExecutingMethodOnFilter(
                       PageLoggerExtensions.PageFilter,
                       nameof(IPageFilter.OnPageHandlerExecuted),
                       filter);

                    filter.OnPageHandlerExecuted(handlerExecutedContext);

                    _diagnosticListener.AfterOnPageHandlerExecuted(handlerExecutedContext, filter);
                    _logger.AfterExecutingMethodOnFilter(
                       PageLoggerExtensions.PageFilter,
                       nameof(IPageFilter.OnPageHandlerExecuted),
                       filter);

                    goto case State.PageEnd;
                }

            case State.PageInside:
                {
                    var task = InvokeHandlerMethodAsync();
                    if (task.Status != TaskStatus.RanToCompletion)
                    {
                        next = State.PageEnd;
                        return task;
                    }

                    goto case State.PageEnd;
                }

            case State.PageEnd:
                {
                    if (scope == Scope.Page)
                    {
                        if (_handlerExecutedContext == null)
                        {
                            _handlerExecutedContext = new PageHandlerExecutedContext(_pageContext, _filters, _handler!, _instance!)
                            {
                                Result = _result,
                            };
                        }

                        isCompleted = true;
                        return Task.CompletedTask;
                    }

                    var handlerExecutedContext = _handlerExecutedContext;
                    Rethrow(handlerExecutedContext);

                    if (handlerExecutedContext != null)
                    {
                        _result = handlerExecutedContext.Result;
                    }

                    isCompleted = true;
                    return Task.CompletedTask;
                }

            default:
                throw new InvalidOperationException();
        }
    }

    private async Task InvokeNextPageFilterAsync()
    {
        try
        {
            var next = State.PageNext;
            var state = (object?)null;
            var scope = Scope.Page;
            var isCompleted = false;
            while (!isCompleted)
            {
                await Next(ref next, ref scope, ref state, ref isCompleted);
            }
        }
        catch (Exception exception)
        {
            _handlerExecutedContext = new PageHandlerExecutedContext(_pageContext, _filters, _handler!, _instance!)
            {
                ExceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception),
            };
        }

        Debug.Assert(_handlerExecutedContext != null);
    }

    private async Task<PageHandlerExecutedContext> InvokeNextPageFilterAwaitedAsync()
    {
        Debug.Assert(_handlerExecutingContext != null);
        if (_handlerExecutingContext.Result != null)
        {
            // If we get here, it means that an async filter set a result AND called next(). This is forbidden.
            var message = Resources.FormatAsyncPageFilter_InvalidShortCircuit(
                typeof(IAsyncPageFilter).Name,
                nameof(PageHandlerExecutingContext.Result),
                typeof(PageHandlerExecutingContext).Name,
                typeof(PageHandlerExecutionDelegate).Name);

            throw new InvalidOperationException(message);
        }

        await InvokeNextPageFilterAsync();

        Debug.Assert(_handlerExecutedContext != null);
        return _handlerExecutedContext;
    }

    private static void Rethrow(PageHandlerExecutedContext? context)
    {
        if (context == null)
        {
            return;
        }

        if (context.ExceptionHandled)
        {
            return;
        }

        context.ExceptionDispatchInfo?.Throw();

        if (context.Exception != null)
        {
            throw context.Exception;
        }
    }

    private enum Scope
    {
        Invoker,
        Page,
    }

    private enum State
    {
        PageBegin,
        PageSelectHandlerBegin,
        PageSelectHandlerNext,
        PageSelectHandlerAsyncBegin,
        PageSelectHandlerAsyncEnd,
        PageSelectHandlerSync,
        PageSelectHandlerEnd,
        PageNext,
        PageAsyncBegin,
        PageAsyncEnd,
        PageSyncBegin,
        PageSyncEnd,
        PageInside,
        PageEnd,
    }
}
