using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace InABox.Core
{
    public interface IBaseColumn
    {
        /// <summary>
        /// Unique name of this column.
        /// </summary>
        /// <remarks>
        /// Every column needs a name to distinguish it from other columns, for example in query result tables, or in SQL queries.
        /// </remarks>
        string Name { get; }

        Type Type { get; }
    }

    public interface IBaseColumns
    {
        IEnumerable<IBaseColumn> Columns();
    }

    public interface IComplexColumn : IBaseColumn
    {
        IComplexFormulaNode Formula { get; }
    }

    public class ComplexColumn<T, TResult> : IComplexColumn
    {
        public string Name { get; }

        public Type Type => typeof(TResult);

        public IComplexFormulaNode<T, TResult> Formula { get; }

        public ComplexColumn(string name, IComplexFormulaNode<T, TResult> formula)
        {
            Name = name;
            Formula = formula;
        }

        #region IComplexColumn

        IComplexFormulaNode IComplexColumn.Formula => Formula;

        #endregion
    }

    public interface IColumn : IBaseColumn
    {
        string Property { get; }

        IProperty PropertyDefinition { get; set; }

        string IBaseColumn.Name => Property;
    }

    public static class Column
    {
        public static IColumn Create(Type concrete, string property)
        {
            var type = typeof(Column<>).MakeGenericType(concrete);
            var result = Activator.CreateInstance(type, property) as IColumn;
            return result!;
        }
    }

    public class Column<T> : IColumn
    {
        public Type Type
        {
            get
            {
                return PropertyDefinition.PropertyType;
            }
        }

        private string _property;
        public string Property
        {
            get => _property;
            set
            {
                _property = value;
                _propertyDefinition = null;
            }
        }

        private IProperty? _propertyDefinition;
        public IProperty PropertyDefinition
        {
            get
            {
                _propertyDefinition ??= DatabaseSchema.PropertyStrict(typeof(T), Property);
                return _propertyDefinition;
            }
            set
            {
                _property = value.Name;
                _propertyDefinition = value;
            }
        }

        public bool IsEqualTo(string name) => 
            !name.IsNullOrWhiteSpace() && Property.Equals(name);
        public bool IsEqualTo(Column<T> column) =>
            string.Equals(Property, column.Property);

        public bool IsParentOf(string name) =>
            !name.IsNullOrWhiteSpace() && name.StartsWith(Property + ".");

        public Column(IProperty property)
        {
            _property = property.Name;
            _propertyDefinition = property;
        }

        public Column(Expression<Func<T, object?>> expression)
        {
            _property = CoreUtils.GetFullPropertyName(expression, ".");
        }

        public Column(string property)
        {
            _property = property;
        }

        public Column<TNew> Cast<TNew>()
            where TNew: T
        {
            return new Column<TNew>(Property);
        }

        public Column<T> SubColumn<TSub>(Column<TSub> subColumn)
        {
            return new Column<T>(Property + "." + subColumn.Property);
        }
        public static Column<T> SubColumn<TSub>(Expression<Func<T, TSub>> super, Column<TSub> sub)
        {
            var prefix = CoreUtils.GetFullPropertyName(super, ".") + ".";
            return new Column<T>(prefix + sub.Property);
        }

        public bool TryCast<TNew>([NotNullWhen(true)] out Column<TNew>? newColumn)
        {
            if(DatabaseSchema.Property(typeof(TNew), Property) is IProperty property)
            {
                newColumn = new Column<TNew>(property);
                return true;
            }
            else
            {
                newColumn = null;
                return false;
            }
        }

        public override string ToString() => Property;
    }

    public interface IColumns : ISerializeBinary, IBaseColumns
    {
        int Count { get; }

        IEnumerable<string> ColumnNames();

        bool Contains(string column);

        IColumns Add(string column);
        IColumns Add(IColumn column);
        IColumns Add<T>(Expression<Func<T, object?>> column);
        IColumns Add(IProperty property);
        IColumns Add(IEnumerable<string> columns);
        IColumns Add(params string[] columns);

        IEnumerator<IColumn> GetEnumerator();
    }

    [Flags]
    public enum ColumnTypeFlags
    {
        /// <summary>
        /// No columns at all.
        /// </summary>
        None = 0,
        /// <summary>
        /// All columns which are marked as <see cref="RequiredColumnAttribute"/>.
        /// </summary>
        Required = 1,
        /// <summary>
        /// All columns.
        /// </summary>
        All = 2,
        /// <summary>
        /// Only columns on the entity, and not on entity links or calculated fields.
        /// </summary>
        /// <remarks>
        /// This option <b>does</b> include foreign keys.
        /// </remarks>
        Local = 4,

        IncludeID = 8,

        IncludeForeignKeys = 16,
        /// <summary>
        /// Include all columns that are accessible through entity links present in the root class.
        /// </summary>
        IncludeLinked = 32,
        /// <summary>
        /// Include all columns that are accessible through entity links, even nested ones.
        /// </summary>
        IncludeNestedLinks = 64,

        IncludeAggregates = 128,
        IncludeFormulae = 256,
        /// <summary>
        /// Include any columns that are a <see cref="CustomProperty"/>.
        /// </summary>
        IncludeUserProperties = 512,
        IncludeEditable = 1024,

        /// <summary>
        /// Include all columns marked as <see cref="Visible.Optional"/>, <see cref="Visible.Default"/> (or that don't have a visibility).
        /// </summary>
        IncludeOptional = 2048,

        /// <summary>
        /// Include all columns marked as <see cref="Visible.Default"/> (or that don't have a visibility).
        /// </summary>
        IncludeVisible = 4096,

        DefaultVisible = IncludeVisible | IncludeID,
        EditorColumns = IncludeID | Required | IncludeOptional | IncludeForeignKeys | IncludeUserProperties | IncludeEditable,
    }

    public static class Columns
    {
        public static IColumns Create<T>(Type concrete, ColumnTypeFlags flags)
        {
            if (!typeof(T).IsAssignableFrom(concrete))
                throw new Exception($"Columns: {concrete.EntityName()} does not implement {typeof(T).EntityName()}");
            var type = typeof(Columns<>).MakeGenericType(concrete);
            var result = Activator.CreateInstance(type, flags);
            return (result as IColumns)!;
        }

        public static IColumns Create(Type concrete, ColumnTypeFlags flags)
        {
            var type = typeof(Columns<>).MakeGenericType(concrete);
            var result = Activator.CreateInstance(type, flags) as IColumns;
            return result!;
        }

        /// <summary>
        /// Create a new <see cref="IColumns"/> that is completely empty.
        /// </summary>
        public static IColumns None(Type T) => Create(T, ColumnTypeFlags.None);
        /// <summary>
        /// Create a new <see cref="IColumns"/> with all columns of <paramref name="T"/> that are marked as <see cref="RequiredColumnAttribute"/>.
        /// </summary>
        public static IColumns Required(Type T) => Create(T, ColumnTypeFlags.Required);
        /// <summary>
        /// Create a new <see cref="IColumns"/> with all columns local to the entity, and not on entity links or calculated fields.
        /// </summary>
        public static IColumns Local(Type T) => Create(T, ColumnTypeFlags.Local);
        /// <summary>
        /// Create a new <see cref="IColumns"/> with all columns that the entity has.
        /// </summary>
        public static IColumns All(Type T) => Create(T, ColumnTypeFlags.All);

        /// <summary>
        /// Create a new <see cref="IColumns"/> that is completely empty.
        /// </summary>
        public static Columns<T> None<T>()
        {
            return new Columns<T>(ColumnTypeFlags.None);
        }

        /// <summary>
        /// Create a new <see cref="IColumns"/> with all columns of <paramref name="T"/> that are marked as <see cref="RequiredColumnAttribute"/>.
        /// </summary>
        public static Columns<T> Required<T>()
        {
            return new Columns<T>(ColumnTypeFlags.Required);
        }

        /// <summary>
        /// Create a new <see cref="IColumns"/> with all columns local to the entity, and not on entity links or calculated fields.
        /// </summary>
        public static Columns<T> Local<T>()
        {
            return new Columns<T>(ColumnTypeFlags.Local);
        }

        /// <summary>
        /// Create a new <see cref="IColumns"/> with all columns that the entity has.
        /// </summary>
        public static Columns<T> All<T>()
        {
            return new Columns<T>(ColumnTypeFlags.All);
        }
    }

    public class Columns<T> : IColumns, ICollection<Column<T>>
    {
        #region Private Fields

        private readonly List<Column<T>> columns;

        #endregion

        #region Public Accessors

        public Column<T> this[int index] => columns[index];

        public int Count => columns.Count;

        bool ICollection<Column<T>>.IsReadOnly => false;

        #endregion

        private Columns()
        {
            columns = new List<Column<T>>();
        }

        public Columns(ColumnTypeFlags flags)
        {
            columns = new List<Column<T>>();
            AddColumns(flags);
        }
        public Columns<T> AddColumns(ColumnTypeFlags flags)
        {
            if (flags == ColumnTypeFlags.None)
                return this;

            var props = DatabaseSchema.Properties(typeof(T))
                .Where(x => x.Setter() != null)
                .OrderBy(x => x.PropertySequence()).ToList();

            if (flags.HasFlag(ColumnTypeFlags.All))
            {
                foreach (var prop in props)
                    columns.Add(new Column<T>(prop.Name));
                return this;
            }

            if (typeof(T).IsSubclassOf(typeof(Entity)) && flags.HasFlag(ColumnTypeFlags.IncludeID))
                columns.Add(new Column<T>(nameof(Entity.ID)));

            var visibleProps = flags.HasFlag(ColumnTypeFlags.IncludeVisible)
                ? DefaultColumns.GetDefaultColumns(typeof(T)).Select(x => x.Property).WithIndex().ToDictionary(x => x.Value, x => x.Key)
                : null;
            var hasVisibleProps = visibleProps != null && visibleProps.Count > 0;

            foreach(var prop in props)
            {
                if (flags.HasFlag(ColumnTypeFlags.Required) && prop.Required)
                {
                    columns.Add(new Column<T>(prop));
                }
                else
                {
                    var isLocal = !prop.HasParentEntityLink()
                        || (prop.Parent?.HasParentEntityLink() != true && prop.Name.EndsWith(".ID"));
                    if (flags.HasFlag(ColumnTypeFlags.Local) && isLocal && !prop.IsCalculated)
                    {
                        columns.Add(new Column<T>(prop));
                    }
                    else if(prop is CustomProperty)
                    {
                        if (flags.HasFlag(ColumnTypeFlags.IncludeUserProperties))
                        {
                            columns.Add(new Column<T>(prop));
                        }
                        else
                        {
                            // Don't add
                        }
                    }
                    else
                    {
                        var parentLink = prop.HasParentEntityLink();
                        var failed = false;
                        if(prop.HasParentEntityLink())
                        {
                            if(prop.Parent?.HasParentEntityLink() == true && !flags.HasFlag(ColumnTypeFlags.IncludeNestedLinks))
                            {
                                failed = true;
                            }
                            else if(!prop.Name.EndsWith(".ID") && !flags.HasFlag(ColumnTypeFlags.IncludeLinked))
                            {
                                failed = true;
                            }
                            else if(prop.Name.EndsWith(".ID") && !flags.HasFlag(ColumnTypeFlags.IncludeForeignKeys))
                            {
                                failed = true;
                            }
                        }
                        if (!failed)
                        {
                            var hasNullEditor = prop.GetParent(x => x.HasEditor && x.Editor is NullEditor) != null;
                            var visible = hasNullEditor || prop.Editor?.Visible == Visible.Hidden
                                ? Visible.Hidden
                                : hasVisibleProps
                                    ? (visibleProps!.ContainsKey(prop) ? Visible.Default : Visible.Optional)
                                    : (prop.Editor?.Visible ?? Visible.Optional);
                            var editable = hasNullEditor ? Editable.Hidden : (prop.Editor?.Editable ?? Editable.Enabled);

                            failed = (!flags.HasFlag(ColumnTypeFlags.IncludeVisible) || visible != Visible.Default)
                                && (!flags.HasFlag(ColumnTypeFlags.IncludeOptional) || (visible != Visible.Optional && visible != Visible.Default))
                                && (!flags.HasFlag(ColumnTypeFlags.IncludeEditable) || !editable.ColumnVisible());
                        }
                        if (!failed)
                        {
                            failed = (!flags.HasFlag(ColumnTypeFlags.IncludeAggregates) && prop.HasAttribute<AggregateAttribute>())
                                || (!flags.HasFlag(ColumnTypeFlags.IncludeFormulae) && (prop.HasAttribute<FormulaAttribute>() || prop.HasAttribute<ComplexFormulaAttribute>()));
                        }
                        if (!failed)
                        {
                            columns.Add(new Column<T>(prop));
                        }
                    }
                }
            }

            if (hasVisibleProps)
            {
                columns.SortBy(x => visibleProps?.GetValueOrDefault(x.PropertyDefinition) ?? (visibleProps!.Count + props.IndexOf(x.PropertyDefinition)));
            }

            return this;
        }

        #region IColumns

        IColumns IColumns.Add(string column) => Add(column);
        IColumns IColumns.Add<TEntity>(Expression<Func<TEntity, object?>> expression)
        {
            return Add(CoreUtils.GetFullPropertyName(expression, "."));
        }
        public IColumns Add(IColumn column)
        {
            if (column is Column<T> col)
                return Add(col);
            return this;
        }
        IColumns IColumns.Add(IProperty property) => Add(property);

        IColumns IColumns.Add(params string[] columnnames) => Add(columnnames);
        IColumns IColumns.Add(IEnumerable<string> columnnames) => Add(columnnames);

        #endregion

        #region IBaseColumns

        IEnumerable<IBaseColumn> IBaseColumns.Columns() => columns;

        #endregion

        #region Add

        private Columns<T> Add(string column)
        {
            if (!Contains(column))
            {
                if(CoreUtils.TryGetProperty(typeof(T), column, out var propertyInfo))
                {
                    columns.Add(new Column<T>(column));
                }
                else
                {
                    var property = DatabaseSchema.Property(typeof(T), column);
                    if(property is null)
                    {
                        Logger.Send(LogType.Error, "", $"Property {column} does not exist on {typeof(T).Name}");
                    }
                    else
                    {
                        columns.Add(new Column<T>(property));
                    }
                }
            }
            return this;
        }

        public Columns<T> Add(Column<T> column)
        {
            if(!Contains(column.Property))
            {
                columns.Add(column);
            }
            return this;
        }

        public Columns<T> Add(IProperty property)
        {
            if (!Contains(property.Name))
            {
                columns.Add(new Column<T>(property));
            }
            return this;
        }

        public Columns<T> Add(Expression<Func<T, object?>> expression)
            => Add(CoreUtils.GetFullPropertyName(expression, "."));

        public Columns<T> Add<TType>(Expression<Func<T, TType>> expression)
            => Add(CoreUtils.GetFullPropertyName(expression, "."));

        #region Range Adds

        public Columns<T> AddSubColumns<TSub>(Expression<Func<T, TSub>> super, Columns<TSub>? sub)
        {
            sub ??= CoreUtils.GetColumns(sub);
            var prefix = CoreUtils.GetFullPropertyName(super, ".") + ".";
            foreach(var column in sub.ColumnNames())
            {
                columns.Add(new Column<T>(prefix + column));
            }
            return this;
        }

        public Columns<T> Add(IEnumerable<Column<T>> columns)
        {
            foreach(var col in columns)
            {
                Add(col);
            }
            return this;
        }

        public Columns<T> Add(params string[] columnnames)
        {
            foreach (var name in columnnames)
                Add(name);
            return this;
        }
        public Columns<T> Add(IEnumerable<string> columnnames)
        {
            foreach (var name in columnnames)
                Add(name);
            return this;
        }

        public Columns<T> Add(params Expression<Func<T, object?>>[] expressions)
        {
            foreach (var expression in expressions)
                columns.Add(new Column<T>(expression));
            return this;
        }

        /// <summary>
        /// Add a range of columns, <b>without</b> checking for duplicates.
        /// </summary>
        /// <param name="columns"></param>
        /// <returns></returns>
        public Columns<T> AddRange(IEnumerable<Column<T>> columns)
        {
            this.columns.AddRange(columns);
            return this;
        }

        #endregion

        #endregion

        #region Remove

        public Columns<T> Remove(string column)
        {
            columns.RemoveAll(x => x.Property == column);
            return this;
        }

        #endregion

        #region Casting Columns Type

        public Columns<TNew> Cast<TNew>()
            where TNew : T
        {
            var cols = Columns.None<TNew>();
            foreach(var column in columns)
            {
                cols.Add(column.Cast<TNew>());
            }
            return cols;
        }

        /// <summary>
        /// Cast the columns to <typeparamref name="TNew"/>, keeping the columns that are found in both <typeparamref name="T"/> and <typeparamref name="TNew"/>.
        /// </summary>
        /// <typeparam name="TNew"></typeparam>
        /// <returns></returns>
        public Columns<TNew> CastIntersection<TNew>()
        {
            var cols = Columns.None<TNew>();
            foreach(var column in columns)
            {
                if (column.TryCast<TNew>(out var newColumn))
                {
                    cols.Add(newColumn);
                }
            }
            return cols;
        }

        #endregion

        public override string ToString()
        {
            return string.Join("; ", columns.Select(x => x.Property));
        }

        public int IndexOf(string column)
        {
            for(int i = 0; i < columns.Count; ++i)
            {
                if (columns[i].Property == column)
                {
                    return i;
                }
            }
            return -1;
        }
        
        public int IndexOf(Expression<Func<T, object>> expression)
        {
            var columnName = CoreUtils.GetFullPropertyName(expression, ".");
            for(int i = 0; i < columns.Count; ++i)
            {
                if (columns[i].Property == columnName)
                {
                    return i;
                }
            }
            return -1;
        }

        public bool Contains(string column) => columns.Any(x => x.IsEqualTo(column));

        public IEnumerable<string> ColumnNames()
        {
            return columns.Select(c => c.Property);
        }

        #region Binary Serialization

        public void SerializeBinary(CoreBinaryWriter writer)
        {
            writer.Write(columns.Count);
            foreach(var column in columns)
            {
                writer.Write(column.Property);
            }
        }
        public void DeserializeBinary(CoreBinaryReader reader)
        {
            columns.Clear();
            var nColumns = reader.ReadInt32();
            for(int i = 0; i < nColumns; ++i)
            {
                var property = reader.ReadString();
                columns.Add(new Column<T>(property));
            }
        }

        #endregion

        #region ICollection

        public IEnumerator<Column<T>> GetEnumerator()
        {
            return columns.GetEnumerator();
        }

        IEnumerator<IColumn> IColumns.GetEnumerator() => GetEnumerator();

        IEnumerator IEnumerable.GetEnumerator()
        {
            return columns.GetEnumerator();
        }

        void ICollection<Column<T>>.Add(Column<T> item)
        {
            Add(item);
        }

        void ICollection<Column<T>>.Clear()
        {
            columns.Clear();
        }

        bool ICollection<Column<T>>.Contains(Column<T> item)
        {
            return Contains(item.Property);
        }

        void ICollection<Column<T>>.CopyTo(Column<T>[] array, int arrayIndex)
        {
            columns.CopyTo(array, arrayIndex);
        }

        bool ICollection<Column<T>>.Remove(Column<T> item)
        {
            return columns.RemoveAll(x => x.Property == item.Property) > 0;
        }

        #endregion
    }

    public static class ColumnsExtensions
    {
        public static Columns<T> ToColumns<T>(this IEnumerable<Column<T>> columns, ColumnTypeFlags flags)
        {
            return new Columns<T>(flags).AddRange(columns);
        }
    }

    public static class ColumnSerialization
    {
        /// <summary>
        /// Inverse of <see cref="Write{T}(CoreBinaryWriter, Columns{T}?)"/>.
        /// </summary>
        /// <param name="reader"></param>
        /// <returns></returns>
        public static Columns<T>? ReadColumns<T>(this CoreBinaryReader reader)
        {
            if (reader.ReadBoolean())
            {
                var columns = Columns.None<T>();
                columns.DeserializeBinary(reader);
                return columns;
            }
            return null;
        }

        /// <summary>
        /// Inverse of <see cref="ReadColumns{T}(CoreBinaryReader)"/>.
        /// </summary>
        /// <param name="filter"></param>
        /// <param name="writer"></param>
        public static void Write<T>(this CoreBinaryWriter writer, Columns<T>? columns)
        {
            if (columns is null)
            {
                writer.Write(false);
            }
            else
            {
                writer.Write(true);
                columns.SerializeBinary(writer);
            }
        }
    }


    public class ColumnJsonConverter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
        {
            if(!(value is IColumn col))
            {
                writer.WriteNull();
                return;
            }

            writer.WriteStartObject();
            
            writer.WritePropertyName("$type");
            writer.WriteValue(value.GetType().FullName);

            writer.WritePropertyName("Property");
            writer.WriteValue(col.Name);

            writer.WriteEndObject();
        }

        public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer)
        {
            if (reader.TokenType == JsonToken.Null)
                return null;

            var data = new Dictionary<string, object>();
            while (reader.TokenType != JsonToken.EndObject && reader.Read())
                if (reader.Value != null)
                {
                    var key = reader.Value.ToString();
                    reader.Read();
                    if (String.Equals(key, "$type"))
                        objectType = Type.GetType(reader.Value.ToString()) ?? objectType;
                    else 
                        data[key] = reader.Value;
                }

            var prop = data["Property"].ToString();
            var result = Activator.CreateInstance(objectType, prop);

            return result;
        }


        public override bool CanConvert(Type objectType)
        {
            if (objectType.IsConstructedGenericType)
            {
                var ot = objectType.GetGenericTypeDefinition();
                var tt = typeof(Column<>);
                if (ot == tt)
                    return true;
            }

            return false;
        }
    }
}