// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

namespace System.Data.Entity.Core.Objects
{
    using System.Collections;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data.Common;
    using System.Data.Entity.Core.Metadata.Edm;
    using System.Data.Entity.Core.Objects.DataClasses;
    using System.Data.Entity.Utilities;
    using System.Diagnostics;
    using System.Globalization;
    using System.Reflection;

    // <summary>
    // Creates instances of ObjectView that provide a binding list for ObjectQuery results and EntityCollections.
    // </summary>
    // <remarks>
    // The factory methods construct an ObjectView whose generic type parameter (and typed of elements in the binding list)
    // is of the same type or a more specific derived type of the generic type of the ObjectQuery or EntityCollection.
    // The EDM type of the query results or EntityType or the EntityCollection is examined to determine
    // the appropriate type to be used.
    // For example, if you have an ObjectQuery whose generic type is "object", but the EDM result type of the Query maps
    // to the CLR type "Customer", then the ObjectView returned will specify a generic type of "Customer", and not "object".
    // </remarks>
    internal static class ObjectViewFactory
    {
        // References to commonly-used generic type definitions.
        private static readonly Type _genericObjectViewType = typeof(ObjectView<>);

        private static readonly Type _genericObjectViewDataInterfaceType = typeof(IObjectViewData<>);
        private static readonly Type _genericObjectViewQueryResultDataType = typeof(ObjectViewQueryResultData<>);
        private static readonly Type _genericObjectViewEntityCollectionDataType = typeof(ObjectViewEntityCollectionData<,>);

        // <summary>
        // Return a list suitable for data binding using the supplied query results.
        // </summary>
        // <typeparam name="TElement"> CLR type of query result elements declared by the caller. </typeparam>
        // <param name="elementEdmTypeUsage"> The EDM type of the query results, used as the primary means of determining the CLR type of list returned by this method. </param>
        // <param name="queryResults"> IEnumerable used to enumerate query results used to populate binding list. Must not be null. </param>
        // <param name="objectContext">
        // <see cref="ObjectContext" /> associated with the query from which results were obtained. Must not be null.
        // </param>
        // <param name="forceReadOnly">
        // <b>True</b> to prevent modifications to the binding list built from the query result; otherwise <b>false</b> . Note that other conditions may prevent the binding list from being modified, so a value of <b>false</b> supplied for this parameter doesn't necessarily mean that the list will be writable.
        // </param>
        // <param name="singleEntitySet">
        // If the query results are composed of entities that only exist in a single
        // <see
        //     cref="EntitySet" />
        // , the value of this parameter is the single EntitySet. Otherwise the value of this parameter should be null.
        // </param>
        // <returns>
        // <see cref="IBindingList" /> that is suitable for data binding.
        // </returns>
        internal static IBindingList CreateViewForQuery<TElement>(
            TypeUsage elementEdmTypeUsage, IEnumerable<TElement> queryResults, ObjectContext objectContext, bool forceReadOnly,
            EntitySet singleEntitySet)
        {
            DebugCheck.NotNull(queryResults);
            DebugCheck.NotNull(objectContext);

            Type clrElementType = null;
            var ospaceElementTypeUsage = GetOSpaceTypeUsage(elementEdmTypeUsage, objectContext);

            // Map the O-Space EDM type to a CLR type.
            // If the mapping is unsuccessful, fallback to TElement type.
            if (ospaceElementTypeUsage == null)
            {
                clrElementType = typeof(TElement);
            }
            {
                clrElementType = GetClrType<TElement>(ospaceElementTypeUsage.EdmType);
            }

            IBindingList objectView;
            object eventDataSource = objectContext.ObjectStateManager;

            // If the clrElementType matches the declared TElement type, optimize the construction of the ObjectView
            // by avoiding a reflection-based instantiation.
            if (clrElementType == typeof(TElement))
            {
                var viewData = new ObjectViewQueryResultData<TElement>(queryResults, objectContext, forceReadOnly, singleEntitySet);

                objectView = new ObjectView<TElement>(viewData, eventDataSource);
            }
            else if (clrElementType == null)
            {
                var viewData = new ObjectViewQueryResultData<DbDataRecord>(queryResults, objectContext, true, null);
                objectView = new DataRecordObjectView(viewData, eventDataSource, (RowType)ospaceElementTypeUsage.EdmType, typeof(TElement));
            }
            else
            {
                if (!typeof(TElement).IsAssignableFrom(clrElementType))
                {
                    throw EntityUtil.ValueInvalidCast(clrElementType, typeof(TElement));
                }

                // Use reflection to create an instance of the generic ObjectView and ObjectViewQueryResultData classes, 
                // using clrElementType as the value of TElement generic type parameter for both classes.

                var objectViewDataType = _genericObjectViewQueryResultDataType.MakeGenericType(clrElementType);

                var viewDataConstructor = objectViewDataType.GetDeclaredConstructor(
                    typeof(IEnumerable), typeof(ObjectContext), typeof(bool), typeof(EntitySet));

                Debug.Assert(
                    viewDataConstructor != null,
                    "ObjectViewQueryResultData constructor not found. Please ensure constructor signature is correct.");

                // Create ObjectViewQueryResultData instance
                var viewData = viewDataConstructor.Invoke(new object[] { queryResults, objectContext, forceReadOnly, singleEntitySet });

                // Create ObjectView instance
                objectView = CreateObjectView(clrElementType, objectViewDataType, viewData, eventDataSource);
            }

            return objectView;
        }

        // <summary>
        // Return a list suitable for data binding using the supplied EntityCollection
        // </summary>
        // <typeparam name="TElement"> CLR type of the elements of the EntityCollection. </typeparam>
        // <param name="entityType"> The EntityType of the elements in the collection. This should either be the same as the EntityType that corresponds to the CLR TElement type, or a EntityType derived from the declared EntityCollection element type. </param>
        // <param name="entityCollection"> The EntityCollection from which a binding list is created. </param>
        // <returns>
        // <see cref="IBindingList" /> that is suitable for data binding.
        // </returns>
        internal static IBindingList CreateViewForEntityCollection<TElement>(
            EntityType entityType, EntityCollection<TElement> entityCollection)
            where TElement : class
        {
            Type clrElementType = null;
            var entityTypeUsage = entityType == null ? null : TypeUsage.Create(entityType);
            var ospaceElementTypeUsage = GetOSpaceTypeUsage(entityTypeUsage, entityCollection.ObjectContext);

            // Map the O-Space EDM type to a CLR type.
            // If the mapping is unsuccessful, fallback to TElement type.
            if (ospaceElementTypeUsage == null)
            {
                clrElementType = typeof(TElement);
            }
            else
            {
                clrElementType = GetClrType<TElement>(ospaceElementTypeUsage.EdmType);

                // A null clrElementType is returned by GetClrType if the EDM type is a RowType with no specific CLR type mapping.
                // This should not happen when working with EntityCollections, but if it does, fallback to TEntityRef type.
                Debug.Assert(clrElementType != null, "clrElementType has unexpected value of null.");

                if (clrElementType == null)
                {
                    clrElementType = typeof(TElement);
                }
            }

            IBindingList objectView;

            // If the clrElementType matches the declared TElement type, optimize the construction of the ObjectView
            // by avoiding a reflection-based instantiation.
            if (clrElementType == typeof(TElement))
            {
                var viewData = new ObjectViewEntityCollectionData<TElement, TElement>(entityCollection);
                objectView = new ObjectView<TElement>(viewData, entityCollection);
            }
            else
            {
                if (!typeof(TElement).IsAssignableFrom(clrElementType))
                {
                    throw EntityUtil.ValueInvalidCast(clrElementType, typeof(TElement));
                }

                // Use reflection to create an instance of the generic ObjectView and ObjectViewEntityCollectionData classes, 
                // using clrElementType as the value of TElement generic type parameter for both classes.

                var objectViewDataType = _genericObjectViewEntityCollectionDataType.MakeGenericType(clrElementType, typeof(TElement));

                var viewDataConstructor = objectViewDataType.GetDeclaredConstructor(typeof(EntityCollection<TElement>));

                Debug.Assert(
                    viewDataConstructor != null,
                    "ObjectViewEntityCollectionData constructor not found. Please ensure constructor signature is correct.");

                // Create ObjectViewEntityCollectionData instance
                var viewData = viewDataConstructor.Invoke(new object[] { entityCollection });

                // Create ObjectView instance
                objectView = CreateObjectView(clrElementType, objectViewDataType, viewData, entityCollection);
            }

            return objectView;
        }

        // <summary>
        // Create an ObjectView using reflection.
        // </summary>
        // <param name="clrElementType"> Type to be used for the ObjectView's generic type parameter. </param>
        // <param name="objectViewDataType"> The type of class that implements the IObjectViewData to be used by the ObjectView. </param>
        // <param name="viewData"> The IObjectViewData to be used by the ObjectView to access the binding list. </param>
        // <param name="eventDataSource"> Event source used by ObjectView for entity and membership changes. </param>
        private static IBindingList CreateObjectView(Type clrElementType, Type objectViewDataType, object viewData, object eventDataSource)
        {
            var objectViewType = _genericObjectViewType.MakeGenericType(clrElementType);

            var viewDataInterfaces =
                objectViewDataType.FindInterfaces(
                    (Type type, object unusedFilter) => type.Name == _genericObjectViewDataInterfaceType.Name, null);
            Debug.Assert(
                viewDataInterfaces.Length == 1, "Could not find IObjectViewData<T> interface definition for ObjectViewQueryResultData<T>.");

            var viewConstructor = objectViewType.GetDeclaredConstructor(viewDataInterfaces[0], typeof(object));

            Debug.Assert(viewConstructor != null, "ObjectView constructor not found. Please ensure constructor signature is correct.");

            // Create ObjectView instance
            return (IBindingList)viewConstructor.Invoke(new[] { viewData, eventDataSource });
        }

        // <summary>
        // Map the supplied TypeUsage to O-Space.
        // </summary>
        // <param name="typeUsage"> The TypeUsage to be mapped to O-Space. Should either be associated with C-Space or O-Space. </param>
        // <param name="objectContext"> ObjectContext used to perform type mapping. </param>
        private static TypeUsage GetOSpaceTypeUsage(TypeUsage typeUsage, ObjectContext objectContext)
        {
            TypeUsage ospaceTypeUsage;

            if (typeUsage == null
                || typeUsage.EdmType == null)
            {
                ospaceTypeUsage = null;
            }
            else
            {
                if (typeUsage.EdmType.DataSpace
                    == DataSpace.OSpace)
                {
                    ospaceTypeUsage = typeUsage;
                }
                else
                {
                    Debug.Assert(
                        typeUsage.EdmType.DataSpace == DataSpace.CSpace,
                        String.Format(
                            CultureInfo.InvariantCulture, "Expected EdmType.DataSpace to be C-Space, but instead it is {0}.",
                            typeUsage.EdmType.DataSpace.ToString()));

                    // The ObjectContext is needed to map the EDM TypeUsage from C-Space to O-Space.
                    if (objectContext == null)
                    {
                        ospaceTypeUsage = null;
                    }
                    else
                    {
                        ospaceTypeUsage = objectContext.Perspective.MetadataWorkspace.GetOSpaceTypeUsage(typeUsage);
                    }
                }
            }

            return ospaceTypeUsage;
        }

        // <summary>
        // Determine CLR Type to be exposed for data binding using the supplied EDM item type.
        // </summary>
        // <typeparam name="TElement"> CLR element type declared by the caller. There is no requirement that this method return the same type, or a type compatible with the declared type; it is merely a suggestion as to which type might be used. </typeparam>
        // <param name="ospaceEdmType"> The EDM O-Space type of the items in a particular query result. </param>
        // <returns>
        // <see cref="Type" /> instance that represents the CLR type that corresponds to the supplied EDM item type; or null if the EDM type does not map to a CLR type. Null is returned in the case where
        // <paramref
        //     name="ospaceEdmType" />
        // is a <see cref="RowType" /> , and no CLR type mapping is specified in the RowType metadata.
        // </returns>
        private static Type GetClrType<TElement>(EdmType ospaceEdmType)
        {
            Type clrType;

            // EDM RowTypes are generally represented by CLR MaterializedDataRecord types
            // that need special handling to properly expose the properties available for binding (using ICustomTypeDescriptor and ITypedList implementations, for example).
            //
            // However, if the RowType has InitializerMetadata with a non-null CLR Type, 
            // that CLR type should be used to determine the properties available for binding.
            if (ospaceEdmType.BuiltInTypeKind
                == BuiltInTypeKind.RowType)
            {
                var itemRowType = (RowType)ospaceEdmType;

                if (itemRowType.InitializerMetadata != null
                    && itemRowType.InitializerMetadata.ClrType != null)
                {
                    clrType = itemRowType.InitializerMetadata.ClrType;
                }
                else
                {
                    // If the generic parameter TElement is not exactly a data record type or object type,
                    // use it as the CLR type.
                    var elementType = typeof(TElement);

                    if (typeof(IDataRecord).IsAssignableFrom(elementType)
                        || elementType == typeof(object))
                    {
                        // No CLR type mapping exists for this RowType.
                        clrType = null;
                    }
                    else
                    {
                        clrType = typeof(TElement);
                    }
                }
            }
            else
            {
                clrType = ospaceEdmType.ClrType;

                // If the CLR type cannot be determined from the EDM type,
                // fallback to the element type declared by the caller.
                if (clrType == null)
                {
                    clrType = typeof(TElement);
                }
            }

            return clrType;
        }
    }
}
