using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Net.Http.Headers;
using System.Reflection;

namespace InABox.Core
{
    [Serializable]
    public class CoreTable : ICoreTable, ISerializeBinary //: IEnumerable, INotifyCollectionChanged
    {

        #region Fields

        private List<CoreRow>? rows;
        private List<CoreColumn> columns = new List<CoreColumn>();
        private string tableName;

        #endregion

        #region Properties

        public string TableName { get => tableName; }
        
        public int TotalRecords { get; set; }
        public int Offset { get; set; } = 0;

        public List<CoreColumn> Columns { get => columns; }
        public List<CoreRow> Rows
        {
            get
            {
                rows ??= new List<CoreRow>();
                //this.rows.CollectionChanged += OnRowsCollectionChanged;
                return rows;
            }
        }

        [field: NonSerialized]
        public IList<Func<object, object?>?> Getters { get; } = new List<Func<object, object?>?>();

        [field: NonSerialized]
        public Dictionary<string, IList<Action<object, object>?>> Setters { get; } = new Dictionary<string, IList<Action<object, object>?>>();

        #endregion

        public CoreTable() : this("")
        {
        }

        public CoreTable(string tableName): base()
        {
            this.tableName = tableName;
        }

        public CoreTable(Type type) : this()
        {
            LoadColumns(type);
        }
        
        /// <summary>
        /// Add a new column of type <typeparamref name="TData"/> and column name <paramref name="column"/>.
        /// </summary>
        public void AddColumn<TData>(string column)
        {
            Columns.Add(new CoreColumn(typeof(TData), column));
        }
        public void AddColumn(Type dataType, string column)
        {
            Columns.Add(new CoreColumn(dataType, column));
        }
        public void AddColumn<T>(Expression<Func<T, object>> column)
        {
            Columns.Add(
                new CoreColumn()
                {
                    ColumnName = CoreUtils.GetFullPropertyName(column, "."),
                    DataType = column.ReturnType
                }
            );
        }
        public bool HasColumn(string column) => Columns.Any(x => x.ColumnName == column);
        
        public CoreRow NewRow(bool populate = false)
        {
            var result = new CoreRow(this);
            if (populate)
                foreach (var column in Columns)
                    result[column.ColumnName] = column.DataType.GetDefault();
            return result;
        }

        public void LoadColumns(Type T)
        {
            var iprops = DatabaseSchema.Properties(T);
            foreach (var iprop in iprops)
                Columns.Add(new CoreColumn { ColumnName = iprop.Name, DataType = iprop.PropertyType });
        }
        
        public void LoadColumns(IColumns columns)
        {
            foreach (var col in columns)
                Columns.Add(new CoreColumn { ColumnName = col.Property, DataType = col.Type });
        }
        
        public void LoadColumns(IEnumerable<CoreColumn> columns)
        {
            Columns.Clear();
            foreach (var col in columns)
                Columns.Add(new CoreColumn() { ColumnName = col.ColumnName, DataType = col.DataType }); 
        }

        /// <summary>
        /// Add the rows from the given <paramref name="table"/> to this table.
        /// </summary>
        /// <remarks>
        /// This method assumes that the columns of this table and <paramref name="table"/> are the same; it is intended for paging functionality.
        /// </remarks>
        /// <param name="table"></param>
        public void AddPage(CoreTable table)
        {
            foreach(var row in table.Rows)
            {
                var newRow = NewRow(populate: false);
                newRow.LoadValues(row.Values);
                Rows.Add(newRow);
            }
        }

        public void LoadRows(IEnumerable<object> objects)
        {
            foreach (var obj in objects)
            {
                LoadRow(obj);
            }
        }

        public void LoadRows(IEnumerable<CoreRow> rows)
        {
            foreach (var row in rows)
            {
                var newrow = NewRow();
                FillRow(newrow, row);
                Rows.Add(newrow);
            }
        }

        public void LoadFrom<T1, T2>(CoreTable table, CoreFieldMap<T1, T2> mappings, Action<CoreRow>? customization = null)
        {
            foreach (var row in table.Rows)
            {
                var newrow = NewRow();
                foreach (var map in mappings.Fields)
                    newrow.Set(map.To, row.Get(map.From));
                customization?.Invoke(newrow);
                Rows.Add(newrow);
            }
        }

        public void FillRow(CoreRow row, object obj)
        {
            var bFirst = Getters.Count == 0;

            for(var i = 0; i < Columns.Count; ++i)
            {
                try
                {
                    if (bFirst)
                    {
                        var prop = DatabaseSchema.Property(obj.GetType(), Columns[i].ColumnName);
                        Getters.Add(prop?.Getter());
                    }

                    var getter = Getters[i];
                    if (getter != null)
                        row.Set(i, getter(obj));
                    else
                        row.Set(i, CoreUtils.GetPropertyValue(obj, Columns[i].ColumnName));
                }
                catch (Exception e)
                {
                    Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
                }
            }
        }

        public void FillRow(CoreRow row, CoreRow from)
        {
            foreach (var col in Columns)
                try
                {
                    if (from.Table.Columns.Any(x => x.ColumnName.Equals(col.ColumnName)))
                        row[col.ColumnName] = from[col.ColumnName];
                }
                catch (Exception e)
                {
                    Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
                }
        }

        public void Filter(Func<CoreRow, bool> predicate)
        {
            for (var i = Rows.Count - 1; i > -1; i--)
            {
                var row = Rows[i];
                var predresult = predicate.Invoke(row);
                if (!predresult)
                    Rows.Remove(row);
            }
        }
        
        #region ToDictionary

        public IDictionary ToDictionary(string keycol, string displaycol, string sortcol = "")
        {
            var kc = Columns.FirstOrDefault(x => x.ColumnName.Equals(keycol));
            var dc = Columns.FirstOrDefault(x => x.ColumnName.Equals(displaycol));
            var dt = typeof(Dictionary<,>).MakeGenericType(kc.DataType, dc.DataType);
            var id = (Activator.CreateInstance(dt) as IDictionary)!;
            var sorted = string.IsNullOrWhiteSpace(sortcol) ? Rows : Rows.OrderBy(x => x.Get<object>(sortcol)).ToList();
            foreach (var row in sorted)
                id[row[keycol]] = row[displaycol];
            return id;
        }

        public Dictionary<TKey, TValue> ToDictionary<T, TKey, TValue>(Expression<Func<T, TKey>> key, Expression<Func<T, TValue>> value,
            Expression<Func<T, object>>? sort = null)
        {
            var result = new Dictionary<TKey, TValue>();
            var sorted = sort == null ? Rows : Rows.OrderBy(x => x.Get(sort)).ToList();
            foreach (var row in sorted)
                // This threw an exception once.
                result[row.Get(key)] = row.Get(value);
            return result;
        }

        public Dictionary<TKey, string> ToDictionary<T, TKey>(Expression<Func<T, TKey>> key, Expression<Func<T, object>>[] values,
            Expression<Func<T, object>>? sort = null)
        {
            var result = new Dictionary<TKey, string>();
            var sorted = sort == null ? Rows : Rows.OrderBy(x => x.Get(sort)).ToList();
            foreach (var row in sorted)
            {
                var display = new List<object>();
                foreach (var value in values)
                    display.Add(row.Get(value));
                result[row.Get(key)] = string.Join(" : ", display);
            }

            return result;
        }

        public Dictionary<TKey, TValue> ToDictionary<T, TKey, TValue>(Expression<Func<T, TKey>> key, Func<CoreRow, TValue> value,
            Expression<Func<T, object>>? sort = null)
        {
            var result = new Dictionary<TKey, TValue>();
            var sorted = sort == null ? Rows : Rows.OrderBy(x => x.Get(sort)).ToList();
            foreach (var row in Rows)
                result[row.Get(key)] = value(row);
            return result;
        }

        public void LoadDictionary<T, TKey, TValue>(Dictionary<TKey, TValue> dictionary, Expression<Func<T, TKey>> key,
            Expression<Func<T, TValue>> value)
        {
            foreach (var row in Rows) 
                dictionary[row.Get(key)] = row.Get(value);
        }
        
        public Dictionary<TKey, string> IntoDictionary<T, TKey>(Dictionary<TKey, string> result, Expression<Func<T, TKey>> key,
            params Expression<Func<T, object>>[] values)
        {
            foreach (var row in Rows)
            {
                var display = new List<object>();
                foreach (var value in values)
                    display.Add(row.Get(value));
                result[row.Get(key)] = string.Join(" : ", display);
            }

            return result;
        }

        public Dictionary<TKey, TValue> IntoDictionary<T, TKey, TValue>(Dictionary<TKey, TValue> result, Expression<Func<T, TKey>> key,
            Func<CoreRow, TValue> value)
        {
            foreach (var row in Rows)
                result[row.Get(key)] = value(row);
            return result;
        }
        
        #endregion

        public DataTable ToDataTable(string name = "", IColumns? additionalColumns = null)
        {
            var result = new DataTable(name);
            foreach (var column in Columns)
                result.Columns.Add(column.ColumnName.Replace('.', '_'), column.DataType);
            if(additionalColumns != null)
            {
                foreach (var column in additionalColumns)
                    result.Columns.Add(column.Property.Replace('.', '_'), column.Type);
            }

            //result.Columns["ID"].Unique = true;
            //result.PrimaryKey = new DataColumn[] { result.Columns["ID"] };

            foreach (var row in Rows)
            {
                //result.Rows.Add(row.Values.ToArray());
                var newrow = result.NewRow();
                newrow.ItemArray = row.Values.ToArray();
                result.Rows.Add(newrow);
            }

            return result;
        }
        
        #region ToObjects

        public IEnumerable<BaseObject> ToObjects(Type T)
            => Rows.Select(x => x.ToObject(T));
        public IEnumerable<T> ToObjects<T>() where T : BaseObject, new()
            => Rows.Select(x => x.ToObject<T>());
        public List<T> ToList<T>() where T : BaseObject, new()
        {
            var list = new List<T>(Rows.Count);
            for(int i = 0; i < Rows.Count; ++i)
            {
                list.Add(Rows[i].ToObject<T>());
            }
            return list;
        }

        public BaseObject[] ToArray(Type T)
        {
            var arr = new BaseObject[Rows.Count];
            for(int i = 0; i < Rows.Count; ++i)
            {
                arr[i] = Rows[i].ToObject(T);
            }
            return arr;
        }
        public T[] ToArray<T>() where T : BaseObject, new()
        {
            var arr = new T[Rows.Count];
            for(int i = 0; i < Rows.Count; ++i)
            {
                arr[i] = Rows[i].ToObject<T>();
            }
            return arr;
        }

        #endregion

        #region ToTuples


        public IEnumerable<Tuple<T1, T2>> ToTuples<TType, T1, T2>(
            Expression<Func<TType, T1>> item1, 
            Expression<Func<TType, T2>> item2
            )
        {
            return Rows.Select(row =>
                new Tuple<T1, T2>
                (
                    row.Get<TType,T1>(item1),
                    row.Get<TType,T2>(item2)
                )
            );
        }
        
        public IEnumerable<Tuple<T1, T2, T3>> ToTuples<TType, T1, T2, T3>(
            Expression<Func<TType, T1>> item1, 
            Expression<Func<TType, T2>> item2, 
            Expression<Func<TType, T3>> item3)
        {
            return Rows.Select(row =>
                new Tuple<T1, T2, T3>
                (
                    row.Get<TType,T1>(item1),
                    row.Get<TType,T2>(item2),
                    row.Get<TType,T3>(item3)
                )
            );
        }
        
        public IEnumerable<Tuple<T1, T2, T3, T4>> ToTuples<TType, T1, T2, T3, T4>(
            Expression<Func<TType, T1>> item1, 
            Expression<Func<TType, T2>> item2, 
            Expression<Func<TType, T3>> item3,
            Expression<Func<TType, T4>> item4
            )
        {
            return Rows.Select(row =>
                new Tuple<T1, T2, T3, T4>
                (
                    row.Get<TType,T1>(item1),
                    row.Get<TType,T2>(item2),
                    row.Get<TType,T3>(item3),
                    row.Get<TType,T4>(item4)
                )
            );
        }
        
        public IEnumerable<Tuple<T1, T2, T3, T4, T5>> ToTuples<TType, T1, T2, T3, T4, T5>(
            Expression<Func<TType, T1>> item1, 
            Expression<Func<TType, T2>> item2, 
            Expression<Func<TType, T3>> item3,
            Expression<Func<TType, T4>> item4,
            Expression<Func<TType, T5>> item5

        )
        {
            return Rows.Select(row =>
                new Tuple<T1, T2, T3, T4,T5>
                (
                    row.Get<TType,T1>(item1),
                    row.Get<TType,T2>(item2),
                    row.Get<TType,T3>(item3),
                    row.Get<TType,T4>(item4),
                    row.Get<TType,T5>(item5)
                )
            );
        }
        
        #endregion

        public void CopyTo(DataTable table)
        {
            var columns = new List<string>();
            foreach (var column in Columns)
                columns.Add(column.ColumnName.Replace('.', '_'));

            foreach (var row in Rows)
            {
                var newrow = table.NewRow();
                for (var i = 0; i < columns.Count; i++)
                    newrow[columns[i]] = row.Values[i];
                table.Rows.Add(newrow);
            }
        }

        public void CopyTo(CoreTable table)
        {
            var columns = new List<string>();
            //foreach (var column in Columns)
            //    columns.Add(column.ColumnName.Replace('.', '_'));

            foreach (var row in Rows)
            {
                var newrow = table.NewRow();
                foreach (var column in Columns)
                    if (table.Columns.Any(x => x.ColumnName.Equals(column.ColumnName)))
                        newrow[column.ColumnName] = row[column.ColumnName];
                //for (int i = 0; i < columns.Count; i++)
                //    newrow.Set(columns[i], row.Values[i]);
                table.Rows.Add(newrow);
            }
        }

        #region Extract Values
        
        public TValue[] ExtractValues<TSource, TValue>(Expression<Func<TSource, TValue>> column, bool distinct = true)
        {
            var columnIdx = GetColumnIndex(column);
            if (distinct)
            {
                return Rows.Select(r => r.Get<TValue>(columnIdx)).Distinct().ToArray();
            }
            else
            {
                return Rows.ToArray(r => r.Get<TValue>(columnIdx));
            }
        }

        public TValue[] ExtractValues<TValue>(string column, bool distinct = true)
        {
            var columnIdx = GetColumnIndex(column);
            if (distinct)
            {
                return Rows.Select(r => r.Get<TValue>(columnIdx)).Distinct().ToArray();
            }
            else
            {
                return Rows.ToArray(r => r.Get<TValue>(columnIdx));
            }
        }

        #endregion

        /// <summary>
        /// Create a new row with data from <paramref name="obj"/>, and adds it to the table.
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public CoreTable LoadRow(object obj)
        {
            var row = NewRow();
            FillRow(row, obj);
            Rows.Add(row);
            return this;
        }

        public int GetColumnIndex<T>(Expression<Func<T, object>> column)
            => GetColumnIndex(CoreUtils.GetFullPropertyName(column, "."));

        public int GetColumnIndex<T, TValue>(Expression<Func<T, TValue>> column)
            => GetColumnIndex(CoreUtils.GetFullPropertyName(column, "."));

        public int GetColumnIndex(string columnname)
        {
            for (var i = 0; i < Columns.Count; i++)
                if (Columns[i].ColumnName.Equals(columnname, StringComparison.CurrentCultureIgnoreCase))
                {
                    return i;
                }

            return -1;
        }

        #region ToLookup

        public IEnumerable<IGrouping<TKey, TValue>> ToGroups<T, TKey, TValue>(Expression<Func<T, TKey>> key,
            Expression<Func<T, TValue>> value)
        {
            return Rows.GroupBy(r => r.Get(key), r => r.Get(value));
        }
        
        public IMutableLookup<TKey, TValue> ToLookup<T, TKey, TValue>(Expression<Func<T, TKey>> key, Expression<Func<T, TValue>> value,
            Expression<Func<T, object>>? sort = null)
        {
            IMutableLookup<TKey, TValue> result = new MutableLookup<TKey, TValue>(
                Rows.ToLookup(
                    r => r.Get(key),
                    r => r.Get(value)
                )
            );
            return result;
        }

        public IMutableLookup<TKey, TValue> ToLookup<T, TKey, TValue>(Expression<Func<T, TKey>> key, Func<CoreRow, TValue> value,
            Expression<Func<T, object>>? sort = null)
        {
            IMutableLookup<TKey, TValue> result = new MutableLookup<TKey, TValue>(
                Rows.ToLookup(
                    r => r.Get(key),
                    r => value(r)
                )
            );
            return result;
        }
        
        #endregion

        #region Serialize Binary

        public void WriteBinary(CoreBinaryWriter writer, bool includeColumns)
        {
            writer.Write(TableName);

            if (includeColumns)
            {
                foreach (var column in Columns)
                {
                    writer.Write(true);
                    writer.Write(column.ColumnName);
                    writer.Write(column.DataType.EntityName());
                }
                writer.Write(false);
            }

            writer.Write(Rows.Count);
            foreach (var row in Rows)
            {
                foreach (var col in Columns)
                {
                    var val = row[col.ColumnName];
                    writer.WriteBinaryValue(col.DataType, val);
                }
            }
        }

        public void SerializeBinary(CoreBinaryWriter writer) => WriteBinary(writer, true);

        public void ReadBinary(CoreBinaryReader reader, IList<CoreColumn>? columns)
        {
            tableName = reader.ReadString();

            Columns.Clear();
            if (columns is null)
            {
                while (reader.ReadBoolean())
                {
                    var columnName = reader.ReadString();
                    var dataType = CoreUtils.GetEntity(reader.ReadString());
                    Columns.Add(new CoreColumn(dataType, columnName));
                }
            }
            else
            {
                foreach (var column in columns)
                {
                    Columns.Add(column);
                }
            }

            Rows.Clear();
            var nRows = reader.ReadInt32();
            for (int i = 0; i < nRows; ++i)
            {
                var row = NewRow();
                foreach (var column in Columns)
                {
                    var value = reader.ReadBinaryValue(column.DataType);
                    row.Values.Add(value);
                }
                Rows.Add(row);
            }
        }

        public void DeserializeBinary(CoreBinaryReader reader) => ReadBinary(reader, null);

        #endregion
    }
}