using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

namespace InABox.Core
{
    public static class DatabaseSchema
    {
        // {className: {propertyName: property}}
        private static ConcurrentDictionary<Type, ImmutableSortedDictionary<string, IProperty>> _properties
            = new ConcurrentDictionary<Type, ImmutableSortedDictionary<string, IProperty>>();

        private struct SubObject
        {
            public Type PropertyType { get; set; }

            public string Name { get; set; }

            public Action<object, object> Setter { get; set; }

            public Func<object, object> Getter { get; set; }

            public SubObject(Type objectType, Type propertyType, string name)
            {
                PropertyType = propertyType;
                Name = name;
                Setter = Expressions.Setter(objectType, name);
                Getter = Expressions.Getter(objectType, name);
            }
        }

        private static ConcurrentDictionary<Type, ImmutableList<SubObject>> _subObjects { get; } = new ConcurrentDictionary<Type, ImmutableList<SubObject>>();

        private static IReadOnlyCollection<SubObject>? GetSubObjectDefs(Type t)
        {
            CheckPropertiesInternal(t);
            return _subObjects.GetValueOrDefault(t);
        }

        public static IEnumerable<BaseObject> GetSubObjects(BaseObject obj)
        {
            var objs = GetSubObjectDefs(obj.GetType());
            if(objs is null)
            {
                yield break;
            }
            foreach (var subObjectDef in objs)
            {
                var subObj = subObjectDef.Getter(obj);
                if(subObj is BaseObject bObj)
                {
                    yield return bObj;
                }
            }
        }

        public static void InitializeSubObjects(BaseObject obj)
        {
            var objs = GetSubObjectDefs(obj.GetType());
            if(objs is null)
            {
                return;
            }
            foreach (var subObjectDef in objs)
            {
                var subObj = (Activator.CreateInstance(subObjectDef.PropertyType) as ISubObject)!;
                subObjectDef.Setter(obj, subObj);
                subObj.SetLinkedParent(obj);
                subObj.SetLinkedPath(subObjectDef.Name);
            }
        }

        // For synchronisation purposes, we register sub objects in bulk, removing the need for nested concurrent dictionaries.
        private static void RegisterSubObjects(Type objectType, IEnumerable<Tuple<Type, string>> objects)
        {
            if (!_subObjects.TryGetValue(objectType, out var subObjects))
            {
                subObjects = ImmutableList<SubObject>.Empty;
            }

            // No synchronisation issues, since the original collection is not being modified, just the entry in the concurrent dictionary is updated.
            _subObjects[objectType] = subObjects.AddRange(
                objects.Where(x => !subObjects.Any(y => x.Item1 == y.PropertyType && x.Item2 == y.Name))
                    .Select(x => new SubObject(objectType, x.Item1, x.Item2)));
        }

        public static void Clear()
        {
            _properties = new ConcurrentDictionary<Type, ImmutableSortedDictionary<string, IProperty>>();
        }

        /// <summary>
        /// Gets the editor for the specified property. If the property has a <see cref="BaseEditor"/> defined, then returns that.<br/>
        /// Otherwise, gets a default for the property type. (<see cref="GetEditor(Type)"/>), which can be <see langword="null"/>.
        /// </summary>
        /// <param name="prop"></param>
        /// <returns></returns>
        private static BaseEditor? GetEditor(PropertyInfo prop)
        {
            var attribute = prop.GetCustomAttributes<BaseEditor>(true).FirstOrDefault();
            var editor = attribute ?? EditorUtils.GetEditor(prop.PropertyType);
            if (editor != null && !prop.CanWrite && !prop.PropertyType.HasInterface<ISubObject>())
            {
                editor = editor.CloneEditor();
                editor.Editable = editor.Editable.Combine(Editable.Disabled);
            }
            return editor;
        }

        private static void RegisterProperties(Type master, Type type, string prefix, StandardProperty? parent, Dictionary<string, IProperty> newProperties)
        {
            try
            {
                var properties = CoreUtils.PropertyList(
                    type,
                    x => !x.PropertyType.IsInterface && x.DeclaringType != typeof(BaseObject)
                );

                var subObjects = new List<Tuple<Type, string>>();

                foreach (var prop in properties)
                {
                    var name = prefix + prop.Name;
                    if (newProperties.ContainsKey(name)) continue;

                    var getMethod = prop.GetGetMethod();
                    if (getMethod is null || !getMethod.IsPublic || getMethod.IsStatic) continue;

                    BaseEditor? editor;
                    if (parent != null && parent.HasEditor && parent.Editor is NullEditor)
                    {
                        editor = parent.Editor;
                    }
                    else
                    {
                        editor = GetEditor(prop);
                    }

                    if(editor is DataLookupEditor data)
                    {
                        data.ParentType = master;
                    }

                    var captionAttr = prop.GetCustomAttribute<Caption>();
                    var subCaption = captionAttr != null ? captionAttr.Text : prop.Name;
                    var path = captionAttr == null || captionAttr.IncludePath; // If no caption attribute, we should always include the path
                    var caption = parent?.Caption ?? string.Empty; // We default to the parent caption if subCaption doesn't exist
                    if (!string.IsNullOrWhiteSpace(subCaption))
                    {
                        if (!string.IsNullOrWhiteSpace(caption) && path)
                        {
                            caption = $"{caption} {subCaption}";
                        }
                        else
                        {
                            caption = subCaption;
                        }
                    }

                    // Once the parent page has been found, this property is cemented to that page - it cannot change page to its parent
                    var page = parent?.Page;
                    var sequence = parent?.Sequence;
                    var sequenceAttribute = prop.GetCustomAttribute<EditorSequence>();
                    if (sequenceAttribute != null)
                    {
                        if (string.IsNullOrWhiteSpace(page))
                        {
                            page = sequenceAttribute.Page;
                        }
                        sequence = sequenceAttribute.Sequence;
                    }
                    editor = editor?.Clone() as BaseEditor;
                    if (editor != null)
                    {
                        editor.Page = page;
                        editor.Caption = caption;
                        editor.EditorSequence = (int)(sequence ?? 999);

                        editor.Security = prop.GetCustomAttributes<SecurityAttribute>().ToArray();
                    }

                    var comment = prop.GetCustomAttribute<CommentAttribute>()?.Comment;
                    if(editor != null && editor.ToolTip.IsNullOrWhiteSpace() && !comment.IsNullOrWhiteSpace())
                    {
                        editor.ToolTip = comment;
                    }

                    bool required = false;
                    if (parent == null || parent.Required)
                    {
                        required = prop.GetCustomAttribute<RequiredColumnAttribute>() != null;
                    }

                    LoggablePropertyAttribute? loggable = null;
                    if (parent == null || parent.Loggable != null)
                    {
                        loggable = prop.GetCustomAttribute<LoggablePropertyAttribute>();
                    }

                    var newProperty = new StandardProperty
                    {
                        _class = master,
                        Name = name,
                        PropertyType = prop.PropertyType,
                        Editor = editor ?? new NullEditor(),
                        HasEditor = editor != null,
                        Caption = caption,
                        Sequence = sequence ?? 999,
                        Page = page ?? string.Empty,
                        Required = required,
                        Loggable = loggable,
                        Parent = parent,
                        Property = prop,
                        Comment = comment ?? ""
                    };

                    var parentWithEditable = newProperty.GetOuterParent(x =>
                        x is StandardProperty st
                        && st.Property.GetCustomAttribute<EditableAttribute>() != null);
                    if(parentWithEditable != null)
                    {
                        var attr = (parentWithEditable as StandardProperty)!.Property.GetCustomAttribute<EditableAttribute>()!;
                        newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable);
                    }
                    else if(prop.GetCustomAttribute<EditableAttribute>() is EditableAttribute attr)
                    {
                        newProperty.Editor.Editable = newProperty.Editor.Editable.Combine(attr.Editable);
                    }

                    var isLink = prop.PropertyType.HasInterface<IEntityLink>();
                    var isEnclosedEntity = prop.PropertyType.HasInterface<IEnclosedEntity>();
                    var isBaseEditor = prop.PropertyType.HasInterface<IBaseEditor>();
                    if ((isLink || isEnclosedEntity) && !isBaseEditor)
                    {
                        subObjects.Add(new Tuple<Type, string>(prop.PropertyType, prop.Name));
                    }

                    if (isLink || isEnclosedEntity || isBaseEditor)
                    {
                        RegisterProperties(master, prop.PropertyType, name + ".", newProperty, newProperties);
                    }
                    newProperties.Add(newProperty.Name, newProperty);
                }

                RegisterSubObjects(type, subObjects);

                // I don't actually think we need this, since PropertyList gives us properties of our parent.
                //if (type.IsSubclassOf(typeof(BaseObject)) && type.BaseType != typeof(BaseObject))
                //    RegisterProperties(master, type.BaseType, prefix, parent, newProperties);
            }
            catch (Exception e)
            {
                Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
            }
        }

        private static void RegisterProperties(Type type)
        {
            var properties = new Dictionary<string, IProperty>();
            RegisterProperties(type, type, "", null, properties);
            if(properties.Count > 0)
            {
                RegisterProperties(type, properties.Values);
            }
        }

        public static object? DefaultValue(Type type)
        {
            if (type.IsValueType)
                return Activator.CreateInstance(type);
            if (type.Equals(typeof(string)))
                return "";
            return null;
        }
        
        private static readonly object _updatelock =  new object();

        private static void RegisterProperties(Type master, IEnumerable<IProperty> toAdd)
        {
            if (!_properties.TryGetValue(master, out var properties))
            {
                properties = ImmutableSortedDictionary<string, IProperty>.Empty;
            }
            var newDict = properties.ToDictionary(x => x.Key, x => x.Value);
            foreach(var prop in toAdd)
            {
                newDict[prop.Name] = prop;
            }

            _properties[master] = newDict.ToImmutableSortedDictionary();
        }
        private static void UnregisterProperties(Type master, IEnumerable<IProperty> toRemove)
        {
            if (!_properties.TryGetValue(master, out var properties))
            {
                properties = ImmutableSortedDictionary<string, IProperty>.Empty;
            }
            var newDict = properties.ToDictionary(x => x.Key, x => x.Value);
            foreach(var prop in toRemove)
            {
                newDict.Remove(prop.Name);
            }

            _properties[master] = newDict.ToImmutableSortedDictionary();
        }
        
        public static void RegisterProperty(IProperty entry)
        {
            var type = entry.ClassType;
            if (type is null) return;

            if (!_properties.TryGetValue(type, out var properties))
            {
                properties = ImmutableSortedDictionary<string, IProperty>.Empty;
            }
            _properties[type] = properties.Add(entry.Name, entry);
        }

        public static void Load(CustomProperty[] customproperties)
        {
            var perType = customproperties.GroupBy(x => x.ClassType);
            foreach(var group in perType)
            {
                if (group.Key is null) continue;

                RegisterProperties(group.Key, group);
            }
        }

        public static void Unload(CustomProperty[] customProperties)
        {
            var perType = customProperties.GroupBy(x => x.ClassType);
            foreach(var group in perType)
            {
                if (group.Key is null) continue;

                UnregisterProperties(group.Key, group);
            }
        }

        private static ImmutableSortedDictionary<string, IProperty>? CheckPropertiesInternal(Type type)
        {
            try
            {
                var props = _properties.GetValueOrDefault(type);
                var hasprops = props?.Any(x => x.Value is StandardProperty) == true;
                if (!hasprops)
                {
                    RegisterProperties(type);
                    return _properties.GetValueOrDefault(type);
                }
                else
                {
                    return props;
                }
            }
            catch (Exception e)
            {
                // This seems to be an intermittent error "Collection has been modified" when checking if the Dictionary has been populated already
                // I've added a .ToArray() to concretise the list, but who knows?
                Logger.Send(LogType.Error,"",$"Error Checking Properties for Type: {type.EntityName()}\n{e.Message}\n{e.StackTrace}");
                return null;
            }

        }

        public static void CheckProperties(Type type)
        {
            CheckPropertiesInternal(type);
        }

        private static IEnumerable<IProperty> PropertiesInternal(Type type)
            => CheckPropertiesInternal(type)?.Values ?? Enumerable.Empty<IProperty>();

        /// <summary>
        /// Returns every property, both parents and children, for <paramref name="type"/>.
        /// </summary>
        public static IEnumerable<IProperty> AllProperties(Type type)
            => PropertiesInternal(type);

        /// <summary>
        /// Return the standard property list for <paramref name="type"/>; this includes nested properties.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static IEnumerable<IProperty> Properties(Type type)
            => PropertiesInternal(type).Where(x => !x.IsParent);

        /// <summary>
        /// Return all properties that are defined directly on <paramref name="type"/>, and does not follow sub objects, but rather includes the
        /// sub object property itself.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static IEnumerable<IProperty> RootProperties(Type type)
            => PropertiesInternal(type).Where(x => x.Parent is null);

        /// <summary>
        /// Return all properties that are defined locally on <paramref name="type"/>, following sub-objects but not entity links; does not retrieve calculated fields. (On entity links, the ID property is retrieved.)
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static IEnumerable<IProperty> LocalProperties(Type type)
            => PropertiesInternal(type).Where(
                x => !x.IsParent && (!x.HasParentEntityLink() || (x.Parent?.HasParentEntityLink() != true && x.Name.EndsWith(".ID")))
                    && !x.IsCalculated);

        /// <summary>
        /// Return the standard property list for <paramref name="type"/>; this includes nested properties.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static IEnumerable<IProperty> Properties<T>() => Properties(typeof(T));

        public static IProperty? Property(Type type, string name)
        {
            var prop = CheckPropertiesInternal(type)?.GetValueOrDefault(name);

            // Walk up the inheritance tree, see if an ancestor has this property.
            // KENRIC: not sure if this is necessary.
            if (prop == null && type.BaseType != null)
                prop = Property(type.BaseType, name);

            return prop;
        }
        public static IProperty? Property<T>(Expression<Func<T, object?>> expression) =>
            Property(typeof(T), CoreUtils.GetFullPropertyName(expression, "."));
        public static IProperty? Property<T, TType>(Expression<Func<T, TType>> expression) =>
            Property(typeof(T), CoreUtils.GetFullPropertyName(expression, "."));

        public static IProperty PropertyStrict(Type type, string name) =>
            Property(type, name) ?? throw new PropertyNotFoundException(type, name);
        public static IProperty PropertyStrict<T>(Expression<Func<T, object?>> expression) =>
            Property(expression) ?? throw new PropertyNotFoundException(typeof(T), CoreUtils.GetFullPropertyName(expression, "."));

        public class PropertyNotFoundException : Exception
        {
            public Type Type { get; set; }

            public string Property { get; set; }

            public PropertyNotFoundException(Type T, string property) : base($"Property '{property}' not found on type {T.FullName}")
            {
                Type = T;
                Property = property;
            }
        } 
    }
}