// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using CommunityToolkit.Mvvm.Messaging.Internals;
namespace CommunityToolkit.Mvvm.Messaging;
///
/// A class providing a reference implementation for the interface.
///
///
///
/// This implementation uses weak references to track the registered
/// recipients, so it is not necessary to manually unregister them when they're no longer needed.
///
///
/// The type will automatically perform internal trimming when
/// full GC collections are invoked, so calling manually is not necessary to
/// ensure that on average the internal data structures are as trimmed and compact as possible.
///
///
public sealed class WeakReferenceMessenger : IMessenger
{
// This messenger uses the following logic to link stored instances together:
// --------------------------------------------------------------------------------------------------------
// Dictionary2 mapping
// / / /
// ___(Type2.TToken)___/ / / ___(if Type2.TToken is Unit)
// /_________(Type2.TMessage)______________/ / /
// / _________________/___MessageHandlerDispatcher?
// / / \
// Dictionary2> recipientsMap; \___(null if using IRecipient)
// --------------------------------------------------------------------------------------------------------
// Just like in the strong reference variant, each pair of message and token types is used as a key in the
// recipients map. In this case, the values in the dictionary are ConditionalWeakTable2<,> instances, that
// link each registered recipient to a map of currently registered handlers, through dependent handles. This
// ensures that handlers will remain alive as long as their associated recipient is also alive (so there is no
// need for users to manually indicate whether a given handler should be kept alive in case it creates a closure).
// The value in each conditional table can either be Dictionary2 or object. The
// first case is used when any token type other than the default Unit type is used, as in this case there could be
// multiple handlers for each recipient that need to be tracked separately. In order to invoke all the handlers from
// a context where their type parameters is not known, handlers are stored as MessageHandlerDispatcher instances. There
// are two possible cases here: either a given instance is of type MessageHandlerDispatcher.For,
// or null. The first is the default case: whenever a subscription is done with a MessageHandler,
// that delegate is wrapped in an instance of this class so that it can keep track internally of the generic context in
// use, so that it can be retrieved when the callback is executed. If the subscription is done directly on a recipient
// that implements IRecipient
/// The map of currently registered recipients for all message types.
///
private readonly Dictionary2> recipientsMap = new();
///
/// Initializes a new instance of the class.
///
public WeakReferenceMessenger()
{
// Proxy function for the GC callback. This needs to be static and to take the target instance as
// an input parameter in order to avoid rooting it from the Gen2GcCallback object invoking it.
static void Gen2GcCallbackProxy(object target)
{
((WeakReferenceMessenger)target).CleanupWithNonBlockingLock();
}
// Register an automatic GC callback to trigger a non-blocking cleanup. This will ensure that the
// current messenger instance is trimmed and without leftover recipient maps that are no longer used.
// This is necessary (as in, some form of cleanup, either explicit or automatic like in this case)
// because the ConditionalWeakTable instances will just remove key-value pairs on their
// own as soon as a key (ie. a recipient) is collected, causing their own keys (ie. the Type2 instances
// mapping to each conditional table for a pair of message and token types) to potentially remain in the
// root mapping structure but without any remaining recipients actually registered there, which just
// adds unnecessary overhead when trying to enumerate recipients during broadcasting operations later on.
Gen2GcCallback.Register(Gen2GcCallbackProxy, this);
}
///
/// Gets the default instance.
///
public static WeakReferenceMessenger Default { get; } = new();
///
public bool IsRegistered(object recipient, TToken token)
where TMessage : class
where TToken : IEquatable
{
ArgumentNullException.ThrowIfNull(recipient);
ArgumentNullException.For.ThrowIfNull(token);
lock (this.recipientsMap)
{
Type2 type2 = new(typeof(TMessage), typeof(TToken));
// Get the conditional table associated with the target recipient, for the current pair
// of token and message types. If it exists, check if there is a matching token.
if (!this.recipientsMap.TryGetValue(type2, out ConditionalWeakTable2