/*  Copyright (C) 2008 - 2011 Jordan Marr

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library. If not, see <http://www.gnu.org/licenses/>. */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Data.Common;
using System.Linq;
using Marr.Data.Mapping;
using System.Reflection;

namespace Marr.Data
{
    /// <summary>
    /// Holds metadata about an object graph that is being queried and eagerly loaded.
    /// Contains all metadata needed to instantiate the object and fill it with data from a DataReader.
    /// Does not iterate through lazy loaded child relationships.
    /// </summary>
    internal class EntityGraph : IEnumerable<EntityGraph>
    {
        private MapRepository _repos;
        private EntityGraph _parent;
        private Type _entityType;
        private Relationship _relationship;
        private ColumnMapCollection _columns;
        private RelationshipCollection _relationships;
        private List<EntityGraph> _children;
        private object _entity;
        private GroupingKeyCollection _groupingKeyColumns;
        private Dictionary<string, EntityReference> _entityReferences;

        internal IList RootList { get; private set; }
        internal bool IsParentReference { get; private set; }

        /// <summary>
        /// Recursively builds an entity graph of the given parent type.
        /// </summary>
        /// <param name="entityType"></param>
        public EntityGraph(Type entityType, IList rootList)
            : this(entityType, null, null) // Recursively constructs hierarchy
        {
            RootList = rootList;
        }

        /// <summary>
        /// Recursively builds entity graph hierarchy.
        /// </summary>
        /// <param name="entityType"></param>
        /// <param name="parent"></param>
        /// <param name="relationship"></param>
        private EntityGraph(Type entityType, EntityGraph parent, Relationship relationship)
        {
            _repos = MapRepository.Instance;

            _entityType = entityType;
            _parent = parent;
            _relationship = relationship;
            IsParentReference = !IsRoot && AnyParentsAreOfType(entityType);
            if (!IsParentReference)
            {
                _columns = _repos.GetColumns(entityType);
            }
            
            _relationships = _repos.GetRelationships(entityType);
            _children = new List<EntityGraph>();
            Member = relationship != null ? relationship.Member : null;
            _entityReferences = new Dictionary<string, EntityReference>();

            if (IsParentReference)
            {
                return;
            }

            // Create a new EntityGraph for each child relationship that is not lazy loaded
            foreach (Relationship childRelationship in this.Relationships)
            {
                if (!childRelationship.IsLazyLoaded)
                {
                    _children.Add(new EntityGraph(childRelationship.RelationshipInfo.EntityType, this, childRelationship));
                }
            }
        }

        public MemberInfo Member { get; private set; }

        /// <summary>
        /// Gets the parent of this EntityGraph.
        /// </summary>
        public EntityGraph Parent
        {
            get
            {
                return _parent;
            }
        }

        /// <summary>
        /// Gets the Type of this EntityGraph.
        /// </summary>
        public Type EntityType
        {
            get { return _entityType; }
        }

        /// <summary>
        /// Gets a boolean than indicates whether this entity is the root node in the graph.
        /// </summary>
        public bool IsRoot
        {
            get
            {
                return _parent == null;
            }
        }

        /// <summary>
        /// Gets a boolean that indicates whether this entity is a child.
        /// </summary>
        public bool IsChild
        {
            get
            {
                return _parent != null;
            }
        }

        /// <summary>
        /// Gets the columns mapped to this entity.
        /// </summary>
        public ColumnMapCollection Columns
        {
            get { return _columns; }
        }

        /// <summary>
        /// Gets the relationships mapped to this entity.
        /// </summary>
        public RelationshipCollection Relationships
        {
            get { return _relationships; }
        }

        /// <summary>
        /// A list of EntityGraph objects that hold metadata about the child entities that will be loaded.
        /// </summary>
        public List<EntityGraph> Children
        {
            get { return _children; }
        }
        
        /// <summary>
        /// Adds an entity to the appropriate place in the object graph.
        /// </summary>
        /// <param name="entityInstance"></param>
        public void AddEntity(object entityInstance)
        {
            _entity = entityInstance;

            // Add newly created entityInstance to list (Many) or set it to field (One)
            if (this.IsRoot)
            {
                RootList.Add(entityInstance);
            }
            else if (_relationship.RelationshipInfo.RelationType == RelationshipTypes.Many)
            {
                var list = _parent._entityReferences[_parent.GroupingKeyColumns.GroupingKey]
                    .ChildLists[_relationship.Member.Name];

                list.Add(entityInstance);
            }
            else // RelationTypes.One
            {
                _repos.ReflectionStrategy.SetFieldValue(_parent._entity, _relationship.Member.Name, entityInstance);
            }

            EntityReference entityRef = new EntityReference(entityInstance);
            _entityReferences.Add(GroupingKeyColumns.GroupingKey, entityRef);

            InitOneToManyChildLists(entityRef);           
        }

        /// <summary>
        /// Searches for a previously loaded parent entity and then sets that reference to the mapped Relationship property.
        /// </summary>
        public void AddParentReference()
        {
            var parentReference = FindParentReference();
            _repos.ReflectionStrategy.SetFieldValue(_parent._entity, _relationship.Member.Name, parentReference);
        }
        
        /// <summary>
        /// Concatenates the values of the GroupingKeys property and compares them
        /// against the LastKeyGroup property.  Returns true if the values are different,
        /// or false if the values are the same.
        /// The currently concatenated keys are saved in the LastKeyGroup property.
        /// </summary>
        /// <param name="reader"></param>
        /// <returns></returns>
        public bool IsNewGroup(DbDataReader reader)
        {
            bool isNewGroup = false;

            // Get primary keys from parent entity and any one-to-one child entites
            GroupingKeyCollection groupingKeyColumns = this.GroupingKeyColumns;

            // Concatenate column values
            KeyGroupInfo keyGroupInfo = groupingKeyColumns.CreateGroupingKey(reader);

            if (!keyGroupInfo.HasNullKey && !_entityReferences.ContainsKey(keyGroupInfo.GroupingKey))
            {
                isNewGroup = true;
            }

            return isNewGroup;
        }

        /// <summary>
        /// Gets the GroupingKeys for this entity.  
        /// GroupingKeys determine when to create and add a new entity to the graph.
        /// </summary>
        /// <remarks>
        /// A simple entity with no relationships will return only its PrimaryKey columns.
        /// A parent entity with one-to-one child relationships will include its own PrimaryKeys,
        /// and it will recursively traverse all Children with one-to-one relationships and add their PrimaryKeys.
        /// A child entity that has a one-to-one relationship with its parent will use the same 
        /// GroupingKeys already defined by its parent.
        /// </remarks>
        public GroupingKeyCollection GroupingKeyColumns
        {
            get
            {
                if (_groupingKeyColumns == null)
                    _groupingKeyColumns = GetGroupingKeyColumns();

                return _groupingKeyColumns;
            }
        }

        private bool AnyParentsAreOfType(Type type)
        {
            EntityGraph parent = _parent;
            while (parent != null)
            {
                if (parent._entityType == type)
                {
                    return true;
                }
                parent = parent._parent;
            }

            return false;
        }

        private object FindParentReference()
        {
            var parent = this.Parent.Parent;
            while (parent != null)
            {
                if (parent._entityType == _relationship.MemberType)
                {
                    return parent._entity;
                }

                parent = parent.Parent;
            }

            return null;
        }

        /// <summary>
        /// Initializes the owning lists on many-to-many Children.
        /// </summary>
        /// <param name="entityInstance"></param>
        private void InitOneToManyChildLists(EntityReference entityRef)
        {
            // Get a reference to the parent's the childrens' OwningLists to the parent entity
            for (int i = 0; i < Relationships.Count; i++)
            {
                Relationship relationship = Relationships[i];
                if (relationship.RelationshipInfo.RelationType == RelationshipTypes.Many)
                {
                    try
                    {
                        IList list = (IList)_repos.ReflectionStrategy.CreateInstance(relationship.MemberType);
                        _repos.ReflectionStrategy.SetFieldValue(entityRef.Entity, relationship.Member.Name, list);
                        
                        // Save a reference to each 1-M list
                        entityRef.AddChildList(relationship.Member.Name, list);
                    }
                    catch (Exception ex)
                    {
                        throw new DataMappingException(
                            string.Format("{0}.{1} is a \"Many\" relationship type so it must derive from IList.",
                                entityRef.Entity.GetType().Name, relationship.Member.Name),
                            ex);
                    }
                }
            }
        }

        /// <summary>
        /// Gets a list of keys to group by.
        /// </summary>
        /// <remarks>
        /// When converting an unnormalized set of data from a database view,
        /// a new entity is only created when the grouping keys have changed.
        /// NOTE: This behavior works on the assumption that the view result set
        /// has been sorted by the root entity primary key(s), followed by the
        /// child entity primary keys.
        /// </remarks>
        /// <returns></returns>
        private GroupingKeyCollection GetGroupingKeyColumns()
        {
            // Get primary keys for this parent entity
            GroupingKeyCollection groupingKeyColumns = new GroupingKeyCollection();
            groupingKeyColumns.PrimaryKeys.AddRange(Columns.PrimaryKeys);

            // The following conditions should fail with an exception:
            // 1) Any parent entity (entity with children) must have at least one PK specified or an exception will be thrown
            // 2) All 1-M relationship entities must have at least one PK specified
            // * Only 1-1 entities with no children are allowed to have 0 PKs specified.
            if ((groupingKeyColumns.PrimaryKeys.Count == 0 && _children.Count > 0) ||
                (groupingKeyColumns.PrimaryKeys.Count == 0 && !IsRoot && _relationship.RelationshipInfo.RelationType == RelationshipTypes.Many))
                throw new MissingPrimaryKeyException(string.Format("There are no primary key mappings defined for the following entity: '{0}'.", this.EntityType.Name));

            // Add parent's keys
            if (IsChild)
                groupingKeyColumns.ParentPrimaryKeys.AddRange(Parent.GroupingKeyColumns);

            return groupingKeyColumns;
        }

        #region IEnumerable<EntityGraph> Members

        public IEnumerator<EntityGraph> GetEnumerator()
        {
            return TraverseGraph(this);
        }

        /// <summary>
        /// Recursively traverses through every entity in the EntityGraph.
        /// </summary>
        /// <param name="entityGraph"></param>
        /// <returns></returns>
        private static IEnumerator<EntityGraph> TraverseGraph(EntityGraph entityGraph)
        {
            Stack<EntityGraph> stack = new Stack<EntityGraph>();
            stack.Push(entityGraph);

            while (stack.Count > 0)
            {
                EntityGraph node = stack.Pop();
                yield return node;

                foreach (EntityGraph childGraph in node.Children)
                {
                    stack.Push(childGraph);
                }
            }
        }


        #endregion

        #region IEnumerable Members

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        #endregion
    }
}

public struct KeyGroupInfo
{
    private string _groupingKey;
    private bool _hasNullKey;

    public KeyGroupInfo(string groupingKey, bool hasNullKey)
    {
        _groupingKey = groupingKey;
        _hasNullKey = hasNullKey;
    }

    public string GroupingKey 
    { 
        get { return _groupingKey; } 
    }

    public bool HasNullKey 
    {
        get { return _hasNullKey; } 
    }
}