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
{
///
/// Unique name of this column.
///
///
/// Every column needs a name to distinguish it from other columns, for example in query result tables, or in SQL queries.
///
string Name { get; }
}
public interface IBaseColumns
{
IEnumerable Columns();
}
public interface IComplexColumn : IBaseColumn
{
IComplexFormulaNode Formula { get; }
}
public class ComplexColumn : IComplexColumn
{
public string Name { get; }
public IComplexFormulaNode Formula { get; }
public ComplexColumn(string name, IComplexFormulaNode formula)
{
Name = name;
Formula = formula;
}
#region IComplexColumn
IComplexFormulaNode IComplexColumn.Formula => Formula;
#endregion
}
public interface IColumn : IBaseColumn
{
string Property { get; }
Type Type { get; }
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 : IColumn
{
private IProperty _property;
public Type Type
{
get
{
if (Expression is null)
throw new Exception($"Expression [{Property}] may not be null");
if (Expression is IndexExpression)
return DatabaseSchema.Property(typeof(T), Property).PropertyType;
return Expression.Type;
}
}
public string Property { get; private set; }
public Expression Expression { get; private set; }
public bool IsEqualTo(string name) =>
!name.IsNullOrWhiteSpace() && Property.Equals(name);
public bool IsEqualTo(Column column) =>
string.Equals(Property, column.Property);
public bool IsParentOf(string name) =>
!name.IsNullOrWhiteSpace() && name.StartsWith(Property + ".");
public Column(IProperty property)
{
Property = property.Name;
Expression = property.Expression();
}
public Column(Expression> expression)
{
Property = CoreUtils.GetFullPropertyName(expression, ".");
Expression = CoreUtils.ExtractMemberExpression(expression);
}
public Column(string property)
{
Property = property;
var iprop = DatabaseSchema.Property(typeof(T), property);
if (iprop != null)
Expression = iprop.Expression();
else
Expression = CoreUtils.CreateMemberExpression(typeof(T), property);
}
public Column Cast()
where TNew: T
{
return new Column(Property);
}
public bool TryCast([NotNullWhen(true)] out Column? newColumn)
{
if(DatabaseSchema.Property(typeof(TNew), Property) is IProperty property)
{
newColumn = new Column(property);
return true;
}
else
{
newColumn = null;
return false;
}
}
public override string ToString() => Property;
}
public interface IColumns : ISerializeBinary, IBaseColumns
{
int Count { get; }
IEnumerable ColumnNames();
bool Contains(string column);
IColumns Add(string column);
IColumns Add(IColumn column);
IColumns Add(Expression> column);
IColumns Add(IProperty property);
IColumns Add(IEnumerable columns);
IColumns Add(params string[] columns);
IEnumerator GetEnumerator();
}
[Flags]
public enum ColumnTypeFlags
{
///
/// No columns at all.
///
None = 0,
///
/// All columns which are marked as .
///
Required = 1,
///
/// All columns.
///
All = 2,
///
/// Only columns on the entity, and not on entity links or calculated fields.
///
///
/// This option does include foreign keys.
///
Local = 4,
IncludeID = 8,
IncludeForeignKeys = 16,
///
/// Include all columns that are accessible through entity links present in the root class.
///
IncludeLinked = 32,
///
/// Include all columns that are accessible through entity links, even nested ones.
///
IncludeNestedLinks = 64,
IncludeAggregates = 128,
IncludeFormulae = 256,
///
/// Include any columns that are a .
///
IncludeUserProperties = 512,
IncludeEditable = 1024,
///
/// Include all columns marked as , (or that don't have a visibility).
///
IncludeOptional = 2048,
///
/// Include all columns marked as (or that don't have a visibility).
///
IncludeVisible = 4096,
DefaultVisible = IncludeVisible | IncludeID,
EditorColumns = IncludeID | Required | IncludeOptional | IncludeForeignKeys | IncludeUserProperties | IncludeEditable,
}
public static class Columns
{
public static IColumns Create(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!;
}
///
/// Create a new that is completely empty.
///
public static IColumns None(Type T) => Create(T, ColumnTypeFlags.None);
///
/// Create a new with all columns of that are marked as .
///
public static IColumns Required(Type T) => Create(T, ColumnTypeFlags.Required);
///
/// Create a new with all columns local to the entity, and not on entity links or calculated fields.
///
public static IColumns Local(Type T) => Create(T, ColumnTypeFlags.Local);
///
/// Create a new with all columns that the entity has.
///
public static IColumns All(Type T) => Create(T, ColumnTypeFlags.All);
///
/// Create a new that is completely empty.
///
public static Columns None()
{
return new Columns(ColumnTypeFlags.None);
}
///
/// Create a new with all columns of that are marked as .
///
public static Columns Required()
{
return new Columns(ColumnTypeFlags.Required);
}
///
/// Create a new with all columns local to the entity, and not on entity links or calculated fields.
///
public static Columns Local()
{
return new Columns(ColumnTypeFlags.Local);
}
///
/// Create a new with all columns that the entity has.
///
public static Columns All()
{
return new Columns(ColumnTypeFlags.All);
}
}
public class Columns : IColumns, ICollection>
{
#region Private Fields
private readonly List> columns;
#endregion
#region Public Accessors
public Column this[int index] => columns[index];
public int Count => columns.Count;
bool ICollection>.IsReadOnly => false;
#endregion
private Columns()
{
columns = new List>();
}
public Columns(ColumnTypeFlags flags)
{
columns = new List>();
AddColumns(flags);
}
public Columns 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(prop.Name));
return this;
}
if (typeof(T).IsSubclassOf(typeof(Entity)) && flags.HasFlag(ColumnTypeFlags.IncludeID))
columns.Add(new Column(nameof(Entity.ID)));
foreach(var prop in props)
{
if (flags.HasFlag(ColumnTypeFlags.Required) && prop.Required)
{
columns.Add(new Column(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(prop));
}
else if(prop is CustomProperty)
{
if (flags.HasFlag(ColumnTypeFlags.IncludeUserProperties))
{
columns.Add(new Column(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 ? Visible.Hidden : (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())
|| (!flags.HasFlag(ColumnTypeFlags.IncludeFormulae) && (prop.HasAttribute() || prop.HasAttribute()));
}
if (!failed)
{
columns.Add(new Column(prop));
}
}
}
}
return this;
}
#region IColumns
IColumns IColumns.Add(string column) => Add(column);
IColumns IColumns.Add(Expression> expression)
{
return Add(CoreUtils.GetFullPropertyName(expression, "."));
}
public IColumns Add(IColumn column)
{
if (column is Column 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 columnnames) => Add(columnnames);
#endregion
#region IBaseColumns
IEnumerable IBaseColumns.Columns() => columns;
#endregion
#region Add
private Columns Add(string column)
{
if (!Contains(column))
{
if(CoreUtils.TryGetProperty(typeof(T), column, out var propertyInfo))
{
columns.Add(new Column(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(property));
}
}
}
return this;
}
public Columns Add(Column column)
{
if(!Contains(column.Property))
{
columns.Add(column);
}
return this;
}
public Columns Add(IProperty property)
{
if (!Contains(property.Name))
{
columns.Add(new Column(property));
}
return this;
}
public Columns Add(Expression> expression)
=> Add(CoreUtils.GetFullPropertyName(expression, "."));
public Columns Add(Expression> expression)
=> Add(CoreUtils.GetFullPropertyName(expression, "."));
#region Range Adds
public Columns AddSubColumns(Expression> super, Columns? sub)
{
sub ??= CoreUtils.GetColumns(sub);
var prefix = CoreUtils.GetFullPropertyName(super, ".") + ".";
foreach(var column in sub.ColumnNames())
{
columns.Add(new Column(prefix + column));
}
return this;
}
public Columns Add(IEnumerable> columns)
{
foreach(var col in columns)
{
Add(col);
}
return this;
}
public Columns Add(params string[] columnnames)
{
foreach (var name in columnnames)
Add(name);
return this;
}
public Columns Add(IEnumerable columnnames)
{
foreach (var name in columnnames)
Add(name);
return this;
}
public Columns Add(params Expression>[] expressions)
{
foreach (var expression in expressions)
columns.Add(new Column(expression));
return this;
}
///
/// Add a range of columns, without checking for duplicates.
///
///
///
public Columns AddRange(IEnumerable> columns)
{
this.columns.AddRange(columns);
return this;
}
#endregion
#endregion
#region Remove
public Columns Remove(string column)
{
columns.RemoveAll(x => x.Property == column);
return this;
}
#endregion
#region Casting Columns Type
public Columns Cast()
where TNew : T
{
var cols = Columns.None();
foreach(var column in columns)
{
cols.Add(column.Cast());
}
return cols;
}
///
/// Cast the columns to , keeping the columns that are found in both and .
///
///
///
public Columns CastIntersection()
{
var cols = Columns.None();
foreach(var column in columns)
{
if (column.TryCast(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> 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 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(property));
}
}
#endregion
#region ICollection
public IEnumerator> GetEnumerator()
{
return columns.GetEnumerator();
}
IEnumerator IColumns.GetEnumerator() => GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
{
return columns.GetEnumerator();
}
void ICollection>.Add(Column item)
{
Add(item);
}
void ICollection>.Clear()
{
columns.Clear();
}
bool ICollection>.Contains(Column item)
{
return Contains(item.Property);
}
void ICollection>.CopyTo(Column[] array, int arrayIndex)
{
columns.CopyTo(array, arrayIndex);
}
bool ICollection>.Remove(Column item)
{
return columns.RemoveAll(x => x.Property == item.Property) > 0;
}
#endregion
}
public static class ColumnsExtensions
{
public static Columns ToColumns(this IEnumerable> columns, ColumnTypeFlags flags)
{
return new Columns(flags).AddRange(columns);
}
}
public static class ColumnSerialization
{
///
/// Inverse of .
///
///
///
public static Columns? ReadColumns(this CoreBinaryReader reader)
{
if (reader.ReadBoolean())
{
var columns = Columns.None();
columns.DeserializeBinary(reader);
return columns;
}
return null;
}
///
/// Inverse of .
///
///
///
public static void Write(this CoreBinaryWriter writer, Columns? 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 null)
{
writer.WriteNull();
return;
}
var property = (CoreUtils.GetPropertyValue(value, "Expression") as Expression)
?? throw new Exception("'Column.Expression' may not be null");
var prop = CoreUtils.ExpressionToString(value.GetType().GenericTypeArguments[0], property, true);
var name = CoreUtils.GetPropertyValue(value, "Property") as string;
writer.WriteStartObject();
writer.WritePropertyName("$type");
writer.WriteValue(value.GetType().FullName);
writer.WritePropertyName("Expression");
writer.WriteValue(prop);
writer.WritePropertyName("Property");
writer.WriteValue(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();
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;
}
}
}