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 AutoProperties;

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

    /// <summary>
    ///     Observable object with INotifyPropertyChanged implemented
    /// </summary>
    public abstract class BaseObject : INotifyPropertyChanged, IBaseObject
    {
        [GetInterceptor]
        protected T GetValue<T>(string name, Type propertyType, PropertyInfo propertyInfo, FieldInfo fieldInfo, object fieldValue, T genricFieldValue, ref T refToBackingField)
        {
            if (propertyType.IsSubclassOf(typeof(BaseObject)) && !propertyType.IsAbstract && refToBackingField == null)
            {
                if (this.GetType().IsAbstract)
                    return refToBackingField;
                if (propertyType.IsGenericType)
                    return refToBackingField;
                try
                {
                    refToBackingField ??= Activator.CreateInstance<T>();
                    if (refToBackingField is ISubObject subObject)
                    {
                        subObject.SetLinkedParent(this);
                        subObject.SetLinkedPath(name);
                    }
                }
                catch (Exception e)
                {

                }
            }

            return refToBackingField;
        }
        
        [SetInterceptor]
        protected void SetValue<T>(string name, Type propertyType, PropertyInfo propertyInfo, FieldInfo fieldInfo, object newValue, T genricNewValue, out T refToBackingField)
        {
            try
            {
                refToBackingField = genricNewValue;
            }
            catch (System.Exception e)
            {
                refToBackingField = default(T);
                //Console.WriteLine(e);
                //throw;
            }
            
        }
        
        
        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<string>();

            //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;

        private readonly ConcurrentDictionary<string, object?> originalValues = new ConcurrentDictionary<string, object?> ();
        private readonly HashSet<string> loadedColumns = new HashSet<string>();
        
        [DoNotPersist] 
        public ConcurrentDictionary<string, object?> OriginalValues => originalValues;
        
        [DoNotPersist] 
        [DoNotSerialize] 
        public HashSet<string> LoadedColumns => loadedColumns;

        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<string, object?>();
        //         _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<string>();
            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<string, object?>? DefaultProperties;

        private void CheckUserProperties()
        {
            if (_userproperties == null)
            {
                _userproperties = new UserProperties();
                if (DefaultProperties == null)
                {
                    DefaultProperties = new Dictionary<string, object?>();
                    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<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))
                {
                    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<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.OriginalValues != null && sender.OriginalValues.ContainsKey(propertyname);
        }

        public static TType GetOriginalValue<T, TType>(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;
        }

        /// <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)
                {
                    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<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)
                {
                    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<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.OriginalValues[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.OriginalValues != null && sender.OriginalValues.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.OriginalValues != null && sender.OriginalValues.ContainsKey(prop.Name)
                ? (TType)CoreUtils.ChangeType(sender.OriginalValues[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.OriginalValues != null && sender.OriginalValues.ContainsKey(prop.Name)
                ? (TType)CoreUtils.ChangeType(sender.OriginalValues[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.OriginalValues[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; }
    }
}