using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;

namespace InABox.Core
{
    public interface ISubObject
    {
        BaseObject? GetLinkedParent();

        void SetLinkedParent(BaseObject obj);

        string? GetLinkedPath();

        void SetLinkedPath(string path);
    }

    public static class SubObjectExtensions
    {
        public static bool Synchronise(this ISubObject obj, object Entity)
        {
            var result = false;
            foreach (var prop in DatabaseSchema.Properties(obj.GetType()))
            {
                if (prop.Name == "ID" || !(prop is StandardProperty stdProp) || stdProp.Property.DeclaringType.Equals(typeof(BaseObject))) continue;

                var entityProp = DatabaseSchema.Property(Entity.GetType(), prop.Name);
                if(entityProp != null)
                {
                    prop.Setter()(obj, entityProp.Getter()(Entity));
                }
            }

            foreach (var link in LinkedProperties.Find(obj, out var parent))
            {
                link.Update(obj, parent);
                result = true;
            }

            return result;
        }
    }

    public class SubObjectOriginalValues : IOriginalValues
    {
        private ISubObject Object { get; set; }

        private IOriginalValues? _rootDictionary;
        private IOriginalValues RootDictionary
        {
            get
            {
                _rootDictionary ??= (LinkedProperties.GetParent(Object)?.OriginalValueList ?? new OriginalValues());
                return _rootDictionary;
            }
        }

        private string? _rootPath;
        /// <summary>
        /// Path of this sub object according to the root, suffixed with a "."
        /// </summary>
        private string RootPath
        {
            get
            {
                _rootPath ??= $"{LinkedProperties.GetPath(Object)}.";
                return _rootPath;
            }
        }

        public SubObjectOriginalValues(ISubObject obj)
        {
            Object = obj;
        }

        private string ChangeKey(string key) => $"{RootPath}{key}";

        public bool ContainsKey(string key)
        {
            return RootDictionary.ContainsKey(ChangeKey(key));
        }

        public void Clear()
        {
            foreach(var (k, v) in RootDictionary)
            {
                if (k.StartsWith(RootPath))
                {
                    RootDictionary.Remove(k);
                }
            }
        }

        public void Remove(string key)
        {
            RootDictionary.Remove(ChangeKey(key));
        }

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

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

        public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
        {
            return RootDictionary.Where(x => x.Key.StartsWith(RootPath)).Select(x => new KeyValuePair<string, object?>(x.Key[RootPath.Length..], x.Value)).GetEnumerator();
        }

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

        public object? this[string key]
        {
            get => RootDictionary[ChangeKey(key)];
            set => RootDictionary[ChangeKey(key)] = value;
        }
    }

    public class SubObjectLoadedColumns : ILoadedColumns
    {
        private ISubObject Object { get; set; }

        private ILoadedColumns? _rootSet;
        private ILoadedColumns RootSet
        {
            get
            {
                _rootSet ??= (LinkedProperties.GetParent(Object)?.LoadedColumns ?? new LoadedColumns());
                return _rootSet;
            }
        }

        private string? _rootPath;
        /// <summary>
        /// Path of this sub object according to the root, suffixed with a "."
        /// </summary>
        private string RootPath
        {
            get
            {
                _rootPath ??= $"{LinkedProperties.GetPath(Object)}.";
                return _rootPath;
            }
        }

        public SubObjectLoadedColumns(ISubObject obj)
        {
            Object = obj;
        }

        private string ChangeKey(string key) => $"{RootPath}{key}";

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

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

        public IEnumerator<string> GetEnumerator()
        {
            return RootSet.Where(x => x.StartsWith(RootPath)).Select(x => x[RootPath.Length..]).GetEnumerator();
        }

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

    public static class LinkedProperties
    {

        private static readonly Dictionary<Type, List<ILinkedProperty>> _LinkedProperties = new Dictionary<Type, List<ILinkedProperty>>();
        
        public static void Register<TLinkedEntity, TEntityLink, TType>(Expression<Func<TLinkedEntity,TEntityLink>> path, Expression<Func<TEntityLink, TType>> source,
            Expression<Func<TLinkedEntity, TType>> target, Func<TLinkedEntity,TType, Result<TType, string>>? func = null)
            where TLinkedEntity : class
            where TEntityLink : class
        {
            var map = new LinkedProperty<TLinkedEntity, TEntityLink, TType>(path, source, target, func);
            if(!_LinkedProperties.TryGetValue(map.Type, out var props))
            {
                props = new List<ILinkedProperty>();
                _LinkedProperties[map.Type] = props;
            }
            if(!props.Any(x => x.Equals(map)))
            {
                props.Add(map);
            }
        }

        /*public static IEnumerable<ILinkedProperty> Find(object parent, object path)
        {
            var all =  _LinkedProperties.Where(x => x.Type == parent.GetType());
            var filtered =  all.Where(x => CoreUtils.GetPropertyValue(parent,x.Path)?.GetType() == path?.GetType());
            return filtered;
        }*/

        public static BaseObject? GetParent(ISubObject link)
        {
            while (true)
            {
                var parent = link.GetLinkedParent();
                if(parent is ISubObject parentLink)
                {
                    link = parentLink;
                }
                else 
                {
                    return parent;
                }
            }
        }

        public static string? GetPath(ISubObject link)
        {
            var path = link.GetLinkedPath();
            while (true)
            {
                var parent = link.GetLinkedParent();
                if (parent is ISubObject parentLink)
                {
                    link = parentLink;
                    path = $"{link.GetLinkedPath()}.{path}";
                }
                else
                {
                    return path;
                }
            }
        }

        public static IEnumerable<ILinkedProperty> FindAll(Type parent)
        {
            if (_LinkedProperties.TryGetValue(parent, out var props))
            {
                return props;
            }
            return Enumerable.Empty<ILinkedProperty>();
        }

        public static IEnumerable<ILinkedProperty> Find(ISubObject subObject, out BaseObject parent)
        {
            parent = GetParent(subObject);
            if (parent is null || !_LinkedProperties.TryGetValue(parent.GetType(), out var props))
            {
                return Enumerable.Empty<ILinkedProperty>();
            }

            var path = GetPath(subObject);
            return props.Where(x => x.Path == path);
        }

        public static bool Find(ISubObject subObject, string name, [NotNullWhen(true)] out ILinkedProperty? property, [NotNullWhen(true)] out BaseObject? parent)
        {
            property = null;
            parent = GetParent(subObject);
            if (parent is null)
                return false;

            if(!_LinkedProperties.TryGetValue(parent.GetType(), out var props))
                return false;
            var path = GetPath(subObject);
            property = props.FirstOrDefault(x => string.Equals(
                $"{x.Path}{(x.Path.IsNullOrWhiteSpace() ? "" : ".")}{x.Source}",
                $"{path}{(path.IsNullOrWhiteSpace() ? "" : ".")}{name}"));
            return property != null;
        }
        
    }
}