using InABox.Clients;
using System;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;

namespace InABox.Core
{
    public interface IProperty
    {
        string Class { get; set; }

        Type? ClassType { get; set; }

        string Name { get; set; }

        string Type { get; set; }

        Type PropertyType { get; set; }

        string Page { get; set; }

        /// <summary>
        /// Whether the property or any parents actually declares an editor.
        /// </summary>
        /// <remarks>
        /// If <c>false</c>, <see cref="Editor"/> will be a <see cref="NullEditor"/>.
        /// </remarks>
        bool HasEditor { get; set; }

        BaseEditor Editor { get; set; }

        long Sequence { get; set; }

        string Comment { get; set; }

        string Caption { get; set; }

        bool IsCalculated { get; }

        bool IsPersistable { get; }

        bool IsSerializable { get; }

        /// <summary>
        /// Returns <see langword="true"/> if this is a column to be stored in a database table.
        /// </summary>
        /// <remarks>
        /// This is equivalent to the property being a <see cref="CustomProperty"/> or a <see cref="StandardProperty"/> which
        /// is non-calculated, persistable, writeable and is a <i>local property</i>.
        /// </remarks>
        bool IsDBColumn { get; }

        /// <summary>
        /// An <see cref="IProperty"/> is required if it has the <see cref="RequiredColumnAttribute"/> defined on it.<br/>
        /// If it is part of an <see cref="IEntityLink"/>, then it is only required if the <see cref="IEntityLink"/> property on the parent class
        /// also has <see cref="RequiredColumnAttribute"/>.
        /// </summary>
        bool Required { get; set; }

        /// <summary>
        /// Null if the <see cref="IProperty"/> is not loggable.<br/>
        /// 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>
        LoggablePropertyAttribute? Loggable { get; set; }

        IProperty? Parent { get; set; }

        bool IsEntityLink { get; set; }

        bool IsEnclosedEntity { get; set; }

        bool IsParent { get; set; }

        Expression Expression();

        Func<object, object> Getter();

        internal Func<object, object> NullSafeGetter();

        Action<object, object?> Setter();

        TAttribute? GetAttribute<TAttribute>() where TAttribute : Attribute;

        decimal PropertySequence();
    }

    public static class PropertyExtensions
    {
        public static bool HasAttribute<TAttribute>(this IProperty property) where TAttribute : Attribute
            => property.GetAttribute<TAttribute>() != null;

        /// <summary>
        /// Get the outermost parent property which has an editor.
        /// </summary>
        /// <param name="property"></param>
        /// <returns></returns>
        public static IProperty? GetParentWithEditor(this IProperty property)
        {
            if (property.Parent == null) return null;

            var parent = property.Parent.GetParentWithEditor();
            if (parent != null) return parent;
            if (property.Parent.HasEditor)
            {
                return property.Parent;
            }
            return null;
        }

        /// <summary>
        /// Gets the outermost parent property which matches the predicate.
        /// </summary>
        /// <param name="property"></param>
        /// <param name="predicate"></param>
        /// <returns></returns>
        public static IProperty? GetOuterParent(this IProperty property, Func<IProperty, bool> predicate)
        {
            if (property.Parent == null) return null;

            return property.Parent.GetOuterParent(predicate)
                ?? (predicate(property.Parent) ? property.Parent : null);
        }

        /// <summary>
        /// Gets the innermost parent property which matches the predicate.
        /// </summary>
        /// <param name="property"></param>
        /// <param name="predicate"></param>
        /// <returns></returns>
        public static IProperty? GetParent(this IProperty property, Func<IProperty, bool> predicate)
        {
            if (property.Parent == null) return null;

            if(predicate(property.Parent)) return property.Parent;
            return property.Parent.GetParent(predicate);
        }

        public static bool HasParentEditor(this IProperty property)
        {
            return property.Parent != null && (property.Parent.HasEditor || property.Parent.HasParentEditor());
        }

        public static bool HasParentEntityLink(this IProperty property)
        {
            return property.Parent != null && (property.Parent.IsEntityLink || property.Parent.HasParentEntityLink());
        }

        public static bool ShouldShowEditor(this IProperty property)
        {
            if (property.Parent == null)
                return true;
            if (property.HasParentEditor())
                return false;
            if (property.Parent.IsEntityLink && !property.Name.EndsWith(".ID"))
                return false;
            if (property.Parent.HasParentEntityLink())
                return false;
            return true;
        }

        public static bool IsForeignKey(this IProperty property)
        {
            return property.Parent != null && property.Parent.IsEntityLink && property.Name.EndsWith(".ID");
        }
    }
}