using System;
using System.Collections;
using System.Linq;
using System.Linq.Expressions;
using Marr.Data.Mapping.Strategies;

namespace Marr.Data.Mapping
{
    /// <summary>
    /// This class has fluent methods that are used to easily configure relationship mappings.
    /// </summary>
    /// <typeparam name="TEntity"></typeparam>
    public class RelationshipBuilder<TEntity>
    {
        private FluentMappings.MappingsFluentEntity<TEntity> _fluentEntity;
        private string _currentPropertyName;

        public RelationshipBuilder(FluentMappings.MappingsFluentEntity<TEntity> fluentEntity, RelationshipCollection relationships)
        {
            _fluentEntity = fluentEntity;
            Relationships = relationships;
        }

        /// <summary>
        /// Gets the list of relationship mappings that are being configured.
        /// </summary>
        public RelationshipCollection Relationships { get; private set; }

        #region - Fluent Methods -

        /// <summary>
        /// Initializes the configurator to configure the given property.
        /// </summary>
        /// <param name="property"></param>
        /// <returns></returns>
        public RelationshipBuilder<TEntity> For(Expression<Func<TEntity, object>> property)
        {
            return For(property.GetMemberName());
        }

        /// <summary>
        /// Initializes the configurator to configure the given property or field.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        public RelationshipBuilder<TEntity> For(string propertyName)
        {
            _currentPropertyName = propertyName;

            // Try to add the relationship if it doesn't exist
            if (Relationships[_currentPropertyName] == null)
            {
                TryAddRelationshipForField(_currentPropertyName);
            }

            return this;
        }

        /// <summary>
        /// Sets a property to be lazy loaded, with a given query.
        /// </summary>
        /// <typeparam name="TChild"></typeparam>
        /// <param name="query"></param>
        /// <param name="condition">condition in which a child could exist. eg. avoid call to db if foreign key is 0 or null</param>
        /// <returns></returns>
        public RelationshipBuilder<TEntity> LazyLoad<TChild>(Func<IDataMapper, TEntity, TChild> query, Func<TEntity, bool> condition = null)
        {
            AssertCurrentPropertyIsSet();
            var relationship = Relationships[_currentPropertyName];

            relationship.LazyLoaded = new LazyLoaded<TEntity, TChild>(query, condition);

            // work out if it's one to many or not
            if (typeof(ICollection).IsAssignableFrom(typeof(TChild)))
            {
                relationship.RelationshipInfo.RelationType = RelationshipTypes.Many;
                relationship.RelationshipInfo.EntityType = typeof(TChild).GetGenericArguments()[0];
            }
            else
            {
                relationship.RelationshipInfo.EntityType = typeof(TChild);
            }
            
            return this;
        }

        public RelationshipBuilder<TEntity> SetOneToOne()
        {
            AssertCurrentPropertyIsSet();
            SetOneToOne(_currentPropertyName);
            return this;
        }

        public RelationshipBuilder<TEntity> SetOneToOne(string propertyName)
        {
            Relationships[propertyName].RelationshipInfo.RelationType = RelationshipTypes.One;
            return this;
        }

        public RelationshipBuilder<TEntity> SetOneToMany()
        {
            AssertCurrentPropertyIsSet();
            SetOneToMany(_currentPropertyName);
            return this;
        }

        public RelationshipBuilder<TEntity> SetOneToMany(string propertyName)
        {
            Relationships[propertyName].RelationshipInfo.RelationType = RelationshipTypes.Many;
            return this;
        }

        public RelationshipBuilder<TEntity> Ignore(Expression<Func<TEntity, object>> property)
        {
            string propertyName = property.GetMemberName();
            Relationships.RemoveAll(r => r.Member.Name == propertyName);
            return this;
        }

        public FluentMappings.MappingsFluentTables<TEntity> Tables
        {
            get
            {
                if (_fluentEntity == null)
                {
                    throw new Exception("This property is not compatible with the obsolete 'MapBuilder' class.");
                }

                return _fluentEntity.Table;
            }
        }

        public FluentMappings.MappingsFluentColumns<TEntity> Columns
        {
            get
            {
                if (_fluentEntity == null)
                {
                    throw new Exception("This property is not compatible with the obsolete 'MapBuilder' class.");
                }

                return _fluentEntity.Columns;
            }
        }

        public FluentMappings.MappingsFluentEntity<TNewEntity> Entity<TNewEntity>()
        {
            return new FluentMappings.MappingsFluentEntity<TNewEntity>(true);
        }

        /// <summary>
        /// Tries to add a Relationship for the given field name.  
        /// Throws and exception if field cannot be found.
        /// </summary>
        private void TryAddRelationshipForField(string fieldName)
        {
            // Set strategy to filter for public or private fields
            ConventionMapStrategy strategy = new ConventionMapStrategy(false);

            // Find the field that matches the given field name
            strategy.RelationshipPredicate = mi => mi.Name == fieldName;
            Relationship relationship = strategy.MapRelationships(typeof(TEntity)).FirstOrDefault();

            if (relationship == null)
            {
                throw new DataMappingException(string.Format("Could not find the field '{0}' in '{1}'.",
                    fieldName,
                    typeof(TEntity).Name));
            }
            Relationships.Add(relationship);
        }

        /// <summary>
        /// Throws an exception if the "current" property has not been set.
        /// </summary>
        private void AssertCurrentPropertyIsSet()
        {
            if (string.IsNullOrEmpty(_currentPropertyName))
            {
                throw new DataMappingException("A property must first be specified using the 'For' method.");
            }
        }

        #endregion
    }
}
