/// <summary>
/// Manages weak references to <see cref="INotifyPropertyChanged"/>
/// subscribers and provides a friendly interface for <see cref="INotifyPropertyChanged"/>
/// implementors to expose typed event subscription without relying on
/// property string names.
/// </summary>
/// <remarks>
/// This class never leaks references to subscribers.
/// </remarks>
/// <typeparam name="TSource">The type of the property changed source.</typeparam>
public class PropertyChangeManager<TSource>
where TSource : INotifyPropertyChanged
{
private List<Subscription> subscriptions = new List<Subscription>();
private TSource source;

/// <summary>
/// Initializes a new instance of the <see cref="PropertyChangeManager&lt;TSource&gt;"/> class.
/// </summary>
/// <param name="source">The property changed source.</param>
public PropertyChangeManager(TSource source)
{
this.source = source;
}

/// <summary>
/// Subscribes to changes in the property referenced in the given
/// <paramref name="propertyExpression"/> with the given
/// <paramref name="callbackAction"/> delegate.
/// </summary>
/// <param name="propertyExpression">A lambda expression that accesses a property, such as <c>x => x.Name</c>
/// (where the type of x is <typeparamref name="TSource"/>).</param>
/// <param name="callbackAction">The callback action to invoke when the given property changes.</param>
public IDisposable SubscribeChanged(Expression<Func<TSource, object>> propertyExpression, Action<TSource> callbackAction)
{
return AddSubscription(new Subscription
{
IsStatic = callbackAction.Target == null,
PropertyName = Reflect<TSource>.GetProperty(propertyExpression).Name,
SubscriberReference = new WeakReference(callbackAction.Target),
MethodCallback = callbackAction.Method
});
}

/// <summary>
/// Registers a regular event handler for change notification.
/// </summary>
public IDisposable AddHandler(PropertyChangedEventHandler handler)
{
return AddSubscription(new Subscription
{
IsStatic = handler.Target == null,
SubscriberReference = new WeakReference(handler.Target),
MethodCallback = handler.Method
});
}

/// <summary>
/// Unregisters the given event handler from change notification.
/// </summary>
/// <param name="handler">The value.</param>
public void RemoveHandler(PropertyChangedEventHandler handler)
{
CleanupSubscribers();

subscriptions.RemoveAll(s => s.SubscriberReference.Target == handler.Target && s.MethodCallback == handler.Method);
}

/// <summary>
/// Notifies subscribers that the given property has changed.
/// </summary>
/// <param name="propertyExpression">A lambda expression that accesses a property, such as <c>x => x.Name</c>
/// (where the type of x is <typeparamref name="TSource"/>).</param>
public void NotifyChanged(Expression<Func<TSource, object>> propertyExpression)
{
CleanupSubscribers();

var propertyName = Reflect<TSource>.GetProperty(propertyExpression).Name;

foreach (var subscription in subscriptions.Where(s => s.PropertyName == propertyName))
{
try
{
subscription.MethodCallback.Invoke(subscription.SubscriberReference.Target, new object[] { this.source });
}
catch (TargetInvocationException tie)
{
tie.InnerException.RethrowWithNoStackTraceLoss();
}
}

// Call "old-style" handlers with the right signature.
foreach (var subscription in subscriptions.Where(s => s.PropertyName == null))
{
try
{
subscription.MethodCallback.Invoke(subscription.SubscriberReference.Target, new object[] { this.source, new PropertyChangedEventArgs(propertyName) });
}
catch (TargetInvocationException tie)
{
tie.InnerException.RethrowWithNoStackTraceLoss();
}
}
}

private IDisposable AddSubscription(Subscription subscription)
{
CleanupSubscribers();

subscriptions.Add(subscription);

return new SubscriptionReference(this.subscriptions, subscription);
}

private void CleanupSubscribers()
{
subscriptions.RemoveAll(s => !s.IsStatic && !s.SubscriberReference.IsAlive);
}

/// <summary>
/// Provides deterministic removal of a subscription without having to
/// create a separate class to hold the delegate reference.
/// Callers can simply keep the returned disposable from Subscribe
/// and use it to unsubscribe.
/// </summary>
private sealed class SubscriptionReference : IDisposable
{
private List<Subscription> subscriptions;
private Subscription entry;

public SubscriptionReference(List<Subscription> subscriptions, Subscription entry)
{
this.subscriptions = subscriptions;
this.entry = entry;
}

public void Dispose()
{
this.subscriptions.Remove(this.entry);
}
}

private class Subscription
{
public bool IsStatic { get; set; }
public string PropertyName { get; set; }
public WeakReference SubscriberReference { get; set; }
public MethodInfo MethodCallback { get; set; }
}
}

#region Helpers

/// <summary>
/// Provides strong-typed reflection of the <typeparamref name="TTarget"/>
/// type.
/// </summary>
/// <typeparam name="TTarget">Type to reflect.</typeparam>
internal static class Reflect<TTarget>
{
/// <summary>
/// Gets the method represented by the lambda expression.
/// </summary>
/// <param name="method">An expression that invokes a method.</param>
/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method invocation.</exception>
/// <returns>The method info.</returns>
public static MethodInfo GetMethod(Expression<Action<TTarget>> method)
{
return GetMethodInfo(method);
}

/// <summary>
/// Gets the method represented by the lambda expression.
/// </summary>
/// <param name="method">An expression that invokes a method.</param>
/// <typeparam name="T1">Type of the first argument.</typeparam>
/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method invocation.</exception>
/// <returns>The method info.</returns>
public static MethodInfo GetMethod<T1>(Expression<Action<TTarget, T1>> method)
{
return GetMethodInfo(method);
}

/// <summary>
/// Gets the method represented by the lambda expression.
/// </summary>
/// <param name="method">An expression that invokes a method.</param>
/// <typeparam name="T1">Type of the first argument.</typeparam>
/// <typeparam name="T2">Type of the second argument.</typeparam>
/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method invocation.</exception>
/// <returns>The method info.</returns>
public static MethodInfo GetMethod<T1, T2>(Expression<Action<TTarget, T1, T2>> method)
{
return GetMethodInfo(method);
}

/// <summary>
/// Gets the method represented by the lambda expression.
/// </summary>
/// <param name="method">An expression that invokes a method.</param>
/// <typeparam name="T1">Type of the first argument.</typeparam>
/// <typeparam name="T2">Type of the second argument.</typeparam>
/// <typeparam name="T3">Type of the third argument.</typeparam>
/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method invocation.</exception>
/// <returns>The method info.</returns>
public static MethodInfo GetMethod<T1, T2, T3>(Expression<Action<TTarget, T1, T2, T3>> method)
{
return GetMethodInfo(method);
}

/// <summary>
/// Gets the property represented by the lambda expression.
/// </summary>
/// <param name="property">An expression that accesses a property.</param>
/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a property access.</exception>
/// <returns>The property info.</returns>
public static PropertyInfo GetProperty(Expression<Func<TTarget, object>> property)
{
PropertyInfo info = GetMemberInfo(property) as PropertyInfo;
if (info == null)
{
throw new ArgumentException("Member is not a property");
}

return info;
}

/// <summary>
/// Gets the field represented by the lambda expression.
/// </summary>
/// <param name="field">An expression that accesses a field.</param>
/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a field access.</exception>
/// <returns>The field info.</returns>
public static FieldInfo GetField(Expression<Func<TTarget, object>> field)
{
FieldInfo info = GetMemberInfo(field) as FieldInfo;
if (info == null)
{
throw new ArgumentException("Member is not a field");
}

return info;
}

private static MethodInfo GetMethodInfo(Expression method)
{
if (method == null)
{
throw new ArgumentNullException("method");
}

LambdaExpression lambda = method as LambdaExpression;
if (lambda == null)
{
throw new ArgumentException("Not a lambda expression", "method");
}

if (lambda.Body.NodeType != ExpressionType.Call)
{
throw new ArgumentException("Not a method call", "method");
}

return ((MethodCallExpression)lambda.Body).Method;
}

private static MemberInfo GetMemberInfo(Expression member)
{
if (member == null)
{
throw new ArgumentNullException("member");
}

LambdaExpression lambda = member as LambdaExpression;
if (lambda == null)
{
throw new ArgumentException("Not a lambda expression", "member");
}

MemberExpression memberExpr = null;

// The Func<TTarget, object> we use returns an object, so first statement can be either
// a cast (if the field/property does not return an object) or the direct member access.
if (lambda.Body.NodeType == ExpressionType.Convert)
{
// The cast is an unary expression, where the operand is the
// actual member access expression.
memberExpr = ((UnaryExpression)lambda.Body).Operand as MemberExpression;
}
else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
{
memberExpr = lambda.Body as MemberExpression;
}

if (memberExpr == null)
{
throw new ArgumentException("Not a member access", "member");
}

return memberExpr.Member;
}
}

/// <summary>
/// Utility methods for exceptions.
/// </summary>
public static class ExceptionExtensions
{
private static readonly FieldInfo remoteStackTraceString =
typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.Instance | BindingFlags.NonPublic) ??
typeof(Exception).GetField("remote_stack_trace", BindingFlags.Instance | BindingFlags.NonPublic);

/// <summary>
/// Rethrows an exception object without losing the existing stack trace information.
/// </summary>
/// <param name="ex">The exception to re-throw.</param>
/// <remarks>
/// For more information on this technique, see
/// http://www.dotnetjunkies.com/WebLog/chris.taylor/archive/2004/03/03/8353.aspx
/// </remarks>
public static void RethrowWithNoStackTraceLoss(this Exception ex)
{
remoteStackTraceString.SetValue(ex, ex.StackTrace + Environment.NewLine);

throw ex;
}
}

#endregion