using System;
using System.Collections.Generic;
using System.Linq;

namespace Quadarax.Foundation.Core.Mapper
{
    /// <summary>
    /// Lightweight Object-to-Object Mapper.
    /// </summary>
    public class Loom
    {
        /// <summary>
        /// Defines a new mapping function using a configuration-driven builder.
        /// </summary>
        /// <typeparam name="TFrom">The source type.</typeparam>
        /// <typeparam name="TTo">The destination type.</typeparam>
        /// <returns></returns>
        public LoomBuilder<TFrom, TTo> Define<TFrom, TTo>() 
            where TFrom : class
            where TTo : class, new()
        {
            return new LoomBuilder<TFrom, TTo>(this);
        }

        /// <summary>
        /// Defines a new mapping function.
        /// </summary>
        /// <typeparam name="TFrom">The source type.</typeparam>
        /// <typeparam name="TTo">The destination type.</typeparam>
        /// <param name="map">The mapping function.</param>
        public void Define<TFrom, TTo>(Action<TFrom, TTo> map)
        {
            Dictionary<Type, Delegate> inner;
            if (!_maps.TryGetValue(typeof(TFrom), out inner))
            {
                inner = new Dictionary<Type, Delegate>();
                _maps[typeof(TFrom)] = inner;
            }
            inner[typeof(TTo)] = map;
        }

        /// <summary>
        /// Finds a mapping function.
        /// </summary>
        /// <typeparam name="TFrom">The source type.</typeparam>
        /// <typeparam name="TTo">The destination type.</typeparam>
        /// <returns>The mapping function for the given combination of types or a null reference when not defined.</returns>
        public Action<TFrom, TTo> Find<TFrom, TTo>()
        {
            Dictionary<Type, Delegate> inner;
            if (!_maps.TryGetValue(typeof(TFrom), out inner)) return null;
            Delegate map;
            if (!inner.TryGetValue(typeof(TTo), out map)) return null;
            return (Action<TFrom, TTo>) map;
        }

        /// <summary>
        /// Updates the destination object from the source using a mapping function.
        /// </summary>
        /// <typeparam name="TFrom">The source type.</typeparam>
        /// <typeparam name="TTo">The destination type.</typeparam>
        /// <param name="from">The source object.</param>
        /// <param name="to">The destination object.</param>
        public void Update<TFrom, TTo>(TFrom from, TTo to)
            where TFrom : class
            where TTo : class
        {
            if (from == null) throw new ArgumentNullException("from");
            if (to == null) throw new ArgumentNullException("to");
            Find<TFrom, TTo>()(from, to);
        }

        /// <summary>
        /// Creates a destination object from the source using a mapping function.
        /// </summary>
        /// <typeparam name="TFrom">The source type.</typeparam>
        /// <typeparam name="TTo">The destination type.</typeparam>
        /// <param name="from">The source object.</param>
        /// <returns>The destination object or a null reference in case <paramref name="from"/> is a null reference, too.</returns>
        public TTo Map<TFrom, TTo>(TFrom from)
            where TFrom : class
            where TTo : class, new()
        {
            if (from == null) return null;
            var to = new TTo();
            Find<TFrom, TTo>()(from, to);
            return to;
        }

        /// <summary>
        /// Creates a list of destination objects from source objects using a mapping function.
        /// </summary>
        /// <typeparam name="TFrom">The source type.</typeparam>
        /// <typeparam name="TTo">The destination type.</typeparam>
        /// <param name="from">The source objects.</param>
        /// <returns>The destination objects.</returns>
        public List<TTo> Map<TFrom, TTo>(IEnumerable<TFrom> from)
            where TFrom : class
            where TTo : class, new()
        {
            if (from == null) return null;
            var map = Find<TFrom, TTo>();
            return new List<TTo>(from.Select(f => { 
                var to = new TTo();
                map(f, to);
                return to;
            }));
        }

        /// <summary>
        /// Creates a list of destination objects from source objects using a mapping function.
        /// </summary>
        /// <typeparam name="TFrom">The source type.</typeparam>
        /// <typeparam name="TTo">The destination type.</typeparam>
        /// <param name="from">The source objects.</param>
        /// <returns>The destination objects.</returns>
        public List<TTo> MapList<TFrom, TTo>(IEnumerable<TFrom> from)
            where TFrom : class
            where TTo : class, new()
        {
            if (from == null) return null;
            var map = Find<TFrom, TTo>();
            return from.Select(f => New(map, f)).ToList();
        }


        /// <summary>
        /// Creates a list of destination objects from source objects using a mapping function.
        /// </summary>
        /// <typeparam name="TFrom">The source type.</typeparam>
        /// <typeparam name="TTo">The destination type.</typeparam>
        /// <param name="from">The source objects.</param>
        /// <returns>The destination objects.</returns>
        public List<TTo> MapList<TFrom, TTo>(IQueryable<TFrom> from)
            where TFrom : class
            where TTo : class, new()
        {
            if (from == null) return null;
            var map = Find<TFrom, TTo>();
            return from.Select(f => New(map, f)).ToList();
        }

        #region ------ Internals ------------------------------------------------------------------

        private TTo New<TFrom, TTo>(Action<TFrom, TTo> map, TFrom f)
            where TFrom : class
            where TTo : class, new()
        {
            var to = new TTo();
            map(f, to);
            return to;
        }

        /// <summary>
        /// Repository of all known mapping functions.
        /// </summary>
        private readonly Dictionary<Type, Dictionary<Type, Delegate>> _maps = new Dictionary<Type, Dictionary<Type, Delegate>>();

        #endregion
    }
}