using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using InABox.Clients;
using InABox.Core;

namespace InABox.Core
{
    public class Credentials
    {
        public virtual string UserID { get; set; }
        public virtual string Password { get; set; }
    }



    public interface IEntity
    {
        Guid ID { get; set; }

        Guid Deleted { get; set; }

        bool IsChanged();

        void CommitChanges();
        void CancelChanges();

    }

    public interface ITaxable
    {
        double ExTax { get; set; }
        double TaxRate { get; set; }
        double Tax { get; set; }
        double IncTax { get; set; }
    }

    public interface IProblem
    {
        string[] Notes { get; set; }
        
        DateTime Resolved { get; set; }
    }
    
    public abstract class Problem : EnclosedEntity, IProblem
    {
        [NotesEditor]
        [EditorSequence(1)]
        [Caption("Notes", IncludePath = false)]
        public string[] Notes { get; set; }
        
        [TimestampEditor]
        [EditorSequence(999)]
        [Caption("Resolved", IncludePath = false)]
        public DateTime Resolved { get; set; }
    }

    public interface IProblems
    {
        IProblem Problem { get; }
    }
    
    public interface IProblems<T>: IProblems where T : Problem
    {
        new T Problem { get; set; }

        IProblem IProblems.Problem => Problem;
    }
    
    public interface IIssues
    {
        string Issues { get; set; }
    }

    public interface IExportable
    {
    }

    public interface IImportable
    {
    }

    /// <summary>
    ///     Indicate that an <see cref="Entity"/> is able to be merged together.
    /// </summary>
    /// <remarks>
    ///     It is recommended that an <see cref="Entity"/> that implements this should provide a <see cref="object.ToString"/> implementation.
    /// </remarks>
    public interface IMergeable
    {
    }

    public interface ISecure { }

    public interface IDuplicatable
    {
        IEntityDuplicator GetDuplicator();
    }

    public interface IEntityDuplicator
    {
        //void Duplicate(IFilter filter);

        void Duplicate(IEnumerable<BaseObject> entities);
    }

    public class EntityDuplicator<TEntity> : IEntityDuplicator where TEntity : Entity, IRemotable, IPersistent
    {
        private interface IRelationship
        {
            Type ParentType { get; }

            Type ChildType { get; }

            IFilter GetFilter(Entity parent);
        }
        private class EntityLinkRelationship<TParent, TChild> : IRelationship
        {
            public Type ParentType => typeof(TParent);

            public Type ChildType => typeof(TChild);

            public Column<TChild> Column { get; set; }

            public IFilter GetFilter(Entity parent)
            {
                return new Filter<TChild>(Column).IsEqualTo(parent.ID);
            }
        }

        private class GenericRelationship<TParent, TChild> : IRelationship
            where TParent : Entity
        {
            public Type ParentType => typeof(TParent);

            public Type ChildType => typeof(TChild);

            public Column<TChild> Column { get; set; }

            public Func<TParent, object?> Func { get; set; }

            public IFilter GetFilter(Entity parent)
            {
                return new Filter<TChild>(Column).IsEqualTo(Func(parent as TParent));
            }
        }

        private readonly List<IRelationship> _relationships = new List<IRelationship>();

        public void Duplicate(IEnumerable<TEntity> entites) =>
            Duplicate(typeof(TEntity),
                new Filter<TEntity>(x => x.ID).InList(entites.Select(x => x.ID).ToArray()));

        private void Duplicate(Type parent, IFilter filter)
        {
            var table = ClientFactory.CreateClient(parent)
                .Query(filter, Columns.Local(parent));
            foreach (var row in table.Rows)
            {
                var update = (row.ToObject(parent) as Entity)!;
                var id = update.ID;
                update.ID = Guid.Empty;
                update.CommitChanges();
                ClientFactory.CreateClient(parent).Save(update, "Duplicated Record");
                foreach (var relationship in _relationships.Where(x => x.ParentType == parent))
                {
                    Duplicate(relationship.ChildType, relationship.GetFilter(update));
                }
            }
        }

        public void AddChild<TParent, TChild, TParentLink>(Expression<Func<TChild, TParentLink>> childkey)
            where TParent : Entity, IRemotable, IPersistent
            where TChild : Entity, IRemotable, IPersistent
            where TParentLink : IEntityLink<TParent>
        {
            _relationships.Add(new EntityLinkRelationship<TParent, TChild>
            {
                Column = new Column<TChild>(CoreUtils.GetFullPropertyName(childkey, ".") + ".ID")
            });
        }

        public void AddChild<TParent, TChild>(Column<TChild> linkColumn, Func<TParent, object?> value)
            where TParent : Entity, IRemotable, IPersistent
            where TChild : Entity, IRemotable, IPersistent
        {
            _relationships.Add(new GenericRelationship<TParent, TChild>
            {
                Column = linkColumn,
                Func = value
            });
        }

        void IEntityDuplicator.Duplicate(IEnumerable<BaseObject> entities) => Duplicate(entities.Cast<TEntity>());
    }

    /// <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"/> (or <see cref="IEnclosedEntity"/>), then it is only required
    /// if the <see cref="IEntityLink"/> property on the parent class also has <see cref="RequiredColumnAttribute"/>.
    /// </summary>
    /// <remarks>
    /// In general, this should be used in one of two places - either if the property is required by the store for the entity, or if it is used by the <see cref="BaseObject.DoPropertyChanged(string, object?, object?)"/> function.
    /// </remarks>
    public class RequiredColumnAttribute : Attribute { }

    public abstract class Entity : BaseObject, IEntity
    {
        private bool bTaxing;

        //public String Name { get; set; }

        [TimestampEditor(Visible = Visible.Optional, Editable = Editable.Hidden)]
        [RequiredColumn]
        public virtual DateTime LastUpdate { get; set; } = DateTime.Now;

        [CodeEditor(Visible = Visible.Optional, Editable = Editable.Hidden)]
        [RequiredColumn]
        public string LastUpdateBy { get; set; } = ClientFactory.UserID;

        [NullEditor]
        [RequiredColumn]
        public virtual DateTime Created { get; set; } = DateTime.Now;

        [NullEditor]
        [RequiredColumn]
        public virtual string CreatedBy { get; set; } = ClientFactory.UserID;

        [NullEditor]
        [RequiredColumn]
        public Guid ID { get; set; } = Guid.Empty;

        /// <summary>
        /// If the entity is deleted, holds the ID of the Deletion. Otherwise, it holds Guid.Empty
        /// </summary>
        [NullEditor]
        [DoNotSerialize]
        [Obsolete]
        public Guid Deleted { get; set; } = Guid.Empty;

        public static Type ClassVersion(Type t)
        {
            //Type t = MethodBase.GetCurrentMethod().DeclaringType;
            var ti = t.GetTypeInfo();
            var interfaces = ti.GetInterfaces();
            if (ti.GetInterfaces().Contains(typeof(IPersistent)))
            {
                if (ti.BaseType != null)
                    throw new Exception(t.Name + " hase no Base Type");

                if (ti.BaseType.Equals(typeof(Entity)))
                    throw new Exception(t.Name + " may not derive directly from TEntity");

                var props = t.GetTypeInfo().GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance);
                if (props.Count() > 0)
                    throw new Exception(t.Name + "may not declare properties");
            }

            return t.GetTypeInfo().BaseType;
        }

        //[NullEditor]
        //public List<EntityHistory> History { get; set; }


        //public Entity() : base()
        //{
        //    CommitChanges();
        //}

        //public Entity(Guid id) : base()
        //{

        //    ID = id;
        //    History = new List<EntityHistory>();
        //    UserProperties = new Dictionary<string, Object>();
        //    DataModel.InitializeEntity(this);
        //    CheckSequence();
        //    CommitChanges();
        //}

        public static bool IsEntityLinkValid<T, U>(Expression<Func<T, U>> expression, CoreRow arg) where U : IEntityLink
        {
            return arg.IsEntityLinkValid(expression);
        }
        /// <summary>
        /// Gets the ID of an entity link of an entity, doing a validity check (see <see cref="IsEntityLinkValid{T, U}(Expression{Func{T, U}}, CoreRow)"/>)
        /// </summary>
        /// <typeparam name="T">The entity type</typeparam>
        /// <typeparam name="U">The entity link type</typeparam>
        /// <param name="expression">An expression to the entity link of type <typeparamref name="U"/></param>
        /// <param name="arg">The row representing the entity of type <typeparamref name="T"/></param>
        /// <returns>The ID on the entity link, or <c>null</c> if the entity link is invalid</returns>
        public static Guid? EntityLinkID<T, U>(Expression<Func<T, U>> expression, CoreRow arg) where U : IEntityLink
        {
            var col = CoreUtils.GetFullPropertyName(expression, ".");
            var id = arg.Get<Guid>(col + ".ID");
            if (id != Guid.Empty && arg.Get<Guid>(col + ".Deleted") == Guid.Empty)
                return id;
            return null;
        }

        protected override void SetChanged(string name, object? before, object? after)
        {
            base.SetChanged(name, before, after);

            CheckTax(name, before, after);
        }
        
        private void CheckTax(string name, object? before, object? after)
        {
            if (this is ITaxable taxable && !bTaxing)
            {
                
                bTaxing = true;
                try
                {
                    
                    if (name.Equals("ExTax"))
                    {
                        taxable.Tax = (double)after * (taxable.TaxRate / 100.0F);
                        taxable.IncTax = (double)after + taxable.Tax;
                    }

                    else if (name.Equals("TaxRate"))
                    {
                        taxable.Tax = taxable.ExTax * ((double)after / 100.0F);
                        taxable.IncTax = taxable.ExTax + taxable.Tax;
                    }

                    else if (name.Equals("Tax"))
                    {
                        taxable.ExTax = taxable.IncTax - (double)after;
                    }

                    else if (name.Equals("IncTax"))
                    {
                        taxable.ExTax = (double)after / ((100.0F + taxable.TaxRate) / 100.0F);
                        taxable.Tax = (double)after - taxable.ExTax;
                    }
                
                }
                catch (Exception e)
                {
                    Logger.Send(LogType.Error, "", String.Join("\n",e.Message,e.StackTrace));
                }
                
                bTaxing = false;
            }
        }


        protected override void DoPropertyChanged(string name, object? before, object? after)
        {
            if (!IsObserving())
                return;
            //CheckSequence();
            if (!name.Equals("LastUpdate"))
                LastUpdate = DateTime.Now;

            LastUpdateBy = ClientFactory.UserID;
            
            // This doesn;t work - keeps being updated to current date
            // Created => null ::Set ID = guid.empty -> now :: any other change -> unchanged!
            // Moved to Create(), should not simply be overwritten on deserialise from json

            //if (Created.Equals(DateTime.MinValue))
            //{
            //    Created = DateTime.Now;
            //    CreatedBy = ClientFactory.UserID;
            //}
        }
        
    }

    public interface ILicense<TLicenseToken> where TLicenseToken : LicenseToken
    {
    }

    public interface IPersistent
    {
    }

    public interface IRemotable
    {
    }

    //public interface IRemoteQuery
    //{

    //}

    //public interface IRemoteUpdate
    //{

    //}

    //public interface IRemoteDelete
    //{

    //}

    public interface ISequenceable
    {
        long Sequence { get; set; }
    }

    public interface IAutoIncrement<T, TType>
    {
        Expression<Func<T, TType>> AutoIncrementField();
        Filter<T>? AutoIncrementFilter();
    }

    public interface INumericAutoIncrement<T> : IAutoIncrement<T, int>
    {
    }

    public interface IStringAutoIncrement
    {
        string AutoIncrementPrefix();
        string AutoIncrementFormat();        
    }
    
    public interface IStringAutoIncrement<T> : IAutoIncrement<T, string>, IStringAutoIncrement
    {
    }

    /// <summary>
    /// Used to flag an entity as exhibiting the properties of a ManyToMany relationship, allowing PRS to auto-generate things like grids and datamodels based on
    /// entity relationships.
    /// </summary>
    /// <remarks>
    /// This will cause a ManyToMany grid of <typeparamref name="TRight"/> to appear on all <typeparamref name="TLeft"/> editors.
    /// Hence, if one wishes to cause both grids to appear (that is, for <typeparamref name="TLeft"/> to appear for <typeparamref name="TRight"/> <i>and</i>
    /// vice versa, one must flag the entity with both <c>IManyToMany&lt;<typeparamref name="TLeft"/>, <typeparamref name="TRight"/>&gt;</c> and 
    /// <c>IManyToMany&lt;<typeparamref name="TRight"/>, <typeparamref name="TLeft"/>&gt;</c>.
    /// </remarks>
    /// <typeparam name="TLeft"></typeparam>
    /// <typeparam name="TRight"></typeparam>
    public interface IManyToMany<TLeft, TRight> where TLeft : Entity where TRight : Entity
    {
    }

    public interface IOneToMany<TOne> where TOne : Entity
    {
    }

    public static class EntityExtensions
    {
        /// <summary>
        /// Sets the ID of an <see cref="Entity"/> while it is not observing, so that the change isn't registered.
        /// </summary>
        public static T SetID<T>(this T entity, Guid id)
            where T : Entity
        {
            entity.SetObserving(false);
            entity.ID = id;
            entity.SetObserving(true);
            return entity;
        }
    }

    public static class EntityFactory
    {
        public delegate object ObjectActivator(params object[] args);

        private static readonly Dictionary<Type, ObjectActivator> _cache = new Dictionary<Type, ObjectActivator>();

        public static ObjectActivator GetActivator<T>(ConstructorInfo ctor)
        {
            var type = ctor.DeclaringType;
            var paramsInfo = ctor.GetParameters();

            //create a single param of type object[]
            var param =
                Expression.Parameter(typeof(object[]), "args");

            var argsExp =
                new Expression[paramsInfo.Length];

            //pick each arg from the params array 
            //and create a typed expression of them
            for (var i = 0; i < paramsInfo.Length; i++)
            {
                Expression index = Expression.Constant(i);
                var paramType = paramsInfo[i].ParameterType;

                Expression paramAccessorExp =
                    Expression.ArrayIndex(param, index);

                Expression paramCastExp =
                    Expression.Convert(paramAccessorExp, paramType);

                argsExp[i] = paramCastExp;
            }

            //make a NewExpression that calls the
            //ctor with the args we just created
            var newExp = Expression.New(ctor, argsExp);

            //create a lambda with the New
            //Expression as body and our param object[] as arg
            var lambda =
                Expression.Lambda(typeof(ObjectActivator), newExp, param);

            //compile it
            var compiled = (ObjectActivator)lambda.Compile();
            return compiled;
        }

        public static T CreateEntity<T>() where T : BaseObject
        {
            if (!_cache.ContainsKey(typeof(T)))
            {
                var ctor = typeof(T).GetConstructors().First();
                _cache[typeof(T)] = GetActivator<T>(ctor);
            }

            var createdActivator = _cache[typeof(T)];
            return (T)createdActivator();
        }

        public static object CreateEntity(Type type)
        {
            if (!_cache.ContainsKey(type))
            {
                var ctor = type.GetConstructors().First();
                var activator = typeof(EntityFactory).GetMethod("GetActivator").MakeGenericMethod(type);
                _cache[type] = (ObjectActivator)activator.Invoke(null, new object[] { ctor });
            }

            var createdActivator = _cache[type];
            return createdActivator();
        }
    }
}