using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace Quadarax.Foundation.Core.Mapper
{
    /// <summary>
    /// Mapping builder.
    /// </summary>
    /// <typeparam name="TFrom">The source type.</typeparam>
    /// <typeparam name="TTo">The destination type.</typeparam>
    public class LoomBuilder<TFrom, TTo> 
        where TFrom : class
        where TTo : class, new()
    {
        private ParameterExpression sourceObj;

        private ParameterExpression destObj;

        private List<Expression> rules;

        private Loom loom;

        /// <summary>
        /// Initializes a new instance of this class.
        /// </summary>
        public LoomBuilder(Loom loom)
        {
            this.loom = loom;
            sourceObj = Expression.Variable(typeof(TFrom), "source");
            destObj = Expression.Variable(typeof(TTo), "dest");
            rules = new List<Expression>();
        }

        internal class ParameterReplacer : ExpressionVisitor
        {
            private ParameterExpression oldParam;

            private ParameterExpression newParam;

            /// <summary>
            /// Initializes a new instance of this class.
            /// </summary>
            public ParameterReplacer(ParameterExpression oldParam, ParameterExpression newParam)
            {
                this.oldParam = oldParam;
                this.newParam = newParam;
            }

            public static Expression Replace(Expression expr, ParameterExpression oldParam, ParameterExpression newParam)
            {
                return new ParameterReplacer(oldParam, newParam).Visit(expr);
            }

            protected override Expression VisitParameter(ParameterExpression node)
            {
                return node.Name.Equals(oldParam.Name) ? newParam : node;
            }
        }

        public LoomBuilder<TFrom, TTo> Custom<T>(Expression<Func<TTo, T>> destSet, Expression<Func<TFrom, T>> sourceGet)
        {
            if (sourceGet == null) throw new ArgumentNullException("sourceGet");
            if (destSet == null) throw new ArgumentNullException("destSet");

            var rule = Expression.Assign(
                ParameterReplacer.Replace(destSet.Body, destSet.Parameters.Single(), destObj),
                ParameterReplacer.Replace(sourceGet.Body, sourceGet.Parameters.Single(), sourceObj));
            rules.Add(rule);
            return this;
        }

        public LoomBuilder<TFrom, TTo> PublicProperties()
        {
            var sourceProps = typeof(TFrom).GetProperties(BindingFlags.Public | BindingFlags.Instance);
            var destProps = typeof(TTo).GetProperties(BindingFlags.Public | BindingFlags.Instance);

            foreach (var sourceProp in sourceProps.Where(p => p.CanRead && p.GetGetMethod() != null))
            {
                var destProp = destProps.FirstOrDefault(p => p.Name == sourceProp.Name && p.CanWrite && p.GetSetMethod() != null);
                if (destProp != null)
                {
                    if (destProp.PropertyType.IsAssignableFrom(sourceProp.PropertyType))
                    {
                        // convert the source property's value to the destination type: required to assign into a Nullable<> destination property, and won't hurt in other cases
                        rules.Add(Expression.Assign(Expression.Property(destObj, destProp), Expression.Convert(Expression.Property(sourceObj, sourceProp), destProp.PropertyType)));
                    }
                    else
                    {
                        // source may be Nullable<T>
                        Type sourceNullabeType = Nullable.GetUnderlyingType(sourceProp.PropertyType);
                        if (sourceNullabeType != null && destProp.PropertyType.IsAssignableFrom(sourceNullabeType))
                        {
                            rules.Add(Expression.IfThen(
                                Expression.Property(Expression.Property(sourceObj, sourceProp), "HasValue"),
                                Expression.Assign(Expression.Property(destObj, destProp), Expression.Property(Expression.Property(sourceObj, sourceProp), "Value"))));
                        }
                    }
                }
            }

            return this;
        }

        public Func<TFrom, TTo> Compile()
        {
            var parameters = new[] {sourceObj, destObj};
            var body = Expression.Block(rules);
            var map = Expression.Lambda<Action<TFrom, TTo>>(body, parameters).Compile();
            loom.Define(map);
            return loom.Map<TFrom, TTo>;
        }
    }
}