using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Collections.ObjectModel;
using FastMember;
using System.Linq;

namespace Knoks.Framework.DataAccess
{
    public class DataResult
    {
        public Dictionary<short, Table> Тables { get; set; } = new Dictionary<short, Table>();
        public Dictionary<string, object> OutputParams { get; set; } = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
        public int? ReturnValue { get; set; }
    }

    public class Row
    {
        public ReadOnlyDictionary<string, int> ColumnNames { get; private set; }
        public ReadOnlyCollection<object> ColumnValues { get; private set; }

        public Row(ReadOnlyDictionary<string, int> columnNames, ReadOnlyCollection<object> columnValues)
        {
            ColumnNames = columnNames;
            ColumnValues = columnValues;
        }

        public object this[int columnIndex]
        {
            get { return ColumnValues[columnIndex]; }
        }

        public object this[string columnName]
        {
            get { return ColumnValues[ColumnNames[columnName]]; }
        }

        public T To<T>() where T : class, new()
        {
            return To<T>(ColumnNames, ColumnValues);
        }
        public T ToMapped<T>(Func<string, string> mapper = null) where T : class, new()
        {
            var accessor = TypeAccessor.Create(typeof(T));
            var meta = Table.Parse<T>(accessor, ColumnNames, mapper);
            return Process<T>(accessor, meta);
        }
        internal T Process<T>(TypeAccessor accessor, Dictionary<Member, int> meta) where T : class, new()
        {
            var res = new T();
            foreach (var c in meta)
            {
                try
                {
                        accessor[res, c.Key.Name] = ColumnValues[c.Value];
                }
                catch (Exception ex)
                {
                    try
                    {
                        accessor[res, c.Key.Name] = Convert.ChangeType(c.Value, c.Key.Type);
                    }
                    catch (Exception exx)
                    {
                        throw new ArgumentException(c.Key.Name + ": " + ex.Message, exx);
                    }
                }
            }
            return res;
        }
        public static T To<T>(IDictionary<string, int> ColumnNames, IList<object> ColumnValues) where T : class, new()
        {
            var res = new T();

            var accessor = TypeAccessor.Create(res.GetType());

            foreach (var cn in accessor.GetMembers())
            {
                int columnIndex;
                try
                {
                    if (ColumnNames.TryGetValue(cn.Name, out columnIndex))
                        accessor[res, cn.Name] = ColumnValues[columnIndex];
                }
                catch (Exception ex)
                {
                    throw new ArgumentException(cn.Name + ": " + ex.Message, ex);
                }
                
            }

            return res;
        }
    }

    public class Table
    {
        private Dictionary<string, int> _columnNames = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
        public ReadOnlyDictionary<string, int> ColumnNames { get; }

        private List<Row> _rows = new List<Row>();
        public ReadOnlyCollection<Row> Rows { get; }

        public Table(SqlDataReader reader)
        {
            ColumnNames = new ReadOnlyDictionary<string, int>(_columnNames);
            Rows = new ReadOnlyCollection<Row>(_rows);

            int fieldCount = reader.FieldCount;

            for (int i = 0; i < fieldCount; i++)
                _columnNames.Add(reader.GetName(i), i);

            while (reader.Read())
            {
                var rowValues = new object[fieldCount];

                for (var i = 0; i < rowValues.Length; i++)
                {
                    var val = reader[i];
                    rowValues[i] = val == DBNull.Value ? null : val;
                }

                _rows.Add(new Row(ColumnNames, new ReadOnlyCollection<object>(rowValues)));
            }
        }

        public Row this[int rowIndex]
        {
            get { return _rows[rowIndex]; }
        }

        public IEnumerable<T> ToList<T>() where T : class, new()
        {
            return _rows.Select(r => r.To<T>());
        }
        public IEnumerable<T> ToListMapped<T>(Func<string,string> mapper = null) where T : class, new()
        {
            var accessor = TypeAccessor.Create(typeof(T));
            var meta = Parse<T>(accessor, ColumnNames, mapper);
            return _rows.Select(r => r.Process<T>(accessor, meta));
        }
        internal static Dictionary<Member, int> Parse<T>(TypeAccessor accessor, IDictionary<string, int> ColumnNames, Func<string, string> mapper = null) where T : class, new()
        {
            return accessor.GetMembers().Select(cn => {
                int columnIndex;
                string name = mapper == null ? cn.Name : mapper(cn.Name);
                if (!ColumnNames.TryGetValue(name, out columnIndex))
                {
                    columnIndex = -1;
                }
                return new { cn, columnIndex };
            }).Where(it => it.columnIndex != -1).ToDictionary(it => it.cn, it => it.columnIndex);
        }

        public IEnumerable<T> ToPrimitiveList<T>()
        {
            return _rows.Select(r => (T)r.ColumnValues.First());
        }
    }
}
