// 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.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity.Core.Objects.DataClasses;
    using System.Data.Entity.Utilities;
    using System.Linq;
    using System.Reflection;
    using System.Reflection.Emit;
    using System.Runtime.Serialization;

    public class DynamicAssembly
    {
        private readonly Dictionary<string, Tuple<TypeBuilder, ConstructorBuilder>> _typeBuilders =
            new Dictionary<string, Tuple<TypeBuilder, ConstructorBuilder>>();

        private readonly Dictionary<string, DynamicType> _dynamicTypes = new Dictionary<string, DynamicType>();
        private readonly Dictionary<string, Type> _types = new Dictionary<string, Type>();
        private readonly List<Attribute> _attributes = new List<Attribute>();

        private static int _assemblyCount = 1;

        public IEnumerable<DynamicType> DynamicTypes
        {
            get { return _dynamicTypes.Values; }
        }

        public IEnumerable<Type> Types
        {
            get { return _types.Values; }
        }

        public DynamicStructuralType DynamicStructuralType(string typeName)
        {
            return GetDynamicType<DynamicStructuralType>(typeName);
        }

        public DynamicEnumType DynamicEnumType(string typeName)
        {
            var dynamicEnumType = GetDynamicType<DynamicEnumType>(typeName);

            return dynamicEnumType ??
                   new DynamicEnumType()
                       {
                           Name = typeName
                       };
        }

        private T GetDynamicType<T>(string typeName)
            where T : DynamicType, new()
        {
            DynamicType dynamicType;

            if (!_dynamicTypes.TryGetValue(typeName, out dynamicType))
            {
                dynamicType = 
                    new T()
                    {
                        Name = typeName
                    };

                _dynamicTypes.Add(typeName, dynamicType);
            }

            return (T)dynamicType;
        }

        public DynamicAssembly HasAttribute(Attribute a)
        {
            _attributes.Add(a);
            return this;
        }

        public List<Attribute> Attributes
        {
            get { return _attributes; }
        }

        private static readonly MethodInfo s_Entity = typeof(DbModelBuilder).GetPublicInstanceMethod("Entity");

        public DbModelBuilder ToBuilder()
        {
            if (Types.Count() == 0)
            {
                Compile();
            }
            var builder = new DbModelBuilder();
            foreach (var t in Types)
            {
                var entityMethod = s_Entity.MakeGenericMethod(t);
                entityMethod.Invoke(builder, null);
            }
            return builder;
        }

        public Assembly Compile()
        {
            return Compile(new AssemblyName(string.Format("DynamicEntities{0}", _assemblyCount)));
        }

        public Assembly Compile(AssemblyName assemblyName)
        {
            var assemblyBuilder =
                AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);

            foreach (var attribute in Attributes)
            {
                assemblyBuilder.SetCustomAttribute(AnnotationAttributeBuilder.Create(attribute));
            }

            var moduleName = string.Format("DynamicEntitiesModule{0}.dll", _assemblyCount);
            var module = assemblyBuilder.DefineDynamicModule(moduleName);
            _assemblyCount++;

            foreach (var enumTypeInfo in DynamicTypes.OfType<DynamicEnumType>())
            {
                _types.Add(enumTypeInfo.Name, DefineEnumType(module, enumTypeInfo));
            }

            foreach (var typeInfo in DynamicTypes.OfType<DynamicStructuralType>())
            {
                DefineStructuralType(module, typeInfo);
            }

            foreach (var typeInfo in DynamicTypes.OfType<DynamicStructuralType>())
            {
                var typeBuilder = _typeBuilders[typeInfo.Name];

                foreach (var fieldInfo in typeInfo.Fields)
                {
                    DefineField(typeBuilder.Item1, typeBuilder.Item2, fieldInfo);
                }

                foreach (var propertyInfo in typeInfo.Properties)
                {
                    DefineProperty(typeBuilder.Item1, propertyInfo);
                }
            }

            foreach (var t in _typeBuilders)
            {
                _types.Add(t.Key, t.Value.Item1.CreateType());
            }

            return assemblyBuilder;
        }

        public Type GetType(string typeName)
        {
            return _types[typeName];
        }

        public bool TryGetType(string typeName, out Type type)
        {
            return _types.TryGetValue(typeName, out type);
        }

        private void DefineStructuralType(ModuleBuilder module, DynamicStructuralType typeInfo)
        {
            var typeAttributes = TypeAttributes.Class | GetTypeAccess(typeInfo.TypeAccess);

            if (typeInfo.IsAbstract)
            {
                typeAttributes |= TypeAttributes.Abstract;
            }
            if (typeInfo.IsSealed)
            {
                typeAttributes |= TypeAttributes.Sealed;
            }

            Type baseClass = null;
            if (typeInfo.BaseClass is Type)
            {
                baseClass = typeInfo.BaseClass as Type;
            }
            else if (typeInfo.BaseClass is DynamicStructuralType)
            {
                baseClass = _typeBuilders[((DynamicStructuralType)typeInfo.BaseClass).Name].Item1;
            }

            var typeBuilder = module.DefineType(typeInfo.Name, typeAttributes, baseClass);
            foreach (var a in typeInfo.Attributes)
            {
                typeBuilder.SetCustomAttribute(AnnotationAttributeBuilder.Create(a));
            }

            // Define the Ctor
            var constructorBuilder = typeInfo.CtorAccess == MemberAccess.None
                                         ? null
                                         : typeBuilder.DefineDefaultConstructor(GetMethodAttributes(false, typeInfo.CtorAccess));

            _typeBuilders.Add(typeInfo.Name, Tuple.Create(typeBuilder, constructorBuilder));
        }

        private void DefineField(TypeBuilder typeBuilder, ConstructorBuilder constructorBuilder, DynamicField fieldInfo)
        {
            var fieldAttributes = FieldAttributes.Public;
            if (fieldInfo.Static)
            {
                fieldAttributes |= FieldAttributes.Static;
            }

            var fieldType = _typeBuilders[fieldInfo.FieldType.Name].Item1;
            var fieldBuilder = typeBuilder.DefineField(fieldInfo.FieldName, fieldType, fieldAttributes);

            if (fieldInfo.SetInstancePattern)
            {
                var staticConstructorBuilder = typeBuilder.DefineConstructor(
                    MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, Type.EmptyTypes);
                var generator = staticConstructorBuilder.GetILGenerator();

                generator.Emit(OpCodes.Newobj, constructorBuilder);
                generator.Emit(OpCodes.Stsfld, fieldBuilder);
                generator.Emit(OpCodes.Ret);
            }
        }

        private void DefineProperty(TypeBuilder typeBuilder, DynamicProperty propertyInfo)
        {
            var getterAccess = propertyInfo.GetterAccess;
            var setterAccess = propertyInfo.SetterAccess;

            Type propertyType;
            if (propertyInfo.EnumType != null)
            {
                propertyType = _types[propertyInfo.EnumType.Item1.Name];

                if (propertyInfo.EnumType.Item2)
                {
                    propertyType = typeof(Nullable<>).MakeGenericType(propertyType);    
                }
            }
            else if (propertyInfo.CollectionType != null)
            {
                if (propertyInfo.ReferenceType != null)
                {
                    propertyType =
                        propertyInfo.CollectionType.MakeGenericType(_typeBuilders[propertyInfo.ReferenceType.Name].Item1);
                }
                else
                {
                    propertyType = propertyInfo.CollectionType.MakeGenericType(propertyInfo.PropertyType);
                }
            }
            else if (propertyInfo.ReferenceType != null)
            {
                propertyType = _typeBuilders[propertyInfo.ReferenceType.Name].Item1;
                switch (_dynamicTypes[propertyInfo.ReferenceType.Name].TypeAccess)
                {
                    case MemberAccess.Private:
                        getterAccess = MemberAccess.Private;
                        setterAccess = MemberAccess.Private;
                        break;
                    case MemberAccess.Internal:
                        getterAccess = MemberAccess.Internal;
                        setterAccess = MemberAccess.Internal;
                        break;
                }
                if (propertyInfo.PropertyType != null
                    && propertyInfo.PropertyType.IsGenericTypeDefinition())
                {
                    propertyType = propertyInfo.PropertyType.MakeGenericType(propertyType);
                }
            }
            else
            {
                propertyType = propertyInfo.PropertyType;
            }

            var fieldBuilder = typeBuilder.DefineField(
                "_" + propertyInfo.PropertyName, propertyType,
                FieldAttributes.Private);

            var propertyBuilder = typeBuilder.DefineProperty(
                propertyInfo.PropertyName,
                PropertyAttributes.None,
                propertyType, Type.EmptyTypes);

            foreach (var a in propertyInfo.Attributes)
            {
                propertyBuilder.SetCustomAttribute(AnnotationAttributeBuilder.Create(a));
            }

            if (propertyInfo.GetterAccess
                != MemberAccess.None)
            {
                var getter = typeBuilder.DefineMethod(
                    "get_" + propertyInfo.PropertyName,
                    GetMethodAttributes(propertyInfo.IsVirtual, getterAccess),
                    propertyType,
                    Type.EmptyTypes);
                var generator = getter.GetILGenerator();
                generator.Emit(OpCodes.Ldarg_0);
                generator.Emit(OpCodes.Ldfld, fieldBuilder);
                generator.Emit(OpCodes.Ret);
                propertyBuilder.SetGetMethod(getter);
            }

            if (propertyInfo.SetterAccess
                != MemberAccess.None)
            {
                var setter = typeBuilder.DefineMethod(
                    "set_" + propertyInfo.PropertyName,
                    GetMethodAttributes(propertyInfo.IsVirtual, setterAccess),
                    null,
                    new[] { propertyType });
                var generator = setter.GetILGenerator();
                generator.Emit(OpCodes.Ldarg_0);
                generator.Emit(OpCodes.Ldarg_1);
                generator.Emit(OpCodes.Stfld, fieldBuilder);
                generator.Emit(OpCodes.Ret);
                propertyBuilder.SetSetMethod(setter);
            }
        }

        private Type DefineEnumType(ModuleBuilder module, DynamicEnumType enumTypeInfo)
        {
            var enumBuilder = 
                module.DefineEnum(
                    enumTypeInfo.Name, 
                    GetTypeAccess(enumTypeInfo.TypeAccess), 
                    enumTypeInfo.UnderlyingType);

            foreach (var a in enumTypeInfo.Attributes)
            {
                enumBuilder.SetCustomAttribute(AnnotationAttributeBuilder.Create(a));
            }

            foreach (var enumMember in enumTypeInfo.Members)
            {
                enumBuilder.DefineLiteral(
                    enumMember.Key, 
                    Convert.ChangeType(enumMember.Value, enumTypeInfo.UnderlyingType));
            }

            return enumBuilder.CreateType();
        }

        private static TypeAttributes GetTypeAccess(MemberAccess typeAccess)
        {
            TypeAttributes typeAttributes = 0;
            switch (typeAccess)
            {
                case MemberAccess.Public:
                    typeAttributes |= TypeAttributes.Public;
                    break;
                case MemberAccess.Private:
                case MemberAccess.Internal:
                    typeAttributes |= TypeAttributes.NotPublic;
                    break;
            }
            return typeAttributes;
        }

        private static MethodAttributes GetMethodAttributes(bool isVirtual, MemberAccess memberAccess)
        {
            var attributes = MethodAttributes.HideBySig | MethodAttributes.SpecialName;
            if (isVirtual)
            {
                attributes |= MethodAttributes.Virtual;
            }
            switch (memberAccess)
            {
                case MemberAccess.Public:
                    attributes |= MethodAttributes.Public | MethodAttributes.NewSlot;
                    break;
                case MemberAccess.Private:
                    attributes |= MethodAttributes.Private;
                    break;
                case MemberAccess.Internal:
                    attributes |= MethodAttributes.Assembly | MethodAttributes.NewSlot;
                    break;
                case MemberAccess.Protected:
                    attributes |= MethodAttributes.Family | MethodAttributes.NewSlot;
                    break;
                case MemberAccess.ProtectedInternal:
                    attributes |= MethodAttributes.FamORAssem | MethodAttributes.NewSlot;
                    break;
            }
            return attributes;
        }
    }

    public class AnnotationAttributeBuilder
    {
        public static CustomAttributeBuilder Create(dynamic attribute)
        {
            CustomAttributeBuilder builder = CreateCustom(attribute);
            if (builder == null)
            {
                object[] args = GetArgs(attribute);
                return
                    new CustomAttributeBuilder(
                        attribute.GetType().GetConstructor(args.Select(x => x.GetType()).ToArray()), args);
            }
            return builder;
        }

        public static CustomAttributeBuilder CreateCustom(dynamic attribute)
        {
            return null;
        }

        public static CustomAttributeBuilder CreateCustom(DataMemberAttribute attribute)
        {
            if (attribute.Order
                == -1)
            {
                return new CustomAttributeBuilder(
                    typeof(DataMemberAttribute).GetDeclaredConstructor(),
                    new object[] { },
                    new PropertyInfo[] { },
                    new object[] { });
            }
            else
            {
                return new CustomAttributeBuilder(
                    typeof(DataMemberAttribute).GetDeclaredConstructor(),
                    new object[] { },
                    new[] { typeof(DataMemberAttribute).GetDeclaredProperty("Order") },
                    new object[] { attribute.Order });
            }
        }

        public static CustomAttributeBuilder CreateCustom(EdmTypeAttribute attribute)
        {
            return new CustomAttributeBuilder(
                attribute.GetType().GetDeclaredConstructor(),
                    new object[0],
                    new PropertyInfo[] 
                    {
                        typeof(EdmTypeAttribute).GetDeclaredProperty("Name"),
                        typeof(EdmTypeAttribute).GetDeclaredProperty("NamespaceName")
                    },
                    new object[] 
                    {
                        attribute.Name,
                        attribute.NamespaceName
                    });
        }

        public static CustomAttributeBuilder CreateCustom(EdmScalarPropertyAttribute attribute)
        {
            return new CustomAttributeBuilder(
                attribute.GetType().GetDeclaredConstructor(),
                    new object[0],
                    new PropertyInfo[] 
                    { 
                        typeof(EdmScalarPropertyAttribute).GetDeclaredProperty("EntityKeyProperty"), 
                        typeof(EdmScalarPropertyAttribute).GetDeclaredProperty("IsNullable") 
                    },
                    new object[] 
                    {
                        attribute.EntityKeyProperty, 
                        attribute.IsNullable
                    });
        }

        private static object[] GetArgs(dynamic attribute)
        {
            return GetArgs(attribute);
        }

        private static object[] GetArgs(KeyAttribute attribute)
        {
            return new object[] { };
        }

        private static object[] GetArgs(RequiredAttribute attribute)
        {
            return new object[] { };
        }

        private static object[] GetArgs(TimestampAttribute attribute)
        {
            return new object[] { };
        }

        private static object[] GetArgs(ConcurrencyCheckAttribute attribute)
        {
            return new object[] { };
        }

        private static object[] GetArgs(StringLengthAttribute attribute)
        {
            return new object[] { attribute.MaximumLength };
        }

        private static object[] GetArgs(MaxLengthAttribute attribute)
        {
            return new object[] { attribute.Length };
        }

        private static object[] GetArgs(DatabaseGeneratedAttribute attribute)
        {
            return new object[] { attribute.DatabaseGeneratedOption };
        }

        private static object[] GetArgs(EdmEntityTypeAttribute attribute)
        {
            return new object[] { };
        }

        private static object[] GetArgs(EdmSchemaAttribute attribute)
        {
            return new object[] { };
        }

        private static object[] GetArgs(EdmRelationshipNavigationPropertyAttribute attribute)
        {
            return new object[] 
                { 
                    attribute.RelationshipNamespaceName, 
                    attribute.RelationshipName, 
                    attribute.TargetRoleName 
                };
        }

        private static object[] GetArgs(EdmComplexPropertyAttribute attribute)
        {
            return new object[0];
        }
    }

    public enum MemberAccess
    {
        None,
        Public,
        Private,
        Internal,
        Protected,
        ProtectedInternal
    }

    public class DynamicProperty
    {
        private MemberAccess _getterAccess = MemberAccess.Public;
        private MemberAccess _setterAccess = MemberAccess.Public;
        private bool _isVirtual = true;
        private readonly List<Attribute> _attributes = new List<Attribute>();

        public Type PropertyType { get; set; }

        public DynamicProperty HasType(Type propertyType)
        {
            PropertyType = propertyType;
            return this;
        }

        public DynamicProperty HasType<T>()
        {
            PropertyType = typeof(T);
            return this;
        }

        public Tuple<DynamicEnumType, bool> EnumType { get; set; }

        public Tuple<DynamicEnumType, bool> HasEnumType(DynamicEnumType enumType, bool nullable)
        {
            EnumType = new Tuple<DynamicEnumType,bool>(enumType, nullable);
            return EnumType;
        }

        public DynamicStructuralType ReferenceType { get; set; }

        public DynamicProperty HasReferenceType(DynamicStructuralType referenceType)
        {
            ReferenceType = referenceType;
            return this;
        }

        public Type CollectionType { get; set; }

        public DynamicProperty HasCollectionType(Type collectionType, DynamicStructuralType referenceType)
        {
            CollectionType = collectionType;
            ReferenceType = referenceType;
            return this;
        }

        public List<Attribute> Attributes
        {
            get { return _attributes; }
        }

        public DynamicProperty HasAttribute(Attribute a)
        {
            _attributes.Add(a);
            return this;
        }

        public string PropertyName { get; set; }

        public DynamicProperty HasName(string propertyName)
        {
            PropertyName = propertyName;
            return this;
        }

        public MemberAccess GetterAccess
        {
            get { return _getterAccess; }
            set { _getterAccess = value; }
        }

        public DynamicProperty HasGetterAccess(MemberAccess access)
        {
            GetterAccess = access;
            return this;
        }

        public MemberAccess SetterAccess
        {
            get { return _setterAccess; }
            set { _setterAccess = value; }
        }

        public DynamicProperty HasSetterAccess(MemberAccess access)
        {
            SetterAccess = access;
            return this;
        }

        public bool IsVirtual
        {
            get { return _isVirtual; }
            set { _isVirtual = value; }
        }

        public DynamicProperty HasVirtual(bool isVirtual)
        {
            IsVirtual = isVirtual;
            return this;
        }
    }

    public class DynamicField
    {
        public DynamicStructuralType FieldType { get; set; }
        public string FieldName { get; set; }
        public bool Static { get; set; }
        public bool SetInstancePattern { get; set; }

        public DynamicField HasType(DynamicStructuralType propertyType)
        {
            FieldType = propertyType;
            return this;
        }

        public DynamicField HasName(string propertyName)
        {
            FieldName = propertyName;
            return this;
        }

        public DynamicField IsStatic()
        {
            Static = true;
            return this;
        }

        /// <summary>
        /// Sets the field up to match the Instance singleton pattern required by ADO.NET.
        /// </summary>
        public DynamicField IsInstance()
        {
            SetInstancePattern = true;
            return this;
        }
    }

    public abstract class DynamicType
    {
        private readonly List<Attribute> _attributes = new List<Attribute>();

        public string Name { get; set; }

        public List<Attribute> Attributes
        {
            get { return _attributes; }
        }

        private MemberAccess _typeAccess = MemberAccess.Public;

        public MemberAccess TypeAccess
        {
            get { return _typeAccess; }
            set { _typeAccess = value; }
        }
    }

    public class DynamicStructuralType : DynamicType
    {
        private readonly Dictionary<string, DynamicProperty> _properties = new Dictionary<string, DynamicProperty>();
        private readonly Dictionary<string, DynamicField> _fields = new Dictionary<string, DynamicField>();
        private MemberAccess _ctorAccess = MemberAccess.None;

        public DynamicStructuralType HasName(string name)
        {
            Name = name;
            return this;
        }

        public bool IsSealed { get; set; }

        public DynamicStructuralType HasSealed(bool isSealed)
        {
            IsSealed = isSealed;
            return this;
        }

        public bool IsAbstract { get; set; }

        public DynamicStructuralType HasAbstract(bool isAbstract)
        {
            IsAbstract = isAbstract;
            return this;
        }

        public DynamicStructuralType HasAttribute(Attribute a)
        {
            Attributes.Add(a);
            return this;
        }

        public DynamicStructuralType HasClassAccess(MemberAccess access)
        {
            TypeAccess = access;
            return this;
        }

        public MemberAccess CtorAccess
        {
            get { return _ctorAccess; }
            set { _ctorAccess = value; }
        }

        public DynamicProperty Property(string propertyName)
        {
            DynamicProperty property;
            if (!_properties.TryGetValue(propertyName, out property))
            {
                property = new DynamicProperty();
                property.PropertyName = propertyName;
                _properties.Add(propertyName, property);
            }
            return property;
        }

        public DynamicField Field(string fieldName)
        {
            DynamicField field;
            if (!_fields.TryGetValue(fieldName, out field))
            {
                field = new DynamicField
                            {
                                FieldName = fieldName
                            };
                _fields.Add(fieldName, field);
            }
            return field;
        }

        public object BaseClass { get; set; }

        public DynamicStructuralType HasBaseClass(object baseClass)
        {
            BaseClass = baseClass;
            return this;
        }

        public IEnumerable<DynamicProperty> Properties
        {
            get { return _properties.Values; }
        }

        public IEnumerable<DynamicField> Fields
        {
            get { return _fields.Values; }
        }
    }

    public class DynamicEnumType : DynamicType
    {
        private readonly Dictionary<string, object> _members = new Dictionary<string, object>();

        public DynamicEnumType HasName(string name)
        {
            Name = name;
            return this;
        }

        public DynamicEnumType()
        {
            UnderlyingType = typeof(int);
        }

        public DynamicEnumType HasClassAccess(MemberAccess access)
        {
            TypeAccess = access;
            return this;
        }

        public Type UnderlyingType { get; set; }

        public DynamicEnumType HasUnderlyingType(Type type)
        {
            UnderlyingType = type;
            return this;
        }

        public DynamicEnumType HasAttribute(Attribute a)
        {
            Attributes.Add(a);
            return this;
        }

        public IEnumerable<KeyValuePair<string, object>> Members
        {
            get { return _members; }
        }

        public DynamicEnumType HasMember(string name, object value)
        {
            _members[name] = value;
            return this;
        }
    }
}
