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

#nullable disable warnings

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Reflection;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using static Microsoft.AspNetCore.Internal.LinkerFlags;

namespace Microsoft.AspNetCore.Components.RenderTree;

/// <summary>
/// Types in the Microsoft.AspNetCore.Components.RenderTree are not recommended for use outside
/// of the Blazor framework. These types will change in a future release.
/// </summary>
//
// Provides mechanisms for rendering hierarchies of <see cref="IComponent"/> instances,
// dispatching events to them, and notifying when the user interface is being updated.
public abstract partial class Renderer : IDisposable, IAsyncDisposable
{
    private readonly object _lockObject = new();
    private readonly IServiceProvider _serviceProvider;
    private readonly Dictionary<int, ComponentState> _componentStateById = new Dictionary<int, ComponentState>();
    private readonly Dictionary<IComponent, ComponentState> _componentStateByComponent = new Dictionary<IComponent, ComponentState>();
    private readonly RenderBatchBuilder _batchBuilder = new RenderBatchBuilder();
    private readonly Dictionary<ulong, (int RenderedByComponentId, EventCallback Callback)> _eventBindings = new();
    private readonly Dictionary<ulong, ulong> _eventHandlerIdReplacements = new Dictionary<ulong, ulong>();
    private readonly ILogger _logger;
    private readonly ComponentFactory _componentFactory;
    private Dictionary<int, ParameterView>? _rootComponentsLatestParameters;
    private Task? _ongoingQuiescenceTask;

    private int _nextComponentId;
    private bool _isBatchInProgress;
    private ulong _lastEventHandlerId;
    private List<Task>? _pendingTasks;
    private Task? _disposeTask;
    private bool _rendererIsDisposed;

    private bool _hotReloadInitialized;

    /// <summary>
    /// Allows the caller to handle exceptions from the SynchronizationContext when one is available.
    /// </summary>
    public event UnhandledExceptionEventHandler UnhandledSynchronizationException
    {
        add
        {
            Dispatcher.UnhandledException += value;
        }
        remove
        {
            Dispatcher.UnhandledException -= value;
        }
    }

    /// <summary>
    /// Constructs an instance of <see cref="Renderer"/>.
    /// </summary>
    /// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used when initializing components.</param>
    /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
    public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory)
        : this(serviceProvider, loggerFactory, GetComponentActivatorOrDefault(serviceProvider))
    {
        // This overload is provided for back-compatibility
    }

    /// <summary>
    /// Constructs an instance of <see cref="Renderer"/>.
    /// </summary>
    /// <param name="serviceProvider">The <see cref="IServiceProvider"/> to be used when initializing components.</param>
    /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
    /// <param name="componentActivator">The <see cref="IComponentActivator"/>.</param>
    public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, IComponentActivator componentActivator)
    {
        ArgumentNullException.ThrowIfNull(serviceProvider);
        ArgumentNullException.ThrowIfNull(loggerFactory);
        ArgumentNullException.ThrowIfNull(componentActivator);

        _serviceProvider = serviceProvider;
        // Would normally use ILogger<T> here to get the benefit of the string name being cached but this is a public ctor that
        // has always taken ILoggerFactory so to avoid the per-instance string allocation of the logger name we just pass the
        // logger name in here as a string literal.
        _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer");
        _componentFactory = new ComponentFactory(componentActivator, this);

        ServiceProviderCascadingValueSuppliers = serviceProvider.GetService<ICascadingValueSupplier>() is null
            ? Array.Empty<ICascadingValueSupplier>()
            : serviceProvider.GetServices<ICascadingValueSupplier>().ToArray();
    }

    internal ICascadingValueSupplier[] ServiceProviderCascadingValueSuppliers { get; }

    internal HotReloadManager HotReloadManager { get; set; } = HotReloadManager.Default;

    private static IComponentActivator GetComponentActivatorOrDefault(IServiceProvider serviceProvider)
    {
        return serviceProvider.GetService<IComponentActivator>()
            ?? new DefaultComponentActivator(serviceProvider);
    }

    /// <summary>
    /// Gets the <see cref="Components.Dispatcher" /> associated with this <see cref="Renderer" />.
    /// </summary>
    public abstract Dispatcher Dispatcher { get; }

    /// <summary>
    /// Gets or sets the <see cref="Components.ElementReferenceContext"/> associated with this <see cref="Renderer"/>,
    /// if it exists.
    /// </summary>
    protected internal ElementReferenceContext? ElementReferenceContext { get; protected set; }

    /// <summary>
    /// Gets a value that determines if the <see cref="Renderer"/> is triggering a render in response to a (metadata update) hot-reload change.
    /// </summary>
    internal bool IsRenderingOnMetadataUpdate { get; private set; }

    /// <summary>
    /// Gets whether the renderer has been disposed.
    /// </summary>
    internal bool Disposed => _rendererIsDisposed;

    /// <summary>
    /// Gets the <see cref="ComponentState"/> associated with the specified component.
    /// </summary>
    /// <param name="componentId">The component ID</param>
    /// <returns>The corresponding <see cref="ComponentState"/>.</returns>
    protected ComponentState GetComponentState(int componentId)
        => GetRequiredComponentState(componentId);

    /// <summary>
    /// Gets the <see cref="IComponentRenderMode"/> for a given component if available.
    /// </summary>
    /// <param name="component">The component type</param>
    /// <returns></returns>
    protected internal virtual IComponentRenderMode? GetComponentRenderMode(IComponent component)
        => null;

    internal IComponentRenderMode? GetComponentRenderMode(int componentId)
        => GetComponentRenderMode(GetRequiredComponentState(componentId).Component);

    /// <summary>
    /// Resolves the component state for a given <see cref="IComponent"/> instance.
    /// </summary>
    /// <param name="component">The <see cref="IComponent"/> instance</param>
    /// <returns></returns>
    protected internal ComponentState GetComponentState(IComponent component)
        => _componentStateByComponent.GetValueOrDefault(component);

    /// <summary>
    /// Gets the <see cref="RendererInfo"/> associated with this <see cref="Renderer"/>.
    /// </summary>
    protected internal virtual RendererInfo RendererInfo { get; }

    /// <summary>
    /// Gets the <see cref="ResourceAssetCollection"/> associated with this <see cref="Renderer"/>.
    /// </summary>
    protected internal virtual ResourceAssetCollection Assets { get; } = ResourceAssetCollection.Empty;

    private async void RenderRootComponentsOnHotReload()
    {
        // Before re-rendering the root component, also clear any well-known caches in the framework
        ComponentFactory.ClearCache();
        ComponentProperties.ClearCache();
        DefaultComponentActivator.ClearCache();

        await Dispatcher.InvokeAsync(() =>
        {
            if (_rootComponentsLatestParameters is null)
            {
                return;
            }

            IsRenderingOnMetadataUpdate = true;
            try
            {
                foreach (var (componentId, parameters) in _rootComponentsLatestParameters)
                {
                    var componentState = GetRequiredComponentState(componentId);
                    componentState.SetDirectParameters(parameters);
                }
            }
            finally
            {
                IsRenderingOnMetadataUpdate = false;
            }
        });
    }

    /// <summary>
    /// Constructs a new component of the specified type.
    /// </summary>
    /// <param name="componentType">The type of the component to instantiate.</param>
    /// <returns>The component instance.</returns>
    protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)] Type componentType)
        => _componentFactory.InstantiateComponent(_serviceProvider, componentType, null, null);

    /// <summary>
    /// Associates the <see cref="IComponent"/> with the <see cref="Renderer"/>, assigning
    /// an identifier that is unique within the scope of the <see cref="Renderer"/>.
    /// </summary>
    /// <param name="component">The component.</param>
    /// <returns>The component's assigned identifier.</returns>
    // Internal for unit testing
    protected internal int AssignRootComponentId(IComponent component)
    {
        if (!_hotReloadInitialized)
        {
            _hotReloadInitialized = true;
            if (HotReloadManager.MetadataUpdateSupported)
            {
                HotReloadManager.OnDeltaApplied += RenderRootComponentsOnHotReload;
            }
        }

        return AttachAndInitComponent(component, -1).ComponentId;
    }

    /// <summary>
    /// Gets the current render tree for a given component.
    /// </summary>
    /// <param name="componentId">The id for the component.</param>
    /// <returns>The <see cref="RenderTreeBuilder"/> representing the current render tree.</returns>
    protected ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId) => GetRequiredComponentState(componentId).CurrentRenderTree.GetFrames();

    /// <summary>
    /// Performs the first render for a root component, waiting for this component and all
    /// children components to finish rendering in case there is any asynchronous work being
    /// done by any of the components. After this, the root component
    /// makes its own decisions about when to re-render, so there is no need to call
    /// this more than once.
    /// </summary>
    /// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
    /// <remarks>
    /// Rendering a root component is an asynchronous operation. Clients may choose to not await the returned task to
    /// start, but not wait for the entire render to complete.
    /// </remarks>
    protected Task RenderRootComponentAsync(int componentId)
    {
        return RenderRootComponentAsync(componentId, ParameterView.Empty);
    }

    /// <summary>
    /// Supplies parameters for a root component, normally causing it to render. This can be
    /// used to trigger the first render of a root component, or to update its parameters and
    /// trigger a subsequent render. Note that components may also make their own decisions about
    /// when to re-render, and may re-render at any time.
    ///
    /// The returned <see cref="Task"/> waits for this component and all descendant components to
    /// finish rendering in case there is any asynchronous work being done by any of them.
    /// </summary>
    /// <param name="componentId">The ID returned by <see cref="AssignRootComponentId(IComponent)"/>.</param>
    /// <param name="initialParameters">The <see cref="ParameterView"/> with the initial or updated parameters to use for rendering.</param>
    /// <remarks>
    /// Rendering a root component is an asynchronous operation. Clients may choose to not await the returned task to
    /// start, but not wait for the entire render to complete.
    /// </remarks>
    protected internal async Task RenderRootComponentAsync(int componentId, ParameterView initialParameters)
    {
        Dispatcher.AssertAccess();

        // Since this is a "render root" operation being invoked from outside the system, we start tracking
        // any async tasks from this point until we reach quiescence. This allows external code such as prerendering
        // to know when the renderer has some finished output. We don't track async tasks at other times
        // because nobody would be waiting for quiescence at other times.
        // Having a nonnull value for _pendingTasks is what signals that we should be capturing the async tasks.
        _pendingTasks ??= new();

        var componentState = GetRequiredRootComponentState(componentId);
        if (HotReloadManager.MetadataUpdateSupported)
        {
            // When we're doing hot-reload, stash away the parameters used while rendering root components.
            // We'll use this to trigger re-renders on hot reload updates.
            _rootComponentsLatestParameters ??= new();
            _rootComponentsLatestParameters[componentId] = initialParameters.Clone();
        }

        componentState.SetDirectParameters(initialParameters);

        await WaitForQuiescence();
        Debug.Assert(_pendingTasks == null);
    }

    /// <summary>
    /// Removes the specified component from the renderer, causing the component and its
    /// descendants to be disposed.
    /// </summary>
    /// <param name="componentId">The ID of the root component.</param>
    protected internal void RemoveRootComponent(int componentId)
    {
        Dispatcher.AssertAccess();

        // Asserts it's a root component
        _ = GetRequiredRootComponentState(componentId);

        // This assumes there isn't currently a batch in progress, and will throw if there is.
        // Currently there's no known scenario where we need to support calling RemoveRootComponentAsync
        // during a batch, but if a scenario emerges we can add support.
        _batchBuilder.ComponentDisposalQueue.Enqueue(componentId);
        if (HotReloadManager.MetadataUpdateSupported)
        {
            _rootComponentsLatestParameters?.Remove(componentId);
        }

        ProcessRenderQueue();
    }

    /// <summary>
    /// Gets the type of the specified root component.
    /// </summary>
    /// <param name="componentId">The root component ID.</param>
    /// <returns>The type of the component.</returns>
    internal Type GetRootComponentType(int componentId)
        => GetRequiredRootComponentState(componentId).Component.GetType();

    /// <summary>
    /// Allows derived types to handle exceptions during rendering. Defaults to rethrowing the original exception.
    /// </summary>
    /// <param name="exception">The <see cref="Exception"/>.</param>
    protected abstract void HandleException(Exception exception);

    private async Task WaitForQuiescence()
    {
        // If there's already a loop waiting for quiescence, just join it
        if (_ongoingQuiescenceTask is not null)
        {
            await _ongoingQuiescenceTask;
            return;
        }

        try
        {
            _ongoingQuiescenceTask = ProcessAsynchronousWork();
            await _ongoingQuiescenceTask;
        }
        finally
        {
            Debug.Assert(_pendingTasks is null || _pendingTasks.Count == 0);
            _pendingTasks = null;
            _ongoingQuiescenceTask = null;
        }

        async Task ProcessAsynchronousWork()
        {
            // Child components SetParametersAsync are stored in the queue of pending tasks,
            // which might trigger further renders.
            while (_pendingTasks?.Count > 0)
            {
                // Create a Task that represents the remaining ongoing work for the rendering process
                var pendingWork = Task.WhenAll(_pendingTasks);

                // Clear all pending work.
                _pendingTasks.Clear();

                // new work might be added before we check again as a result of waiting for all
                // the child components to finish executing SetParametersAsync
                await pendingWork;
            }
        }
    }

    private ComponentState AttachAndInitComponent(IComponent component, int parentComponentId)
    {
        var componentId = _nextComponentId++;
        var parentComponentState = GetOptionalComponentState(parentComponentId);
        var componentState = CreateComponentState(componentId, component, parentComponentState);
        Log.InitializingComponent(_logger, componentState, parentComponentState);
        _componentStateById.Add(componentId, componentState);
        _componentStateByComponent.Add(component, componentState);
        component.Attach(new RenderHandle(this, componentId));
        return componentState;
    }

    /// <summary>
    /// Creates a <see cref="ComponentState"/> instance to track state associated with a newly-instantiated component.
    /// This is called before the component is initialized and tracked within the <see cref="Renderer"/>. Subclasses
    /// may override this method to use their own subclasses of <see cref="ComponentState"/>.
    /// </summary>
    /// <param name="componentId">The ID of the newly-created component.</param>
    /// <param name="component">The component instance.</param>
    /// <param name="parentComponentState">The <see cref="ComponentState"/> associated with the parent component, or null if this is a root component.</param>
    /// <returns>A <see cref="ComponentState"/> for the new component.</returns>
    protected virtual ComponentState CreateComponentState(int componentId, IComponent component, ComponentState? parentComponentState)
        => new ComponentState(this, componentId, component, parentComponentState);

    /// <summary>
    /// Updates the visible UI.
    /// </summary>
    /// <param name="renderBatch">The changes to the UI since the previous call.</param>
    /// <returns>A <see cref="Task"/> to represent the UI update process.</returns>
    protected abstract Task UpdateDisplayAsync(in RenderBatch renderBatch);

    /// <summary>
    /// Notifies the renderer that an event has occurred.
    /// </summary>
    /// <param name="eventHandlerId">The <see cref="RenderTreeFrame.AttributeEventHandlerId"/> value from the original event attribute.</param>
    /// <param name="eventArgs">Arguments to be passed to the event handler.</param>
    /// <param name="fieldInfo">Information that the renderer can use to update the state of the existing render tree to match the UI.</param>
    /// <returns>
    /// A <see cref="Task"/> which will complete once all asynchronous processing related to the event
    /// has completed.
    /// </returns>
    public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fieldInfo, EventArgs eventArgs)
    {
        return DispatchEventAsync(eventHandlerId, fieldInfo, eventArgs, waitForQuiescence: false);
    }

    /// <summary>
    /// Notifies the renderer that an event has occurred.
    /// </summary>
    /// <param name="eventHandlerId">The <see cref="RenderTreeFrame.AttributeEventHandlerId"/> value from the original event attribute.</param>
    /// <param name="eventArgs">Arguments to be passed to the event handler.</param>
    /// <param name="fieldInfo">Information that the renderer can use to update the state of the existing render tree to match the UI.</param>
    /// <param name="waitForQuiescence">A flag indicating whether to wait for quiescence.</param>
    /// <returns>
    /// A <see cref="Task"/> which will complete once all asynchronous processing related to the event
    /// has completed.
    /// </returns>
    public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fieldInfo, EventArgs eventArgs, bool waitForQuiescence)
    {
        Dispatcher.AssertAccess();

        if (waitForQuiescence)
        {
            _pendingTasks ??= new();
        }

        var (renderedByComponentId, callback) = GetRequiredEventBindingEntry(eventHandlerId);

        // If this event attribute was rendered by a component that's since been disposed, don't dispatch the event at all.
        // This can occur because event handler disposal is deferred, so event handler IDs can outlive their components.
        // The reason the following check is based on "which component rendered this frame" and not on "which component
        // receives the callback" (i.e., callback.Receiver) is that if parent A passes a RenderFragment with events to child B,
        // and then child B is disposed, we don't want to dispatch the events (because the developer considers them removed
        // from the UI) even though the receiver A is still alive.
        if (!_componentStateById.ContainsKey(renderedByComponentId))
        {
            // This is not an error since it can happen legitimately (in Blazor Server, the user might click a button at the same
            // moment that the component is disposed remotely, and then the click event will arrive after disposal).
            Log.SkippingEventOnDisposedComponent(_logger, renderedByComponentId, eventHandlerId, eventArgs);
            return Task.CompletedTask;
        }

        Log.HandlingEvent(_logger, eventHandlerId, eventArgs);

        // Try to match it up with a receiver so that, if the event handler later throws, we can route the error to the
        // correct error boundary (even if the receiving component got disposed in the meantime).
        ComponentState? receiverComponentState = null;
        if (callback.Receiver is IComponent receiverComponent) // The receiver might be null or not an IComponent
        {
            // Even if the receiver is an IComponent, it might not be one of ours, or might be disposed already
            // We can only route errors to error boundaries if the receiver is known and not yet disposed at this stage
            _componentStateByComponent.TryGetValue(receiverComponent, out receiverComponentState);
        }

        if (fieldInfo != null)
        {
            var latestEquivalentEventHandlerId = FindLatestEventHandlerIdInChain(eventHandlerId);
            UpdateRenderTreeToMatchClientState(latestEquivalentEventHandlerId, fieldInfo);
        }

        Task? task = null;
        try
        {
            // The event handler might request multiple renders in sequence. Capture them
            // all in a single batch.
            _isBatchInProgress = true;

            task = callback.InvokeAsync(eventArgs);
        }
        catch (Exception e)
        {
            HandleExceptionViaErrorBoundary(e, receiverComponentState);
            return Task.CompletedTask;
        }
        finally
        {
            _isBatchInProgress = false;

            // Since the task has yielded - process any queued rendering work before we return control
            // to the caller.
            ProcessPendingRender();
        }

        // Task completed synchronously or is still running. We already processed all of the rendering
        // work that was queued so let our error handler deal with it.
        var errorHandledTask = GetErrorHandledTask(task, receiverComponentState);

        if (waitForQuiescence)
        {
            AddPendingTask(receiverComponentState, errorHandledTask);
            return WaitForQuiescence();
        }
        else
        {
            return errorHandledTask;
        }
    }

    /// <summary>
    /// Gets the event arguments type for the specified event handler.
    /// </summary>
    /// <param name="eventHandlerId">The <see cref="RenderTreeFrame.AttributeEventHandlerId"/> value from the original event attribute.</param>
    /// <returns>The parameter type expected by the event handler. Normally this is a subclass of <see cref="EventArgs"/>.</returns>
    public Type GetEventArgsType(ulong eventHandlerId)
    {
        var methodInfo = GetRequiredEventBindingEntry(eventHandlerId).Callback.Delegate?.Method;

        // The DispatchEventAsync code paths allow for the case where Delegate or its method
        // is null, and in this case the event receiver just receives null. This won't happen
        // under normal circumstances, but to avoid creating a new failure scenario, allow for
        // that edge case here too.
        return methodInfo == null
            ? typeof(EventArgs)
            : EventArgsTypeCache.GetEventArgsType(methodInfo);
    }

    internal ComponentState InstantiateChildComponentOnFrame(RenderTreeFrame[] frames, int frameIndex, int parentComponentId)
    {
        ref var frame = ref frames[frameIndex];
        if (frame.FrameTypeField != RenderTreeFrameType.Component)
        {
            throw new ArgumentException($"The frame's {nameof(RenderTreeFrame.FrameType)} property must equal {RenderTreeFrameType.Component}", nameof(frameIndex));
        }

        if (frame.ComponentStateField != null)
        {
            throw new ArgumentException($"The frame already has a non-null component instance", nameof(frameIndex));
        }

        var callerSpecifiedRenderMode = frame.ComponentFrameFlags.HasFlag(ComponentFrameFlags.HasCallerSpecifiedRenderMode)
            ? FindCallerSpecifiedRenderMode(frames, frameIndex)
            : null;

        var newComponent = _componentFactory.InstantiateComponent(_serviceProvider, frame.ComponentTypeField, callerSpecifiedRenderMode, parentComponentId);
        var newComponentState = AttachAndInitComponent(newComponent, parentComponentId);
        frame.ComponentStateField = newComponentState;
        frame.ComponentIdField = newComponentState.ComponentId;

        return newComponentState;
    }

    private static IComponentRenderMode? FindCallerSpecifiedRenderMode(RenderTreeFrame[] frames, int componentFrameIndex)
    {
        // ComponentRenderMode frames are immediate children of Component frames. So, they have to appear after any parameter
        // attributes (since attributes must always immediately follow Component frames), but before anything that would
        // represent a different child node, such as text/element or another component. It's OK to do this linear scan
        // because we consider it uncommon to specify a rendermode, and none of this happens if you don't.
        var endIndex = componentFrameIndex + frames[componentFrameIndex].ComponentSubtreeLengthField;
        for (var index = componentFrameIndex + 1; index <= endIndex; index++)
        {
            ref var frame = ref frames[index];
            switch (frame.FrameType)
            {
                case RenderTreeFrameType.Attribute:
                    continue;
                case RenderTreeFrameType.ComponentRenderMode:
                    return frame.ComponentRenderMode;
                default:
                    break;
            }
        }

        return null;
    }

    internal void AddToPendingTasksWithErrorHandling(Task task, ComponentState? owningComponentState)
    {
        switch (task == null ? TaskStatus.RanToCompletion : task.Status)
        {
            // If it's already completed synchronously, no need to add it to the list of
            // pending Tasks as no further render (we already rerender synchronously) will.
            // happen.
            case TaskStatus.RanToCompletion:
            case TaskStatus.Canceled:
                break;
            case TaskStatus.Faulted:
                // We want to immediately handle exceptions if the task failed synchronously instead of
                // waiting for it to throw later. This can happen if the task is produced by
                // an 'async' state machine (the ones generated using async/await) where even
                // the synchronous exceptions will get captured and converted into a faulted
                // task.
                var baseException = task.Exception.GetBaseException();
                HandleExceptionViaErrorBoundary(baseException, owningComponentState);
                break;
            default:
                // It's important to evaluate the following even if we're not going to use
                // handledErrorTask below, because it has the side-effect of calling HandleException.
                var handledErrorTask = GetErrorHandledTask(task, owningComponentState);
                AddPendingTask(owningComponentState, handledErrorTask);
                break;
        }
    }

    /// <summary>
    /// Notifies the renderer that there is a pending task associated with a component. The
    /// renderer is regarded as quiescent when all such tasks have completed.
    /// </summary>
    /// <param name="componentState">The <see cref="ComponentState"/> for the component associated with this pending task, if any.</param>
    /// <param name="task">The <see cref="Task"/>.</param>
    protected virtual void AddPendingTask(ComponentState? componentState, Task task)
    {
        // The pendingTasks collection is only used during prerendering to track quiescence,
        // so will be null at other times.
        if (_pendingTasks is { } tasks)
        {
            Dispatcher.AssertAccess();
            tasks.Add(task);
        }
    }

    internal void AssignEventHandlerId(int renderedByComponentId, ref RenderTreeFrame frame)
    {
        var id = ++_lastEventHandlerId;

        if (frame.AttributeValueField is EventCallback callback)
        {
            // We hit this case when a EventCallback object is produced that needs an explicit receiver.
            // Common cases for this are "chained bind" or "chained event handler" when a component
            // accepts a delegate as a parameter and then hooks it up to a DOM event.
            //
            // When that happens we intentionally box the EventCallback because we need to hold on to
            // the receiver.
            _eventBindings.Add(id, (renderedByComponentId, callback));
        }
        else if (frame.AttributeValueField is MulticastDelegate @delegate)
        {
            // This is the common case for a delegate, where the receiver of the event
            // is the same as delegate.Target. In this case since the receiver is implicit we can
            // avoid boxing the EventCallback object and just re-hydrate it on the other side of the
            // render tree.
            _eventBindings.Add(id, (renderedByComponentId, new EventCallback(@delegate.Target as IHandleEvent, @delegate)));
        }

        // NOTE: we do not to handle EventCallback<T> here. EventCallback<T> is only used when passing
        // a callback to a component, and never when used to attaching a DOM event handler.

        frame.AttributeEventHandlerIdField = id;
    }

    /// <summary>
    /// Schedules a render for the specified <paramref name="componentId"/>. Its display
    /// will be populated using the specified <paramref name="renderFragment"/>.
    /// </summary>
    /// <param name="componentId">The ID of the component to render.</param>
    /// <param name="renderFragment">A <see cref="RenderFragment"/> that will supply the updated UI contents.</param>
    internal void AddToRenderQueue(int componentId, RenderFragment renderFragment)
    {
        Dispatcher.AssertAccess();

        var componentState = GetOptionalComponentState(componentId);
        if (componentState == null)
        {
            // If the component was already disposed, then its render handle trying to
            // queue a render is a no-op.
            return;
        }

        _batchBuilder.ComponentRenderQueue.Enqueue(
            new RenderQueueEntry(componentState, renderFragment));

        if (!_isBatchInProgress)
        {
            ProcessPendingRender();
        }
    }

    internal void TrackReplacedEventHandlerId(ulong oldEventHandlerId, ulong newEventHandlerId)
    {
        // Tracking the chain of old->new replacements allows us to interpret incoming EventFieldInfo
        // values even if they refer to an event handler ID that's since been superseded. This is essential
        // for tree patching to work in an async environment.
        _eventHandlerIdReplacements.Add(oldEventHandlerId, newEventHandlerId);
    }

    private (int RenderedByComponentId, EventCallback Callback) GetRequiredEventBindingEntry(ulong eventHandlerId)
    {
        if (!_eventBindings.TryGetValue(eventHandlerId, out var entry))
        {
            throw new ArgumentException($"There is no event handler associated with this event. EventId: '{eventHandlerId}'.", nameof(eventHandlerId));
        }

        return entry;
    }

    private ulong FindLatestEventHandlerIdInChain(ulong eventHandlerId)
    {
        while (_eventHandlerIdReplacements.TryGetValue(eventHandlerId, out var replacementEventHandlerId))
        {
            eventHandlerId = replacementEventHandlerId;
        }

        return eventHandlerId;
    }

    private ComponentState GetRequiredComponentState(int componentId)
        => _componentStateById.TryGetValue(componentId, out var componentState)
            ? componentState
            : throw new ArgumentException($"The renderer does not have a component with ID {componentId}.");

    private ComponentState? GetOptionalComponentState(int componentId)
        => _componentStateById.TryGetValue(componentId, out var componentState)
            ? componentState
            : null;

    private ComponentState GetRequiredRootComponentState(int componentId)
    {
        var componentState = GetRequiredComponentState(componentId);
        if (componentState.ParentComponentState is not null)
        {
            throw new InvalidOperationException("The specified component is not a root component");
        }

        return componentState;
    }

    /// <summary>
    /// Processes pending renders requests from components if there are any.
    /// </summary>
    protected virtual void ProcessPendingRender()
    {
        if (_rendererIsDisposed)
        {
            // Once we're disposed, we'll disregard further attempts to render anything
            return;
        }

        ProcessRenderQueue();
    }

    private void ProcessRenderQueue()
    {
        Dispatcher.AssertAccess();

        if (_isBatchInProgress)
        {
            throw new InvalidOperationException("Cannot start a batch when one is already in progress.");
        }

        _isBatchInProgress = true;
        var updateDisplayTask = Task.CompletedTask;

        try
        {
            if (_batchBuilder.ComponentRenderQueue.Count == 0)
            {
                if (_batchBuilder.ComponentDisposalQueue.Count == 0)
                {
                    // Nothing to do
                    return;
                }
                else
                {
                    // Normally we process the disposal queue after each component rendering step,
                    // but in this case disposal is the only pending action so far
                    ProcessDisposalQueueInExistingBatch();
                }
            }

            // Process render queue until empty
            while (_batchBuilder.ComponentRenderQueue.Count > 0)
            {
                var nextToRender = _batchBuilder.ComponentRenderQueue.Dequeue();
                RenderInExistingBatch(nextToRender);
            }

            var batch = _batchBuilder.ToBatch();
            updateDisplayTask = UpdateDisplayAsync(batch);

            // Fire off the execution of OnAfterRenderAsync, but don't wait for it
            // if there is async work to be done.
            _ = InvokeRenderCompletedCalls(batch.UpdatedComponents, updateDisplayTask);
        }
        catch (Exception e)
        {
            // Ensure we catch errors while running the render functions of the components.
            HandleException(e);
            return;
        }
        finally
        {
            RemoveEventHandlerIds(_batchBuilder.DisposedEventHandlerIds.ToRange(), updateDisplayTask);
            _batchBuilder.ClearStateForCurrentBatch();
            _isBatchInProgress = false;
        }

        // An OnAfterRenderAsync callback might have queued more work synchronously.
        // Note: we do *not* re-render implicitly after the OnAfterRenderAsync-returned
        // task (that would be an infinite loop). We only render after an explicit render
        // request (e.g., StateHasChanged()).
        if (_batchBuilder.ComponentRenderQueue.Count > 0)
        {
            ProcessRenderQueue();
        }
    }

    private Task InvokeRenderCompletedCalls(ArrayRange<RenderTreeDiff> updatedComponents, Task updateDisplayTask)
    {
        if (updateDisplayTask.IsCanceled)
        {
            // The display update was canceled.
            // This can be due to a timeout on the components server-side case, or the renderer being disposed.

            // The latter case is normal during prerendering, as the render never fully completes (the display never
            // gets updated, no references get populated and JavaScript interop is not available) and we simply discard
            // the renderer after producing the prerendered content.
            return Task.CompletedTask;
        }
        if (updateDisplayTask.IsFaulted)
        {
            // The display update failed so we don't care any more about running on render completed
            // fallbacks as the entire rendering process is going to be torn down.
            HandleException(updateDisplayTask.Exception);
            return Task.CompletedTask;
        }

        if (!updateDisplayTask.IsCompleted)
        {
            var updatedComponentsId = new int[updatedComponents.Count];
            var updatedComponentsArray = updatedComponents.Array;
            for (int i = 0; i < updatedComponentsId.Length; i++)
            {
                updatedComponentsId[i] = updatedComponentsArray[i].ComponentId;
            }

            return InvokeRenderCompletedCallsAfterUpdateDisplayTask(updateDisplayTask, updatedComponentsId);
        }

        List<Task> batch = null;
        var array = updatedComponents.Array;
        for (var i = 0; i < updatedComponents.Count; i++)
        {
            var componentState = GetOptionalComponentState(array[i].ComponentId);
            if (componentState != null)
            {
                NotifyRenderCompleted(componentState, ref batch);
            }
        }

        return batch != null ?
            Task.WhenAll(batch) :
            Task.CompletedTask;
    }

    private async Task InvokeRenderCompletedCallsAfterUpdateDisplayTask(
        Task updateDisplayTask,
        int[] updatedComponents)
    {
        try
        {
            await updateDisplayTask;
        }
        catch // avoiding exception filters for AOT runtimes
        {
            if (updateDisplayTask.IsCanceled)
            {
                return;
            }

            HandleException(updateDisplayTask.Exception);
            return;
        }

        List<Task> batch = null;
        var array = updatedComponents;
        for (var i = 0; i < updatedComponents.Length; i++)
        {
            var componentState = GetOptionalComponentState(array[i]);
            if (componentState != null)
            {
                NotifyRenderCompleted(componentState, ref batch);
            }
        }

        var result = batch != null ?
            Task.WhenAll(batch) :
            Task.CompletedTask;

        await result;
    }

    private void NotifyRenderCompleted(ComponentState state, ref List<Task> batch)
    {
        // The component might be rendered and disposed in the same batch (if its parent
        // was rendered later in the batch, and removed the child from the tree).
        // This can also happen between batches if the UI takes some time to update and within
        // that time the component gets removed out of the tree because the parent chose not to
        // render it in a later batch.
        // In any of the two cases mentioned happens, OnAfterRenderAsync won't run but that is
        // ok.
        var task = state.NotifyRenderCompletedAsync();

        // We want to avoid allocations per rendering. Avoid allocating a state machine or an accumulator
        // unless we absolutely have to.
        if (task.IsCompleted)
        {
            if (task.Status == TaskStatus.RanToCompletion || task.Status == TaskStatus.Canceled)
            {
                // Nothing to do here.
                return;
            }
            else if (task.Status == TaskStatus.Faulted)
            {
                HandleExceptionViaErrorBoundary(task.Exception, state);
                return;
            }
        }

        // The Task is incomplete.
        // Queue up the task and we can inspect it later.
        batch = batch ?? new List<Task>();
        batch.Add(GetErrorHandledTask(task, state));
    }

    private void RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
    {
        var componentState = renderQueueEntry.ComponentState;
        Log.RenderingComponent(_logger, componentState);
        componentState.RenderIntoBatch(_batchBuilder, renderQueueEntry.RenderFragment, out var renderFragmentException);
        if (renderFragmentException != null)
        {
            // If this returns, the error was handled by an error boundary. Otherwise it throws.
            HandleExceptionViaErrorBoundary(renderFragmentException, componentState);
        }

        // Process disposal queue now in case it causes further component renders to be enqueued
        ProcessDisposalQueueInExistingBatch();
    }

    private void ProcessDisposalQueueInExistingBatch()
    {
        List<Exception> exceptions = null;
        while (_batchBuilder.ComponentDisposalQueue.Count > 0)
        {
            var disposeComponentId = _batchBuilder.ComponentDisposalQueue.Dequeue();
            var disposeComponentState = GetRequiredComponentState(disposeComponentId);
            Log.DisposingComponent(_logger, disposeComponentState);

            try
            {
                var disposalTask = disposeComponentState.DisposeInBatchAsync(_batchBuilder);
                if (disposalTask.IsCompletedSuccessfully)
                {
                    // If it's a IValueTaskSource backed ValueTask,
                    // inform it its result has been read so it can reset
                    disposalTask.GetAwaiter().GetResult();
                }
                else
                {
                    // We set owningComponentState to null because we don't want exceptions during disposal to be recoverable
                    var result = disposalTask.AsTask();
                    AddToPendingTasksWithErrorHandling(GetHandledAsynchronousDisposalErrorsTask(result), owningComponentState: null);

                    async Task GetHandledAsynchronousDisposalErrorsTask(Task result)
                    {
                        try
                        {
                            await result;
                        }
                        catch (Exception e)
                        {
                            HandleException(e);
                        }
                    }
                }
            }
            catch (Exception exception)
            {
                exceptions ??= new List<Exception>();
                exceptions.Add(exception);
            }

            _componentStateById.Remove(disposeComponentId);
            _componentStateByComponent.Remove(disposeComponentState.Component);
            _batchBuilder.DisposedComponentIds.Append(disposeComponentId);
        }

        if (exceptions?.Count > 1)
        {
            HandleException(new AggregateException("Exceptions were encountered while disposing components.", exceptions));
        }
        else if (exceptions?.Count == 1)
        {
            HandleException(exceptions[0]);
        }
    }

    private void RemoveEventHandlerIds(ArrayRange<ulong> eventHandlerIds, Task afterTaskIgnoreErrors)
    {
        if (eventHandlerIds.Count == 0)
        {
            return;
        }

        if (afterTaskIgnoreErrors.IsCompleted)
        {
            var array = eventHandlerIds.Array;
            var count = eventHandlerIds.Count;
            for (var i = 0; i < count; i++)
            {
                var eventHandlerIdToRemove = array[i];
                _eventBindings.Remove(eventHandlerIdToRemove);
                _eventHandlerIdReplacements.Remove(eventHandlerIdToRemove);
            }
        }
        else
        {
            _ = ContinueAfterTask(eventHandlerIds, afterTaskIgnoreErrors);
        }

        // Factor out the async part into a separate local method purely so, in the
        // synchronous case, there's no state machine or task construction
        async Task ContinueAfterTask(ArrayRange<ulong> eventHandlerIds, Task afterTaskIgnoreErrors)
        {
            // We need to delay the actual removal (e.g., until we've confirmed the client
            // has processed the batch and hence can be sure not to reuse the handler IDs
            // any further). We must clone the data because the underlying RenderBatchBuilder
            // may be reused and hence modified by an unrelated subsequent batch.
            var eventHandlerIdsClone = eventHandlerIds.Clone();

            try
            {
                await afterTaskIgnoreErrors;
            }
            catch (Exception)
            {
                // As per method contract, we're not error-handling the task.
                // That remains the caller's business.
            }

            // We know the next execution will complete synchronously, so no infinite loop
            RemoveEventHandlerIds(eventHandlerIdsClone, Task.CompletedTask);
        }
    }

    // We do not want the debugger to stop more than once per user-unhandled exception.
    [DebuggerDisableUserUnhandledExceptions]
    private async Task GetErrorHandledTask(Task taskToHandle, ComponentState? owningComponentState)
    {
        try
        {
            await taskToHandle;
        }
        catch (Exception ex)
        {
            // Ignore errors due to task cancellations.
            if (!taskToHandle.IsCanceled)
            {
                HandleExceptionViaErrorBoundary(ex, owningComponentState);
            }
        }
    }

    private void UpdateRenderTreeToMatchClientState(ulong eventHandlerId, EventFieldInfo fieldInfo)
    {
        var componentState = GetOptionalComponentState(fieldInfo.ComponentId);
        if (componentState != null)
        {
            RenderTreeUpdater.UpdateToMatchClientState(
                componentState.CurrentRenderTree,
                eventHandlerId,
                fieldInfo.FieldValue);
        }
    }

    internal void HandleComponentException(Exception exception, int componentId)
        => HandleExceptionViaErrorBoundary(exception, GetRequiredComponentState(componentId));

    /// <summary>
    /// If the exception can be routed to an error boundary around <paramref name="errorSourceOrNull"/>, do so.
    /// Otherwise handle it as fatal.
    /// </summary>
    private void HandleExceptionViaErrorBoundary(Exception error, ComponentState? errorSourceOrNull)
    {
        // We only get here in specific situations. Currently, all of them are when we're
        // already on the sync context (and if not, we have a bug we want to know about).
        Dispatcher.AssertAccess();

        // We don't allow NavigationException instances to be caught by error boundaries.
        // These are special exceptions whose purpose is to be as invisible as possible to
        // user code and bubble all the way up to get handled by the framework as a redirect.
        if (error is NavigationException)
        {
            HandleException(error);
            return;
        }

        // Find the closest error boundary, if any
        var candidate = errorSourceOrNull;
        while (candidate is not null)
        {
            if (candidate.Component is IErrorBoundary errorBoundary)
            {
                // Don't just trust the error boundary to dispose its subtree - force it to do so by
                // making it render an empty fragment. Ensures that failed components don't continue to
                // operate, which would be a whole new kind of edge case to support forever.
                AddToRenderQueue(candidate.ComponentId, builder => { });

                try
                {
                    errorBoundary.HandleException(error);
                }
                catch (Exception errorBoundaryException)
                {
                    // If *notifying* about an exception fails, it's OK for that to be fatal
                    HandleException(errorBoundaryException);
                }

                return; // Handled successfully
            }

            candidate = candidate.LogicalParentComponentState;
        }

        // It's unhandled, so treat as fatal
        HandleException(error);
    }

    /// <summary>
    /// Releases all resources currently used by this <see cref="Renderer"/> instance.
    /// </summary>
    /// <param name="disposing"><see langword="true"/> if this method is being invoked by <see cref="IDisposable.Dispose"/>, otherwise <see langword="false"/>.</param>
    protected virtual void Dispose(bool disposing)
    {
        // Unlike other Renderer APIs, we need Dispose to be thread-safe 
        // (and not require being called only from the sync context) 
        // because other classes many need to dispose a Renderer during their own Dispose (rather than DisposeAsync) 
        // and we don't want to force that other code to deal with calling InvokeAsync from a synchronous method.
        lock (_lockObject)
        {
            if (_rendererIsDisposed)
            {
                // quitting synchronously as soon as possible is avoiding
                // possible async dispatch to another thread and
                // possible deadlock on synchronous `done.Wait()` below.
                return;
            }
        }

        if (!Dispatcher.CheckAccess())
        {
            // It's important that we only call the components' Dispose/DisposeAsync lifecycle methods
            // on the sync context, like other lifecycle methods. In almost all cases we'd already be
            // on the sync context here since DisposeAsync dispatches, but just in case someone is using
            // Dispose directly, we'll dispatch and block.
            var done = Dispatcher.InvokeAsync(() => Dispose(disposing));

            // only block caller when this is not finalizer
            if (disposing)
            {
                done.Wait();
            }

            return;
        }

        lock (_lockObject)
        {
            _rendererIsDisposed = true;
        }

        if (_hotReloadInitialized && HotReloadManager.MetadataUpdateSupported)
        {
            HotReloadManager.OnDeltaApplied -= RenderRootComponentsOnHotReload;
        }

        // It's important that we handle all exceptions here before reporting any of them.
        // This way we can dispose all components before an error handler kicks in.
        List<Exception> exceptions = null;
        List<Task> asyncDisposables = null;
        foreach (var componentState in _componentStateById.Values)
        {
            Log.DisposingComponent(_logger, componentState);

            try
            {
                var task = componentState.DisposeAsync();
                if (task.IsCompletedSuccessfully)
                {
                    // If it's a IValueTaskSource backed ValueTask,
                    // inform it its result has been read so it can reset
                    task.GetAwaiter().GetResult();
                }
                else
                {
                    asyncDisposables ??= new();
                    asyncDisposables.Add(task.AsTask());
                }
            }
            catch (Exception exception)
            {
                exceptions ??= new List<Exception>();
                exceptions.Add(exception);
            }
        }

        _componentStateById.Clear(); // So we know they were all disposed
        _componentStateByComponent.Clear();
        _batchBuilder.Dispose();

        NotifyExceptions(exceptions);

        if (asyncDisposables?.Count >= 1)
        {
            _disposeTask = HandleAsyncExceptions(asyncDisposables);
        }

        async Task HandleAsyncExceptions(List<Task> tasks)
        {
            List<Exception> asyncExceptions = null;
            foreach (var task in tasks)
            {
                try
                {
                    await task;
                }
                catch (Exception exception)
                {
                    asyncExceptions ??= new List<Exception>();
                    asyncExceptions.Add(exception);
                }
            }

            NotifyExceptions(asyncExceptions);
        }

        void NotifyExceptions(List<Exception> exceptions)
        {
            if (exceptions?.Count > 1)
            {
                HandleException(new AggregateException("Exceptions were encountered while disposing components.", exceptions));
            }
            else if (exceptions?.Count == 1)
            {
                HandleException(exceptions[0]);
            }
        }
    }

    /// <summary>
    /// Determines how to handle an <see cref="IComponentRenderMode"/> when obtaining a component instance.
    /// This is only called when a render mode is specified either at the call site or on the component type.
    ///
    /// Subclasses may override this method to return a component of a different type, or throw, depending on whether the renderer
    /// supports the render mode and how it implements that support.
    /// </summary>
    /// <param name="componentType">The type of component that was requested.</param>
    /// <param name="parentComponentId">The parent component ID, or null if it is a root component.</param>
    /// <param name="componentActivator">An <see cref="IComponentActivator"/> that should be used when instantiating component objects.</param>
    /// <param name="renderMode">The <see cref="IComponentRenderMode"/> declared on <paramref name="componentType"/> or at the call site (for example, by the parent component).</param>
    /// <returns>An <see cref="IComponent"/> instance.</returns>
    protected internal virtual IComponent ResolveComponentForRenderMode(
        [DynamicallyAccessedMembers(Component)] Type componentType,
        int? parentComponentId,
        IComponentActivator componentActivator,
        IComponentRenderMode renderMode)
    {
        // Nothing is supported by default. Subclasses must override this to opt into supporting specific render modes.
        throw new NotSupportedException($"Cannot supply a component of type '{componentType}' because the current platform does not support the render mode '{renderMode}'.");
    }

    /// <summary>
    /// Releases all resources currently used by this <see cref="Renderer"/> instance.
    /// </summary>
    public void Dispose()
    {
        Dispose(disposing: true);
    }

    /// <inheritdoc />
    public async ValueTask DisposeAsync()
    {
        lock (_lockObject)
        {
            if (_rendererIsDisposed)
            {
                return;
            }
        }

        if (_disposeTask != null)
        {
            await _disposeTask;
        }
        else
        {
            await Dispatcher.InvokeAsync(Dispose);

            if (_disposeTask != null)
            {
                await _disposeTask;
            }
            else
            {
                await default(ValueTask);
            }
        }
    }
}
