using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using System.Data.Common;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using EasyDevCore.Common;
using Microsoft.EntityFrameworkCore.Storage;

namespace EasyDevCore.Database.EntityFrameworkCore
{
    /// <summary>
    /// Mapper <see cref="DbDataReader" /> to model of type />
    /// </summary>
    /// <typeparam name="T">Model type</typeparam>
    public class DbDataReaderMapper<T> where T : class, new()
    {
        private struct Prop
        {
            public int ColumnOrdinal;
            public string ColumnName;
            public Action<object, object> Setter { get; set; }
        }

        /// <summary>
        /// Contains different columns set information mapped to type <typeparamref name="T"/>.
        /// </summary>
        private static readonly ConcurrentDictionary<ulong, Prop[]> PropertiesCache = new ConcurrentDictionary<ulong, Prop[]>();

        private readonly DbDataReader _reader;
        private readonly Prop[] _properties;

        /// <summary>
        /// Initializes a new instance of the <see cref="DbDataReaderMapper{T}"/> class.
        /// </summary>
        /// <param name="reader">The reader.</param>
        public DbDataReaderMapper(RelationalDataReader reader) : this(reader.DbDataReader)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="DbDataReaderMapper{T}" /> class.
        /// </summary>
        /// <param name="reader">The reader.</param>
        public DbDataReaderMapper(DbDataReader reader)
        {
            _reader = reader;
            _properties = MapColumnsToProperties();
        }

        /// <summary>
        /// Gets the rows.
        /// </summary>
        /// <returns></returns>
        public IEnumerable<T> GetRows()
        {
            while (_reader.Read())
            {
                yield return MapNextRow();
            }
        }

        /// <summary>
        /// Gets the rows asynchronous.
        /// </summary>
        /// <param name="cancellationToken">The cancellation token.</param>
        /// <returns></returns>
        public async IAsyncEnumerable<T> GetRowsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
        {
            while (await _reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                yield return await MapNextRowAsync().ConfigureAwait(false);
            }
        }

        /// <summary>
        /// Map <see cref="DbDataReader"/> to a T and apply an action on it for each row
        /// </summary>
        /// <param name="action">Action to apply to each row</param>
        public void Map(Action<T> action)
        {
            while (_reader.Read())
            {
                T row = MapNextRow();
                action(row);
            }
        }

        /// <summary>
        /// Map <see cref="DbDataReader"/> to a T and apply an action on it for each row
        /// </summary>
        /// <param name="action">Action to apply to each row</param>
        public Task MapAsync(Action<T> action)
        {
            return MapAsync(action, CancellationToken.None);
        }

        /// <summary>
        /// Map <see cref="DbDataReader"/> to a T and apply an action on it for each row
        /// </summary>
        /// <param name="action">Action to apply to each row</param>
        /// <param name="cancellationToken">The cancellation instruction, which propagates a notification that operations should be canceled</param>
        public async Task MapAsync(Action<T> action, CancellationToken cancellationToken)
        {
            while (await _reader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                T row = await MapNextRowAsync(cancellationToken).ConfigureAwait(false);
                action(row);
            }
        }

        /// <summary>
        /// Maps the next row.
        /// </summary>
        /// <returns></returns>
        public T MapNextRow()
        {
            T row = new T();
            for (int i = 0; i < _properties.Length; ++i)
            {
                object value = _reader.IsDBNull(_properties[i].ColumnOrdinal) ? null : _reader.GetValue(_properties[i].ColumnOrdinal);
                try
                {
                    _properties[i].Setter(row, value);
                }
                catch (Exception ex)
                {
                    ex.Data["Mapping"] = $"Model[{_properties[i].ColumnName}] = Column[{_reader.GetName(_properties[i].ColumnOrdinal)}]";
                    throw;
                }
            }
            return row;
        }

        /// <summary>
        /// Maps the next row asynchronous.
        /// </summary>
        /// <returns></returns>
        public Task<T> MapNextRowAsync()
        {
            return MapNextRowAsync(CancellationToken.None);
        }

        /// <summary>
        /// Maps the next row asynchronous.
        /// </summary>
        /// <param name="cancellationToken">The cancellation token.</param>
        /// <returns></returns>
        public async Task<T> MapNextRowAsync(CancellationToken cancellationToken)
        {
            T row = new T();
            for (int i = 0; i < _properties.Length; ++i)
            {
                object value = await _reader.IsDBNullAsync(_properties[i].ColumnOrdinal, cancellationToken).ConfigureAwait(false)
                    ? null
                    : _reader.GetValue(_properties[i].ColumnOrdinal);
                try
                {
                    _properties[i].Setter(row, value);
                }
                catch (Exception ex)
                {
                    ex.Data["Mapping"] = $"Column[{_reader.GetName(_properties[i].ColumnOrdinal)}] = Model[{_properties[i].ColumnName}]";
                    throw;
                }

            }
            return row;
        }

        private Prop[] MapColumnsToProperties()
        {
            Type modelType = typeof(T);
            string[] columns = new string[_reader.FieldCount];
            Type[] colTypes = new Type[_reader.FieldCount];
            for (int i = 0; i < _reader.FieldCount; ++i)
            {
                columns[i] = _reader.GetName(i);
                colTypes[i] = _reader.GetFieldType(i);
            }

            ulong propKey = CheckSumHelper.CRC64(modelType.FullName + "|" + string.Join("|", columns) + "|" + string.Join("|", colTypes.Select(c => c.FullName)));

            if (PropertiesCache.TryGetValue(propKey, out var s))
            {
                return s;
            }

            var propertyInfos = modelType.GetProperties(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanWrite && !p.GetCustomAttributes<NotMappedAttribute>().Any())
                                            .Select(p => new { Property = p, Mapping = p.GetCustomAttribute<ColumnAttribute>() });

            var mappingFields = (from c in columns.Select((v, i) => new { Index = i, Name = v, ColType = colTypes[i] })
                                 from p in propertyInfos
                                 where c.Name.Equals(p.Mapping != null ? p.Mapping.Name : p.Property.Name, StringComparison.InvariantCultureIgnoreCase)
                                 select new { c.Index, p.Property, p.Mapping, c.ColType });

            var properties = new List<Prop>();

            ParameterExpression instance = Expression.Parameter(typeof(object), "instance");
            ParameterExpression value = Expression.Parameter(typeof(object), "value");
            //MethodInfo changeTypeMethod = typeof(Convert).GetMethod("ChangeType", new[] { typeof(object), typeof(Type) });
            foreach (var fi in mappingFields)
            {
                PropertyInfo prop = fi.Property;

                // "x as T" is faster than "(T) x" if x is a reference type
                UnaryExpression instanceCast = prop.DeclaringType.IsValueType ? Expression.Convert(instance, prop.DeclaringType) : Expression.TypeAs(instance, prop.DeclaringType);
                UnaryExpression valueCast = prop.PropertyType.IsValueType ? Expression.Convert(value, prop.PropertyType) : Expression.TypeAs(value, prop.PropertyType); MethodCallExpression setterCall = Expression.Call(instanceCast, prop.GetSetMethod(), valueCast);

                var setter = (Action<object, object>)Expression.Lambda(setterCall, instance, value).Compile();
                Action<object, object> setterConvert = null;

                if (prop.PropertyType != fi.ColType)
                {
                    setterConvert = ((Expression<Action<object, object>>)((o, v) => setter(o, v == null ? null : Convert.ChangeType(v, fi.Property.PropertyType)))).Compile();
                }

                properties.Add(new Prop
                {
                    ColumnOrdinal = fi.Index,
                    ColumnName = prop.Name,
                    Setter = setterConvert == null ? setter : setterConvert
                });
            }

            Prop[] propertiesArray = properties.ToArray();
            PropertiesCache[propKey] = propertiesArray;
            return propertiesArray;
        }
    }
}
