using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
namespace BindingSample
{
    /// <summary>
    ///  A bind engine supports custom data binding.
    /// </summary>
    /// <remarks>
    /// It exports two methods for data binding:
    /// 1.SetPropertyBinding
    /// 2.ClearPropertyBinding
    /// The binding engine use weak reference to hold the dependent, that means if u miss to clear data binding,
    /// the dependent will not leak memory.
    /// </remarks>
    /// <author>
    ///  yohan zhou 
    ///http://www.cnblogs.com/zhouyongh
    /// </author>
    public class BindingEngine
    {
        #region Field
        private static Dictionary<WeakEntry, Delegate> _expressionSources = new Dictionary<WeakEntry, Delegate>();
        #endregion
        /// <summary>
        /// Sets the property binding.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="target">The target.</param>
        /// <param name="sourceProp">The source prop.</param>
        /// <param name="targetProp">The target prop.</param>
        /// <param name="converter">The converter.</param>
        /// <param name="parameter">The converter parameter.</param>
        public static void SetPropertyBinding(Object source, INotifyPropertyChanged target, string sourceProp, string targetProp, IDataConverter converter = null, object parameter = null)
        {
            SetPropertyBinding(source, target, sourceProp, targetProp, true, converter, parameter);
        }
        /// <summary>
        /// Sets the property binding.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="target">The target.</param>
        /// <param name="sourceProp">The source prop.</param>
        /// <param name="targetProp">The target prop.</param>
        /// <param name="notify">if set to <c>true</c> update immediately.</param>
        /// <param name="converter">The converter.</param>
        /// <param name="parameter">The converter parameter.</param>
        public static void SetPropertyBinding(Object source, INotifyPropertyChanged target, string sourceProp, string targetProp, bool notify, IDataConverter converter = null, object parameter = null)
        {
            WeakEntry entry = new WeakEntry(source.GetType(), target.GetType(), sourceProp, targetProp);
            Delegate setAction = GetExpressionAction(entry, source, true, converter);
            WeakSource wSource = WeakSource.Register(source, target, setAction, sourceProp, targetProp, converter, parameter);
            if (notify)
            {
                wSource.NotifyPropertyChanged(target, targetProp);
            }
        }
        /// <summary>
        /// Clears the property binding.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="target">The target.</param>
        /// <param name="sourceProp">The source prop.</param>
        /// <param name="targetProp">The target prop.</param>
        public static void ClearPropertyBinding(Object source, INotifyPropertyChanged target, string sourceProp, string targetProp)
        {
            WeakSource.UnRegister(source, target, sourceProp, targetProp);
        }
        /// <summary>
        /// Gets the expression action.
        /// </summary>
        /// <param name="entry">The entry.</param>
        /// <param name="source">The source.</param>
        /// <param name="createNew">if set to <c>true</c> [create new].</param>
        /// <param name="converter">The converter.</param>
        /// <returns></returns>
        private static Delegate GetExpressionAction(WeakEntry entry, object source, bool createNew, IDataConverter converter = null)
        {
            Delegate action = null;
            if (_expressionSources.ContainsKey(entry))
            {
                action = _expressionSources[entry];
            }
            else if (createNew)
            {
                /////  Code of the below Expression Tree  ////////////////////////////////
                //if (converter != null)
                //{
                //    target.Property = converter.Convert(source.Property, parameter);
                //}
                //else
                //{
                //    if (target.Property.GetType() == source.Property.GetType())
                //    {
                //        target.Property = source.Property;
                //    }
                //    else
                //    {
                //        throw new InvalidOperationException("The property type between binding source and target does not match, please use IDataConverter to do custom convert.");
                //    }  
                //}
                //////////////////////////////////////////////////////////////////////////
                //Set Property
                var prop = entry.SourceType.GetProperty(entry.SourceProp);
                var paraObj = Expression.Parameter(entry.SourceType);
                //Get Property
                var targetProperty = entry.TargetType.GetProperty(entry.TargetProp);
                var paraTarget = Expression.Parameter(entry.TargetType);
                var getter = Expression.Property(paraTarget, targetProperty);
                //Combine
                Expression boy;
                var paraConvert = Expression.Variable(typeof(IDataConverter));
                var paraParameter = Expression.Variable(typeof(object));
                boy = Expression.IfThenElse(
                        Expression.NotEqual(paraConvert, Expression.Constant(null)),
                        Expression.Call(paraObj, prop.GetSetMethod(), Expression.Convert(Expression.Call(paraConvert, typeof(IDataConverter).GetMethod("Convert"),
                                            Expression.Convert(getter, typeof(object)), Expression.Convert(paraParameter, typeof(object))), prop.PropertyType)),
                        Expression.IfThenElse(
                            Expression.Equal(Expression.Constant(prop.PropertyType, typeof(Type)), Expression.Constant(getter.Type, typeof(Type))),
                            Expression.Call(paraObj, prop.GetSetMethod(), Expression.Convert(Expression.Convert(getter, typeof(object)), prop.PropertyType)),
                            Expression.Throw(Expression.Constant(new InvalidOperationException(
                                "The property type between binding source and target does not match, please use IDataConverter to do custom convert.")))));
                action = Expression.Lambda(boy, paraObj, paraTarget, paraConvert, paraParameter).Compile();
                _expressionSources.Add(entry, action);
            }
            return action;
        }
        private struct WeakEntry
        {
            public Type SourceType;
            public Type TargetType;
            public string SourceProp;
            public string TargetProp;
            /// <summary>
            /// Initializes a new instance of the <see cref="WeakEntry"/> struct.
            /// </summary>
            /// <param name="sourceType">Type of the source.</param>
            /// <param name="targetType">Type of the target.</param>
            /// <param name="sourceProp">The source prop.</param>
            /// <param name="targetProp">The target prop.</param>
            public WeakEntry(Type sourceType, Type targetType, string sourceProp, string targetProp)
            {
                SourceType = sourceType;
                TargetType = targetType;
                SourceProp = sourceProp;
                TargetProp = targetProp;
            }
            /// <summary>
            /// Returns the hash code for this instance.
            /// </summary>
            /// <returns>
            /// A 32-bit signed integer that is the hash code for this instance.
            /// </returns>
            public override int GetHashCode()
            {
                return SourceType.GetHashCode() ^ TargetType.GetHashCode() ^ SourceProp.GetHashCode() ^ TargetProp.GetHashCode();
            }
            /// <summary>
            /// Indicates whether this instance and a specified object are equal.
            /// </summary>
            /// <param name="obj">Another object to compare to.</param>
            /// <returns>
            /// true if <paramref name="obj"/> and this instance are the same type and represent the same value; otherwise, false.
            /// </returns>
            public override bool Equals(object obj)
            {
                WeakEntry entry = (WeakEntry)obj;
                return ((this.SourceType == entry.SourceType) && (this.TargetType == entry.TargetType) &&
                    (this.SourceProp == entry.SourceProp) && (this.TargetProp == entry.TargetProp));
            }
            /// <summary>
            /// Implements the operator ==.
            /// </summary>
            /// <param name="obj1">The obj1.</param>
            /// <param name="obj2">The obj2.</param>
            /// <returns>The result of the operator.</returns>
            public static bool operator ==(WeakEntry obj1, WeakEntry obj2)
            {
                return obj1.Equals(obj2);
            }
            /// <summary>
            /// Implements the operator !=.
            /// </summary>
            /// <param name="obj1">The obj1.</param>
            /// <param name="obj2">The obj2.</param>
            /// <returns>The result of the operator.</returns>
            public static bool operator !=(WeakEntry obj1, WeakEntry obj2)
            {
                return !(obj1 == obj2);
            }
        }
        private class WeakAction
        {
            public string SourceProp;
            public Delegate Action;
            public IDataConverter Converter;
            public object Parameter;
            public WeakAction(string sourceProp, Delegate action, IDataConverter converter, object parameter)
            {
                System.Diagnostics.Debug.Assert(action != null);
                SourceProp = sourceProp;
                Action = action;
                Converter = converter;
                Parameter = parameter;
            }
        }
        private class WeakSource : WeakReference
        {
            private static Dictionary<int, WeakSource> _weakSources = new Dictionary<int, WeakSource>();
            public Dictionary<int, Dictionary<string, IList<WeakAction>>> Targets = new Dictionary<int, Dictionary<string, IList<WeakAction>>>();
            /// <summary>
            /// Registers the specified source.
            /// </summary>
            /// <param name="source">The source.</param>
            /// <param name="target">The target.</param>
            /// <param name="action">The action.</param>
            /// <param name="sourceProp">The source prop.</param>
            /// <param name="targetProp">The target prop.</param>
            /// <param name="converter">The converter.</param>
            /// <param name="parameter">The converter parameter.</param>
            /// <returns></returns>
            public static WeakSource Register(Object source, INotifyPropertyChanged target, Delegate action, string sourceProp, string targetProp, IDataConverter converter = null, object parameter = null)
            {
                WeakSource wSource = _weakSources.ContainsKey(source.GetHashCode()) ? _weakSources[source.GetHashCode()] : null;
                if (wSource == null)
                {
                    wSource = new WeakSource(source);
                    _weakSources.Add(source.GetHashCode(), wSource);
                }
                Dictionary<string, IList<WeakAction>> targetActions;
                int id = target.GetHashCode();
                if (wSource.Targets.ContainsKey(id))
                {
                    targetActions = wSource.Targets[id];
                }
                else
                {
                    targetActions = new Dictionary<string, IList<WeakAction>>();
                    wSource.Targets.Add(id, targetActions);
                    target.PropertyChanged += new PropertyChangedEventHandler(wSource.HandlePropertyChanged);
                }
                WeakAction wAction = new WeakAction(sourceProp, action, converter, parameter);
                if (targetActions.ContainsKey(targetProp))
                {
                    var actions = targetActions[targetProp];
                    var oldAction = actions.FirstOrDefault(item => string.Equals(item.SourceProp, sourceProp));
                    if (oldAction != null)
                    {
                        actions.Remove(oldAction);
                    }
                    actions.Add(wAction);
                }
                else
                {
                    targetActions.Add(targetProp, new List<WeakAction>() { wAction });
                }
                return wSource;
            }
            /// <summary>
            /// Unregister the specified source.
            /// </summary>
            /// <param name="source">The source.</param>
            /// <param name="target">The target.</param>
            /// <param name="targetProp">The target prop.</param>
            public static void UnRegister(Object source, INotifyPropertyChanged target, string sourceProp, string targetProp)
            {
                WeakSource wSource = _weakSources.ContainsKey(source.GetHashCode()) ? _weakSources[source.GetHashCode()] : null;
                int targetId = target.GetHashCode();
                if (wSource != null && wSource.Targets.ContainsKey(targetId))
                {
                    Dictionary<string, IList<WeakAction>> actions = wSource.Targets[targetId];
                    if (actions.ContainsKey(targetProp))
                    {
                        var targetActions = actions[targetProp];
                        var action = targetActions.FirstOrDefault(item => string.Equals(item.SourceProp, sourceProp));
                        if (action != null)
                        {
                            targetActions.Remove(action);
                        }
                        if (targetActions.Count == 0)
                        {
                            actions.Remove(targetProp);
                        }
                        if (actions.Count == 0)
                        {
                            wSource.Targets.Remove(targetId);
                        }
                    }
                }
            }
            /// <summary>
            /// Initializes a new instance of the <see cref="WeakSource"/> class.
            /// </summary>
            /// <param name="source">The source.</param>
            private WeakSource(Object source)
                : base(source)
            {
            }
            /// <summary>
            /// Notifies the property changed.
            /// </summary>
            /// <param name="target">The target.</param>
            /// <param name="targetProp">The target prop.</param>
            public void NotifyPropertyChanged(INotifyPropertyChanged target, string targetProp)
            {
                int id = target.GetHashCode();
                if (!Targets.ContainsKey(id))
                {
                    return;
                }
                if (Targets[id].ContainsKey(targetProp))
                {
                    foreach (var action in Targets[id][targetProp])
                    {
                        action.Action.DynamicInvoke(this.Target, target, action.Converter, action.Parameter);                    
                    }
                }
            }
            /// <summary>
            /// Handles the property changed.
            /// </summary>
            /// <param name="sender">The sender.</param>
            /// <param name="args">The <see cref="System.ComponentModel.PropertyChangedEventArgs"/> instance containing the event data.</param>
            public void HandlePropertyChanged(object sender, PropertyChangedEventArgs args)
            {
                INotifyPropertyChanged target = sender as INotifyPropertyChanged;
                if (target == null)
                {
                    throw new NotSupportedException("WeakSource can only work on INotifyPropertyChanged");
                }
                if (IsAlive)
                {
                    NotifyPropertyChanged(target, args.PropertyName);
                }
                else
                {
                    target.PropertyChanged -= new PropertyChangedEventHandler(HandlePropertyChanged);
                    Targets.Remove(target.GetHashCode());
                }
            }
        }
    }
    public interface IDataConverter
    {
        object Convert(object value, object parameter);
    }
}