using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
//using PropertyChanged;
namespace InABox.Core
{
public class DoNotSerialize : Attribute
{
}
public interface IBaseObject
{
public bool IsChanged();
public void CancelChanges();
public void CommitChanges();
public bool IsObserving();
public void SetObserving(bool active);
}
///
/// Observable object with INotifyPropertyChanged implemented
///
public abstract class BaseObject : INotifyPropertyChanged, IBaseObject
{
public BaseObject()
{
SetObserving(false);
DatabaseSchema.InitializeSubObjects(this);
Init();
SetObserving(true);
}
[OnDeserializing]
internal void OnDeserializingMethod(StreamingContext context)
{
if (_observing)
SetObserving(false);
}
[OnDeserialized]
internal void OnDeserializedMethod(StreamingContext context)
{
if (!_observing)
SetObserving(true);
}
protected virtual void Init()
{
CheckOriginalValues();
LoadedColumns = new HashSet();
UserProperties = new UserProperties();
//UserProperties.ParentType = this.GetType();
DatabaseSchema.InitializeObject(this);
UserProperties.OnPropertyChanged += (o, n, b, a) =>
{
if (IsObserving())
OnPropertyChanged(n, b, a);
};
CheckSequence();
}
private void CheckSequence()
{
if (this is ISequenceable seq && seq.Sequence <= 0)
{
seq.Sequence = CoreUtils.GenerateSequence();
}
}
#region Observing Flags
public static bool GlobalObserving = true;
private bool _observing = true;
public bool IsObserving()
{
return GlobalObserving && _observing;
}
public void SetObserving(bool active)
{
bApplyingChanges = true;
_observing = active;
foreach (var oo in DatabaseSchema.GetSubObjects(this))
oo.SetObserving(active);
bApplyingChanges = false;
}
protected virtual void DoPropertyChanged(string name, object? before, object? after)
{
}
public event PropertyChangedEventHandler? PropertyChanged;
private bool bApplyingChanges;
private bool bChanged;
[DoNotPersist]
public ConcurrentDictionary OriginalValues { get; set; }
[DoNotPersist]
[DoNotSerialize]
public HashSet LoadedColumns { get; set; }
protected virtual void SetChanged(string name, object? before, object? after)
{
bChanged = true;
if (!bApplyingChanges)
{
try
{
bApplyingChanges = true;
DoPropertyChanged(name, before, after);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
catch (Exception e)
{
}
bApplyingChanges = false;
}
}
private bool QueryChanged()
{
CheckOriginalValues();
if (OriginalValues.Count > 0)
return true;
foreach (var oo in DatabaseSchema.GetSubObjects(this))
if (oo.IsChanged())
return true;
return false;
}
public void OnPropertyChanged(string name, object? before, object? after)
{
if (!IsObserving())
return;
if (name.Equals("IsChanged"))
return;
if (name.Equals("Observing"))
return;
if (name.Equals("OriginalValues"))
return;
LoadedColumns.Add(name);
if (!BaseObjectExtensions.HasChanged(before, after))
return;
CheckOriginalValues();
if (!OriginalValues.ContainsKey(name))
OriginalValues[name] = before;
SetChanged(name, before, after);
}
private void CheckOriginalValues()
{
if (OriginalValues == null)
{
var bObserving = _observing;
_observing = false;
OriginalValues = new ConcurrentDictionary();
_observing = bObserving;
}
}
public bool IsChanged()
{
return IsObserving() ? QueryChanged() : bChanged;
}
public void CancelChanges()
{
bApplyingChanges = true;
var bObs = IsObserving();
SetObserving(false);
CheckOriginalValues();
foreach (var key in OriginalValues.Keys.ToArray())
{
try
{
var prop = key.Contains(".")
? CoreUtils.GetProperty(GetType(),key)
: GetType().GetRuntimeProperty(key);
if(prop != null)
{
if (prop.SetMethod != null)
{
var val = OriginalValues[key];
// Funky 64bit stuff here?
if (prop.PropertyType == typeof(int) && val?.GetType() == typeof(long))
val = Convert.ToInt32(val);
prop.SetValue(this, val);
}
}
else if (UserProperties.ContainsKey(key))
{
UserProperties[key] = OriginalValues[key];
}
else
{
Logger.Send(LogType.Error, "", $"'{key}' is neither a runtime property nor custom property of {GetType().Name}");
}
}
catch (Exception e)
{
Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
}
}
OriginalValues.Clear();
bChanged = false;
foreach (var oo in DatabaseSchema.GetSubObjects(this)) oo.CancelChanges();
SetObserving(bObs);
bApplyingChanges = false;
}
public void CommitChanges()
{
bApplyingChanges = true;
CheckOriginalValues();
OriginalValues.Clear();
bChanged = false;
foreach (var oo in DatabaseSchema.GetSubObjects(this))
oo.CommitChanges();
bApplyingChanges = false;
}
public string ChangedValues()
{
var result = new List();
var type = GetType();
try
{
CheckOriginalValues();
foreach (var key in OriginalValues.Keys)
try
{
if (UserProperties.ContainsKey(key))
{
var obj = UserProperties[key];
result.Add(string.Format("[{0} = {1}]", key, obj != null ? obj.ToString() : "null"));
}
else
{
var prop = DatabaseSchema.Property(type, key);// GetType().GetProperty(key);
if (prop is StandardProperty standard && standard.Loggable != null)
{
/*var attribute = //prop.GetCustomAttributes(typeof(LoggablePropertyAttribute), true).FirstOrDefault();
if (attribute != null)
{*/
//var lpa = (LoggablePropertyAttribute)attribute;
var format = standard.Loggable.Format;
var value = standard.Getter()(this);
if (string.IsNullOrEmpty(format))
result.Add($"[{key} = {value}]");
else
result.Add(string.Format("[{0} = {1:" + format + "}]", key, value));
//}
}
}
}
catch (Exception e)
{
Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
}
}
catch (Exception e)
{
Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
}
return string.Join(" ", result);
}
#endregion
#region UserProperties
private UserProperties _userproperties;
[DoNotPersist]
public UserProperties UserProperties
{
get
{
CheckUserProperties();
return _userproperties;
}
set
{
_userproperties = value;
CheckUserProperties();
}
}
private static Dictionary? DefaultProperties;
private void CheckUserProperties()
{
if (_userproperties == null)
{
_userproperties = new UserProperties();
if (DefaultProperties == null)
{
DefaultProperties = new Dictionary();
var props = DatabaseSchema.Properties(GetType()).Where(x => x is CustomProperty).ToArray();
foreach (var field in props)
DefaultProperties[field.Name] = DatabaseSchema.DefaultValue(field.PropertyType);
}
_userproperties.LoadFromDictionary(DefaultProperties);
}
}
#endregion
}
public class BaseObjectSnapshot
where T : BaseObject
{
private readonly List<(IProperty, object?)> Values;
private readonly T Object;
public BaseObjectSnapshot(T obj)
{
Values = new List<(IProperty, object?)>();
foreach(var property in DatabaseSchema.Properties(obj.GetType()))
{
Values.Add((property, property.Getter()(obj)));
}
Object = obj;
}
public void ResetObject()
{
Object.CancelChanges();
var bObs = Object.IsObserving();
Object.SetObserving(false);
foreach(var (prop, value) in Values)
{
var oldValue = prop.Getter()(Object);
prop.Setter()(Object, value);
if(BaseObjectExtensions.HasChanged(oldValue, value))
{
var parent = prop.Parent != null ? prop.Parent.Getter()(Object) as BaseObject : Object;
if(parent != null)
{
var localPropName = prop is StandardProperty stdProp ? stdProp.Property.Name : prop.Name;
parent.OriginalValues[localPropName] = oldValue;
}
}
}
Object.SetObserving(bObs);
}
}
public static class BaseObjectExtensions
{
public static bool HasChanged(object? before, object? after)
{
if ((before == null || before.Equals("")) && (after == null || after.Equals("")))
return false;
if (before == null != (after == null))
return true;
if (!before!.GetType().Equals(after!.GetType()))
return true;
if (before is string[] && after is string[])
return !(before as string[]).SequenceEqual(after as string[]);
return !before.Equals(after);
}
public static bool HasColumn(this T sender, string column)
where T : BaseObject
{
return sender.LoadedColumns.Contains(column);
}
public static bool HasColumn(this T sender, Expression> column)
where T : BaseObject
{
return sender.LoadedColumns.Contains(CoreUtils.GetFullPropertyName(column, "."));
}
public static bool HasOriginalValue(this T sender, string propertyname) where T : BaseObject
{
return sender.OriginalValues != null && sender.OriginalValues.ContainsKey(propertyname);
}
public static TType GetOriginalValue(this T sender, string propertyname) where T : BaseObject
{
return sender.OriginalValues != null && sender.OriginalValues.ContainsKey(propertyname)
? (TType)CoreUtils.ChangeType(sender.OriginalValues[propertyname], typeof(TType))
: default;
}
///
/// Get all database values (i.e., non-calculated, local properties) for a given object .
/// If is , only retrieve values which have changed.
///
public static Dictionary GetValues(this T sender, bool all) where T : BaseObject
{
var result = new Dictionary();
foreach(var property in DatabaseSchema.Properties(sender.GetType()))
{
if (property.IsDBColumn)
{
var isLocal = !property.HasParentEntityLink()
|| (property.Parent?.HasParentEntityLink() != true && property.Name.EndsWith(".ID"));
if (isLocal)
{
if (all)
{
result[property.Name] = property.Getter()(sender);
}
else
{
if(property is StandardProperty stdProp)
{
var parent = property.Parent != null ? property.Parent.Getter()(sender) as BaseObject : sender;
if(parent?.HasOriginalValue(stdProp.Property.Name) == true)
{
result[property.Name] = property.Getter()(sender);
}
}
else if(property is CustomProperty customProp)
{
if (sender.HasOriginalValue(customProp.Name))
{
result[property.Name] = property.Getter()(sender);
}
}
}
}
}
}
return result;
}
public static Dictionary GetOriginaValues(this T sender) where T : BaseObject
{
var result = new Dictionary();
foreach(var property in DatabaseSchema.Properties(sender.GetType()))
{
if (property.IsDBColumn)
{
var isLocal = !property.HasParentEntityLink()
|| (property.Parent?.HasParentEntityLink() != true && property.Name.EndsWith(".ID"));
if (isLocal)
{
var parent = property.Parent != null ? property.Parent.Getter()(sender) as BaseObject : sender;
var localPropName = property is StandardProperty stdProp ? stdProp.Property.Name : property.Name;
if (parent != null)
if (parent.OriginalValues.TryGetValue(localPropName, out var value))
result[property.Name] = value;
}
}
}
return result;
}
public static BaseObjectSnapshot TakeSnapshot(this T obj)
where T : BaseObject
{
return new BaseObjectSnapshot(obj);
}
public static List Compare(this T sender, Dictionary original) where T : BaseObject
{
var result = new List();
var current = GetValues(sender, true);
foreach (var key in current.Keys)
if (original.ContainsKey(key))
{
if (current[key] == null)
{
if (original[key] != null)
result.Add(string.Format("[{0}] has changed from [{1}] to [{2}]", key, original[key], current[key]));
}
else
{
if (!current[key].Equals(original[key]))
result.Add(string.Format("[{0}] has changed from [{1}] to [{2}]", key, original[key], current[key]));
}
}
else
{
result.Add(string.Format("[{0}] not present in previous dictionary!", key));
}
return result;
}
public static bool GetValue(this T sender, Expression> property, bool original, out TType result) where T : BaseObject
{
if (sender.HasOriginalValue(property))
{
if (original)
result = sender.GetOriginalValue(property);
else
{
var expr = property.Compile();
result = expr(sender);
}
return true;
}
result = default(TType);
return false;
}
public static void SetOriginalValue(this T sender, string propertyname, TType value) where T : BaseObject
{
sender.OriginalValues[propertyname] = value;
}
public static bool HasOriginalValue(this T sender, Expression> property) where T : BaseObject
{
//var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
String propname = CoreUtils.GetFullPropertyName(property, ".");
return !String.IsNullOrWhiteSpace(propname) && sender.OriginalValues != null && sender.OriginalValues.ContainsKey(propname);
}
public static TType GetOriginalValue(this T sender, Expression> property) where T : BaseObject
{
var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
return prop != null && sender.OriginalValues != null && sender.OriginalValues.ContainsKey(prop.Name)
? (TType)CoreUtils.ChangeType(sender.OriginalValues[prop.Name], typeof(TType))
: default;
}
public static TType GetOriginalValue(this T sender, Expression> property, TType defaultValue) where T : BaseObject
{
var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
return prop != null && sender.OriginalValues != null && sender.OriginalValues.ContainsKey(prop.Name)
? (TType)CoreUtils.ChangeType(sender.OriginalValues[prop.Name], typeof(TType))
: defaultValue;
}
public static void SetOriginalValue(this T sender, Expression> property, TType value) where T : BaseObject
{
var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
sender.OriginalValues[prop.Name] = value;
}
}
///
/// An is loggable if it has the defined on it.
/// If it is part of an , then it is only loggable if the property on the parent class
/// also has .
///
public class LoggablePropertyAttribute : Attribute
{
public string Format { get; set; }
}
}