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

namespace System.Data.Entity
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Data.Entity.Core.Objects.DataClasses;
    using System.Data.Entity.Core.Metadata.Edm;
    using System.Data.Entity.Utilities;
    using System.Diagnostics;
    using System.Reflection;
    using System.Reflection.Emit;
    using System.Xml.Linq;

    internal class CsdlToClrAssemblyConverter
    {
        private static readonly XNamespace _csdlNs = "http://schemas.microsoft.com/ado/2009/11/edm";
        private static readonly XNamespace _csdlMappingTestExtNs = "MappingTestExtension";

        private static readonly Dictionary<string, Type> _primitiveTypes = new Dictionary<string, Type>()
        {
            { "Binary", typeof(Byte[]) },
            { "Boolean", typeof(bool) },
            { "Byte", typeof(Byte) },
            { "DateTime", typeof(DateTime) },
            { "DateTimeOffset", typeof(DateTimeOffset) },
            { "Decimal", typeof(Decimal) },
            { "Double", typeof(Double) },
            { "Guid", typeof(Guid) },
            { "Int16", typeof(Int16) },
            { "Int32", typeof(Int32) },
            { "Int64", typeof(Int64) },
            { "SByte", typeof(SByte) },
            { "Single", typeof(Single) },
            { "String", typeof(String) },
            { "Time", typeof(TimeSpan) },

            // Not valid EDM primitive types but having them is useful for negative OC Mapping tests
            { "UInt32", typeof(UInt32) },
        };

        private static readonly IList<Assembly> _createdAssemblies = new List<Assembly>(); 

        static CsdlToClrAssemblyConverter()
        {
            // This enables resolving types used in the EdmRelationshipAttribute. 
            // Dynamic, in-memory assemblies are not considered for assembly load.
            AppDomain.CurrentDomain.AssemblyResolve += (o, e) =>
            {
                return e.Name == e.RequestingAssembly.FullName ? 
                    e.RequestingAssembly : 
                    _createdAssemblies.SingleOrDefault(a => a.FullName == e.Name);
            };                
        }

        private readonly XDocument[] _csdlArtifacts;
        private readonly bool _isPOCO;
        private DynamicAssembly _assembly;

        public CsdlToClrAssemblyConverter(bool isPOCO, params XDocument[] csdlArtifacts)
        {
            _csdlArtifacts = csdlArtifacts;
            _isPOCO = isPOCO;
        }

        public Assembly BuildAssembly(string assemblyName)
        {
            _assembly = new DynamicAssembly();

            AddSchemaAttribute();

            foreach (var csdl in _csdlArtifacts)
            {
                BuildTypes(csdl);
            }

            var assemblyBuilder = _assembly.Compile(new AssemblyName(assemblyName));

            // add the assembly to the list for assembly resolution
            _createdAssemblies.Add(assemblyBuilder);

            // EdmRelationshipAttributes require real Clr types so it adds these
            // attributes using CustomAttributeBuilder on already created assembly 
            foreach (var csdl in _csdlArtifacts)
            {
                AddRelationshipAttributes(csdl, (AssemblyBuilder)assemblyBuilder);
            }

            return assemblyBuilder;
        }

        private void BuildTypes(XDocument csdl)
        {
            foreach (var enumTypeDefinition in csdl.Descendants(_csdlNs + "EnumType"))
            {
                BuildEnumType(enumTypeDefinition);
            }

            BuildStructuralTypes(
                csdl.Descendants(_csdlNs + "ComplexType")
                .Concat(csdl.Descendants(_csdlNs + "EntityType")));
        }

        private void BuildEnumType(XElement enumTypeDefinition)
        {
            var dynamicEnumType = _assembly.DynamicEnumType(GetFullTypeName(enumTypeDefinition));
            dynamicEnumType.HasUnderlyingType(
                _primitiveTypes[(string)enumTypeDefinition.Attribute("UnderlyingType") ?? "Int32"]);
            
            if (!((bool?)enumTypeDefinition.Attribute(_csdlMappingTestExtNs + "SuppressEdmTypeAttribute") ?? false))
            {
                AddEdmTypeAttribute<EdmEnumTypeAttribute>(enumTypeDefinition, dynamicEnumType);
            }
            
            long memberValue = -1;
            foreach (var enumMember in enumTypeDefinition.Elements(_csdlNs + "Member"))
            {
                var definedMemberValue = (int?)enumMember.Attribute("Value");
                memberValue = definedMemberValue.HasValue ? definedMemberValue.Value : memberValue + 1;
                dynamicEnumType.HasMember(
                    (string)enumMember.Attribute("Name"),
                    Convert.ChangeType(memberValue, dynamicEnumType.UnderlyingType));
            }
        }

        private void BuildStructuralTypes(IEnumerable<XElement> structuralTypeDefinitions)
        {       
            foreach (var structuralTypeDefinition in structuralTypeDefinitions)
            {
                BuildStructuralType(structuralTypeDefinition);
            }

            // properties are built after all types have been registered to
            // make navigation properties work
            foreach (var structuralTypeDefinition in structuralTypeDefinitions)
            {
                BuildNavigationProperties(structuralTypeDefinition);
            }
        }

        private void BuildStructuralType(XElement structuralTypeDefinition)
        {
            var fullTypeName = GetFullTypeName(structuralTypeDefinition);

            if (_assembly.DynamicTypes.All(t => t.Name != fullTypeName))
            {
                var newType = _assembly.DynamicStructuralType(fullTypeName);

                var baseTypeName = (string)structuralTypeDefinition.Attribute("BaseType");

                if (baseTypeName != null)
                {
                    BuildStructuralType(
                        structuralTypeDefinition
                            .Parent
                            .Elements(_csdlNs + "EntityType")
                            .Single(
                                e => (string)e.Attribute("Name") == baseTypeName.Split('.').Last()));
                    
                    newType.HasBaseClass(_assembly.DynamicTypes.Single(t => t.Name == baseTypeName));
                }

                if (structuralTypeDefinition.Name.LocalName == "EntityType") 
                {
                    AddEdmTypeAttribute<EdmEntityTypeAttribute>(structuralTypeDefinition, newType);
                }
                else
                {
                    AddEdmTypeAttribute<EdmComplexTypeAttribute>(structuralTypeDefinition, newType);
                }

                foreach (var propertyDefinition in structuralTypeDefinition.Elements(_csdlNs + "Property"))
                {
                    var propertyName = (string)propertyDefinition.Attribute("Name");
                    var propertyTypeName = (string)propertyDefinition.Attribute("Type");
                    var propertyIsNullable = ((bool?)propertyDefinition.Attribute("Nullable") ?? false);
                    var property = newType.Property(propertyName);
                    
                    if ((bool?)propertyDefinition.Attribute(_csdlMappingTestExtNs + "SuppressGetter") ?? false)
                    {
                        property.HasGetterAccess(MemberAccess.None);
                    }

                    if ((bool?)propertyDefinition.Attribute(_csdlMappingTestExtNs + "SuppressSetter") ?? false)
                    {
                        property.HasSetterAccess(MemberAccess.None);
                    }

                    Type primitiveType;
                    if(_primitiveTypes.TryGetValue(propertyTypeName, out primitiveType))
                    {
                        if (primitiveType.IsValueType() && propertyIsNullable)
                        {
                            primitiveType = typeof(Nullable<>).MakeGenericType(primitiveType);
                        }

                        property.HasType(primitiveType);
                        AddScalarPropertyAttribute(propertyDefinition, property);
                    }
                    else
                    {
                        var type = ResolveType(propertyTypeName);                      

                        var dynamicEnumType = type as DynamicEnumType;
                        if(dynamicEnumType != null)
                        {
                            property.HasEnumType(dynamicEnumType, propertyIsNullable);
                            AddScalarPropertyAttribute(propertyDefinition, property);
                        }
                        else
                        {
                            var dynamicStructuralType = type as DynamicStructuralType;
                            if (dynamicStructuralType != null)
                            {
                                property.HasReferenceType((DynamicStructuralType)dynamicStructuralType);
                                AddComplexPropertyAttribute(property);
                            }
                            else
                            {
                                Debug.Assert(type.GetType() is Type);

                                var clrType = (Type)type;
                                property.HasType(clrType);

                                if (clrType.IsEnum())
                                {
                                    AddScalarPropertyAttribute(propertyDefinition, property);
                                }
                                else
                                {
                                    AddComplexPropertyAttribute(property);
                                }
                            }
                        }
                    }
                }
            }
        }

        private void BuildNavigationProperties(XElement structuralTypeDefinition)
        {
            foreach (var navigationPropertyDefinition in structuralTypeDefinition.Elements(_csdlNs + "NavigationProperty"))
            {
                var property = _assembly
                    .DynamicTypes.OfType<DynamicStructuralType>()
                    .Single(t => t.Name == GetFullTypeName(structuralTypeDefinition))
                    .Property((string)navigationPropertyDefinition.Attribute("Name"));

                var correspondingAssociationType =
                    navigationPropertyDefinition
                    .Document
                    .Root
                    .Elements(_csdlNs + "Association")
                    .Single(at => GetFullTypeName(at) == (string)navigationPropertyDefinition.Attribute("Relationship"));

                var targetEnd = 
                    correspondingAssociationType
                    .Elements(_csdlNs + "End")
                    .Single(e => (string)e.Attribute("Role") == (string)navigationPropertyDefinition.Attribute("ToRole"));

                var propertyType = 
                    _assembly
                    .DynamicTypes.OfType<DynamicStructuralType>()
                    .Single(t => t.Name == (string)targetEnd.Attribute("Type"));

                if ((string)targetEnd.Attribute("Multiplicity") == "*")
                {
                    property.HasCollectionType(
                        _isPOCO ? typeof(ICollection<>) : typeof(EntityCollection<>),
                        propertyType);
                }
                else
                {
                    property.HasReferenceType(propertyType);
                }

                AddNavigationPropertyAttribute(navigationPropertyDefinition, property);
            }
        }

        private void AddRelationshipAttributes(XDocument csdl, AssemblyBuilder assemblyBuilder)
        {
            if (!_isPOCO)
            {
                var relationshipNamespaceName = GetModelNamespace(csdl.Root);

                foreach (var associationDefinition in csdl.Descendants(_csdlNs + "Association"))
                {
                    var relationshipName = (string)associationDefinition.Attribute("Name");
                    string end1Name, end2Name;
                    Type end1Type, end2Type;
                    RelationshipMultiplicity end1Multiplicity, end2Multiplicity;

                    GetEndDetails(
                        assemblyBuilder, associationDefinition.Elements(_csdlNs + "End").First(), out end1Name, out end1Multiplicity,
                        out end1Type);
                    GetEndDetails(
                        assemblyBuilder, associationDefinition.Elements(_csdlNs + "End").Last(), out end2Name, out end2Multiplicity,
                        out end2Type);

                    assemblyBuilder.SetCustomAttribute(
                        new CustomAttributeBuilder(
                            typeof(EdmRelationshipAttribute).GetConstructors().Single(ci => ci.GetParameters().Length == 8),
                            new object[]
                                {
                                    relationshipNamespaceName, relationshipName, end1Name, end1Multiplicity, end1Type, end2Name,
                                    end2Multiplicity, end2Type
                                }));
                }
            }
        }

        private void GetEndDetails(AssemblyBuilder assemblyBuilder, XElement end, out string endRole, out RelationshipMultiplicity endMultiplicity, out Type endType)
        {
            Debug.Assert(assemblyBuilder != null, "assemblyBuilder != null");
            Debug.Assert(end != null, "end != null");
            Debug.Assert(end.Name == _csdlNs + "End", "End element expected");

            endRole = (string)end.Attribute("Role");

            var multiplicity = (string)end.Attribute("Multiplicity");
            Debug.Assert(multiplicity == "*" || multiplicity == "0..1" || multiplicity == "1");
            endMultiplicity = multiplicity == "*" ? RelationshipMultiplicity.Many :
                              multiplicity == "1" ? RelationshipMultiplicity.One :
                              RelationshipMultiplicity.ZeroOrOne;

            endType = assemblyBuilder.GetType((string)end.Attribute("Type"));
        }

        private void AddSchemaAttribute()
        {
            if (!_isPOCO)
            {
                _assembly.Attributes.Add(new EdmSchemaAttribute());
            }
        }

        private void AddEdmTypeAttribute<T>(XElement typeDefinition, DynamicType dynamicType)
            where T: EdmTypeAttribute, new()
        {
            if (!_isPOCO)
            {
                string typeName, typeNamespace;
                GetModelTypeNameAndNamespace(typeDefinition, out typeName, out typeNamespace);
                dynamicType.Attributes.Add(
                    new T()
                    {
                        Name = typeName,
                        NamespaceName = typeNamespace
                    });
            }
        }

        private void AddScalarPropertyAttribute(XElement propertyDefinition, DynamicProperty property)
        {
            if (!_isPOCO)
            {
                property.HasAttribute(
                    new EdmScalarPropertyAttribute()
                    {
                        IsNullable = ((bool?)propertyDefinition.Attribute("Nullable") ?? false),
                        EntityKeyProperty =
                            propertyDefinition.Parent.Descendants(_csdlNs + "PropertyRef")
                                              .Any(
                                                  e =>
                                                  e.Parent.Name == _csdlNs + "Key"
                                                  && (string)e.Attribute("Name") == property.PropertyName)
                    });
            }
        }

        private void AddComplexPropertyAttribute(DynamicProperty property)
        {
            if (!_isPOCO)
            {
                property.HasAttribute(new EdmComplexPropertyAttribute());
            }
        }

        private void AddNavigationPropertyAttribute(XElement propertyDefinition, DynamicProperty property)
        {
            if (!_isPOCO)
            {
                string[] relationship = propertyDefinition.Attribute("Relationship").Value.Split('.');

                property.HasAttribute(
                    new EdmRelationshipNavigationPropertyAttribute(
                        relationship[0],
                        relationship[1],
                        (string)propertyDefinition.Attribute("ToRole")));
            }
        }


        private static string GetFullTypeName(XElement typeDefinition)
        {
            return string.Format(
                "{0}.{1}",
                (string)GetModelNamespace(typeDefinition),
                (string)typeDefinition.Attribute("Name"));
        }

        private static string GetModelNamespace(XElement csdlElement)
        {
            return csdlElement.Document.Root.Attribute("Namespace").Value;
        }

        private static void GetModelTypeNameAndNamespace(XElement typeElement, out string typeName, out string typeNamespace)
        {
            typeName =
                (string)typeElement.Attribute(_csdlMappingTestExtNs + "OSpaceTypeName") ??
                (string)typeElement.Attribute("Name");

            typeNamespace =
                (string)typeElement.Attribute(_csdlMappingTestExtNs + "OSpaceTypeNamespace") ??
                GetModelNamespace(typeElement);
        }

        private object ResolveType(string typeName)
        {
            object type;
            if((type = _assembly.DynamicTypes.SingleOrDefault(t => t.Name == typeName)) == null)
            {
                if ((type = _createdAssemblies.SelectMany(a => a.GetTypes()).SingleOrDefault(t => t.FullName == typeName)) == null)
                {
                    throw new InvalidOperationException(string.Format("Cannot resolve type {0}", typeName));
                }
            }
                    
            return type;
        }
    }
}