using AutoProperties;
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;

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);
    }

    public interface IOriginalValues : IEnumerable<KeyValuePair<string, object?>>
    {
        public object? this[string key] { get; set; }

        public bool ContainsKey(string key);

        public void Clear();

        public bool TryGetValue(string key, out object? value);

        public bool TryAdd(string key, object? value);

        public void Remove(string key);

        public object? GetValueOrDefault(string key)
        {
            if(TryGetValue(key, out object? value)) return value;
            return null;
        }
    }
    public class OriginalValues : IOriginalValues
    {
        public OriginalValues()
        {
        }

        public ConcurrentDictionary<string, object?> Dictionary { get; set; } = new ConcurrentDictionary<string, object?>();

        public object? this[string key] { get => Dictionary[key]; set => Dictionary[key] = value; }

        public void Clear()
        {
            Dictionary.Clear();
        }

        public bool ContainsKey(string key) => Dictionary.ContainsKey(key);

        public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
        {
            return Dictionary.GetEnumerator();
        }

        public bool TryGetValue(string key, out object? value)
        {
            return Dictionary.TryGetValue(key, out value);
        }

        public bool TryAdd(string key, object? value)
        {
            return Dictionary.TryAdd(key, value);
        }

        public void Remove(string key)
        {
            (Dictionary as IDictionary<string, object?>).Remove(key);
        }

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
    }

    public interface ILoadedColumns : IEnumerable<string>
    {
        public bool Add(string key);

        public bool Contains(string key);
    }
    public class LoadedColumns : ILoadedColumns
    {
        private HashSet<string> Columns = new HashSet<string>();

        public bool Add(string key)
        {
            return Columns.Add(key);
        }

        public bool Contains(string key)
        {
            return Columns.Contains(key);
        }

        public IEnumerator<string> GetEnumerator()
        {
            return Columns.GetEnumerator();
        }

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

    /// <summary>
    ///     Observable object with INotifyPropertyChanged implemented
    /// </summary>
    public abstract class BaseObject : INotifyPropertyChanged, IBaseObject
    {
        public BaseObject()
        {
            SetObserving(false);
            Init();
            SetObserving(true);
        }

        internal bool _disabledInterceptor;

        protected T InitializeField<T>(ref T? field, [CallerMemberName] string name = "")
            where T : BaseObject, ISubObject, new()
        {
            if(field is null)
            {
                var value = new T();
                value.SetLinkedParent(this);
                value.SetLinkedPath(name);
                value.SetObserving(_observing);
                field = value;
            }
            return field;
        }

        [GetInterceptor]
        protected T GetValue<T>(Type propertyType, ref T field, string name)
        {
            if (_disabledInterceptor) return field;

            if(field is null && propertyType.HasInterface<ISubObject>() && !propertyType.IsAbstract)
            {
                var value = Activator.CreateInstance<T>();
                var subObj = (value as ISubObject)!;
                subObj.SetLinkedParent(this);
                subObj.SetLinkedPath(name);
                if(subObj is BaseObject obj)
                {
                    obj.SetObserving(_observing);
                }
                field = value;
            }
            return field;
        }

        [SetInterceptor]
        protected void SetValue<T>(ref T field, T newValue)
        {
            field = newValue;
        }

        [OnDeserializing]
        internal void OnDeserializingMethod(StreamingContext context)
        {
            if (_observing)
                SetObserving(false);
        }

        [OnDeserialized]
        internal void OnDeserializedMethod(StreamingContext context)
        {
            if (!_observing)
                SetObserving(true);
        }

        protected virtual void Init()
        {
            LoadedColumns = CreateLoadedColumns();
            
            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 = false;

        public bool IsObserving()
        {
            return GlobalObserving && _observing;
        }

        public void SetObserving(bool active)
        {
            bApplyingChanges = true;
            _observing = active;
            _disabledInterceptor = true;
            foreach (var oo in DatabaseSchema.GetSubObjects(this))
                oo.SetObserving(active);
            _disabledInterceptor = false;
            bApplyingChanges = false;
        }

        protected virtual void DoPropertyChanged(string name, object? before, object? after)
        {
        }

        public event PropertyChangedEventHandler? PropertyChanged;
        private bool bApplyingChanges;

        private bool bChanged;

        private IOriginalValues? _originalValues;
        [DoNotPersist]
        [DoNotSerialize]
        public IOriginalValues OriginalValueList
        {
            get
            {
                _originalValues ??= CreateOriginalValues();
                return _originalValues;
            }
        }

        [DoNotPersist]
        public ConcurrentDictionary<string, object?>? OriginalValues
        {
            get
            {
                if(OriginalValueList is OriginalValues v)
                {
                    return v.Dictionary;
                }
                else
                {
                    return null;
                }
            }
            set
            {
                if(value != null && OriginalValueList is OriginalValues v)
                {
                    v.Dictionary = value;
                }
            }
        }

        [DoNotPersist]
        [DoNotSerialize]
        [JsonIgnore]
        public ILoadedColumns 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)
                {
                    
                }
                bApplyingChanges = false;
            }
        }

        // This function is *only* meant to be called by EnclosedEntity and EntityLink
        internal void CascadePropertyChanged(string name, object? before, object? after)
        {
            SetChanged(name, before, after);
        }

        private bool QueryChanged()
        {
            if (OriginalValueList.Any())
                return true;

            _disabledInterceptor = true;
            foreach (var oo in DatabaseSchema.GetSubObjects(this))
                if (oo.IsChanged())
                {
                    _disabledInterceptor = false;
                    return true;
                }
            _disabledInterceptor = false;

            return false;
        }

        public void OnPropertyChanged(string name, object? before, object? after)
        {
            if (!IsObserving())
                return;

            if (name.Equals("IsChanged") || name.Equals("Observing") || name.Equals("OriginalValues"))
                return;

            LoadedColumns.Add(name);

            if (!BaseObjectExtensions.HasChanged(before, after))
                return;

            if (!OriginalValueList.ContainsKey(name))
                OriginalValueList[name] = before;
            SetChanged(name, before, after);
        }

        protected virtual IOriginalValues CreateOriginalValues()
        {
            return new OriginalValues();
        }
        protected virtual ILoadedColumns CreateLoadedColumns()
        {
            return new LoadedColumns();
        }

        public bool IsChanged()
        {
            return IsObserving() ? QueryChanged() : bChanged;
        }

        public void CancelChanges()
        {
            bApplyingChanges = true;
            var bObs = IsObserving();
            SetObserving(false);

            foreach (var (key, value) in OriginalValueList)
            {
                try
                {
                    var prop = DatabaseSchema.Property(GetType(), key);
                    if(prop != null)
                    {
                        prop.Setter()(this, value);
                    }
                    else
                    {
                        Logger.Send(LogType.Error, "", $"'{key}' is not a property of {GetType().Name}");
                    }
                }
                catch (Exception e)
                {
                    Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
                }
            }

            OriginalValueList.Clear();


            bChanged = false;

            _disabledInterceptor = true;
            foreach (var oo in DatabaseSchema.GetSubObjects(this))
            {
                oo.CancelChanges();
            }
            _disabledInterceptor = false;

            SetObserving(bObs);
            bApplyingChanges = false;
        }

        public void CommitChanges()
        {
            bApplyingChanges = true;

            OriginalValueList.Clear();

            bChanged = false;

            _disabledInterceptor = true;
            foreach (var oo in DatabaseSchema.GetSubObjects(this))
                oo.CommitChanges();
            _disabledInterceptor = false;

            bApplyingChanges = false;
        }

        public string ChangedValues()
        {
            var result = new List<string>();
            var type = GetType();
            try
            {
                foreach (var (key, _) in OriginalValueList)
                    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;

        private static readonly Dictionary<Type, Dictionary<string, object?>> DefaultProperties = new Dictionary<Type, Dictionary<string, object?>>();

        [DoNotPersist]
        public UserProperties UserProperties
        {
            get
            {
                if (_userproperties == null)
                {
                    _userproperties = new UserProperties();

                    var type = GetType();
                    if (!DefaultProperties.TryGetValue(type, out var defaultProps))
                    {
                        defaultProps = new Dictionary<string, object?>();
                        var props = DatabaseSchema.Properties(type).Where(x => x is CustomProperty);
                        foreach (var field in props)
                            defaultProps[field.Name] = DatabaseSchema.DefaultValue(field.PropertyType);
                        DefaultProperties[type] = defaultProps;
                    }

                    _userproperties.LoadFromDictionary(defaultProps);

                    _userproperties.OnPropertyChanged += (o, n, b, a) =>
                    {
                        if (IsObserving())
                            OnPropertyChanged(n, b, a);
                    };
                }
                return _userproperties;
            }
        }

        #endregion
    }

    public class BaseObjectSnapshot<T>
        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))
                {
                    Object.OriginalValueList[prop.Name] = oldValue;
                }
            }
            Object.SetObserving(bObs);
        }
    }

    public static class BaseObjectExtensions
    {
        public static T Clone<T>(this T obj) where T : BaseObject, new()
        {
            var newObj = new T();
            obj._disabledInterceptor = true;
            foreach(var property in DatabaseSchema.Properties(obj.GetType()))
            {
                if (property.Parent != null && property.Parent.NullSafeGetter()(obj) is null) continue;

                property.Setter()(newObj, property.Getter()(obj));
            }
            obj._disabledInterceptor = false;
            return newObj;
        }
        public static BaseObject Clone(BaseObject obj)
        {
            var newObj = (Activator.CreateInstance(obj.GetType()) as BaseObject)!;
            obj._disabledInterceptor = true;
            foreach(var property in DatabaseSchema.Properties(obj.GetType()))
            {
                if (property.Parent != null && property.Parent.NullSafeGetter()(obj) is null) continue;

                property.Setter()(newObj, property.Getter()(obj));
            }
            obj._disabledInterceptor = false;
            return newObj;
        }

        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<T>(this T sender, string column)
            where T : BaseObject
        {
            return sender.LoadedColumns.Contains(column);
        }
        public static bool HasColumn<T, TType>(this T sender, Expression<Func<T, TType>> column)
            where T : BaseObject
        {
            return sender.LoadedColumns.Contains(CoreUtils.GetFullPropertyName(column, "."));
        }

        public static bool HasOriginalValue<T>(this T sender, string propertyname) where T : BaseObject
        {
            return sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(propertyname);
        }

        public static TType GetOriginalValue<T, TType>(this T sender, string propertyname) where T : BaseObject
        {
            return sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(propertyname)
                ? (TType)CoreUtils.ChangeType(sender.OriginalValueList[propertyname], typeof(TType))
                : default;
        }

        /// <summary>
        ///     Get all database values (i.e., non-calculated, local properties) for a given object <paramref name="sender"/>.
        ///     If <paramref name="all"/> is <see langword="false"/>, only retrieve values which have changed.
        /// </summary>
        public static Dictionary<string, object?> GetValues<T>(this T sender, bool all) where T : BaseObject
        {
            var result = new Dictionary<string, object?>();

            foreach(var property in DatabaseSchema.Properties(sender.GetType()))
            {
                if (property.IsDBColumn && (all || sender.HasOriginalValue(property.Name)))
                {
                    result[property.Name] = property.Getter()(sender);
                }
            }

            return result;
        }

        public static Dictionary<string, object?> GetOriginaValues<T>(this T sender) where T : BaseObject
        {
            var result = new Dictionary<string, object?>();

            foreach(var property in DatabaseSchema.Properties(sender.GetType()))
            {
                if (property.IsDBColumn && sender.OriginalValueList.TryGetValue(property.Name, out var value))
                {
                    result[property.Name] = value;
                }
            }

            return result;
        }

        public static BaseObjectSnapshot<T> TakeSnapshot<T>(this T obj)
            where T : BaseObject
        {
            return new BaseObjectSnapshot<T>(obj);
        }

        public static List<string> Compare<T>(this T sender, Dictionary<string, object> original) where T : BaseObject
        {
            var result = new List<string>();

            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<T,TType>(this T sender, Expression<Func<T,TType>> property, bool original, out TType result) where T : BaseObject
        {
            if (sender.HasOriginalValue<T, TType>(property))
            {
                if (original)
                    result = sender.GetOriginalValue<T, TType>(property);
                else
                {
                    var expr = property.Compile();
                    result = expr(sender);                    
                }
                return true;
            }
            result = default(TType);
            return false;
        }
        
        public static void SetOriginalValue<T, TType>(this T sender, string propertyname, TType value) where T : BaseObject
        {
            sender.OriginalValueList[propertyname] = value;
        }

        public static bool HasOriginalValue<T, TType>(this T sender, Expression<Func<T, TType>> property) where T : BaseObject
        {
            //var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
            String propname = CoreUtils.GetFullPropertyName(property, ".");
            return !String.IsNullOrWhiteSpace(propname) && sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(propname);
        }

        public static TType GetOriginalValue<T, TType>(this T sender, Expression<Func<T, TType>> property) where T : BaseObject
        {
            var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
            return prop != null && sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(prop.Name)
                ? (TType)CoreUtils.ChangeType(sender.OriginalValueList[prop.Name], typeof(TType))
                : default;
        }
        
        public static TType GetOriginalValue<T, TType>(this T sender, Expression<Func<T, TType>> property, TType defaultValue) where T : BaseObject
        {
            var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
            return prop != null && sender.OriginalValueList != null && sender.OriginalValueList.ContainsKey(prop.Name)
                ? (TType)CoreUtils.ChangeType(sender.OriginalValueList[prop.Name], typeof(TType))
                : defaultValue;
        }

        public static void SetOriginalValue<T, TType>(this T sender, Expression<Func<T, TType>> property, TType value) where T : BaseObject
        {
            var prop = ((MemberExpression)property.Body).Member as PropertyInfo;
            sender.OriginalValueList[prop.Name] = value;
        }
    }

    /// <summary>
    /// An <see cref="IProperty"/> is loggable if it has the <see cref="LoggablePropertyAttribute"/> defined on it.<br/>
    /// If it is part of an <see cref="IEntityLink"/>, then it is only loggable if the <see cref="IEntityLink"/> property on the parent class
    /// also has <see cref="LoggablePropertyAttribute"/>.
    /// </summary>
    public class LoggablePropertyAttribute : Attribute
    {
        public string Format { get; set; }
    }
}