// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Composition.Convention;
using System.Composition.Hosting;
using System.Composition.Hosting.Core;
using System.Composition.TypedParts.ActivationFeatures;
using System.Linq;
using System.Reflection;

namespace System.Composition.TypedParts.Discovery
{
    internal sealed class TypeInspector
    {
        private static readonly IDictionary<string, object> s_noMetadata = new Dictionary<string, object>();

        private readonly ActivationFeature[] _activationFeatures;
        private readonly AttributedModelProvider _attributeContext;

        public TypeInspector(AttributedModelProvider attributeContext, ActivationFeature[] activationFeatures)
        {
            _attributeContext = attributeContext;
            _activationFeatures = activationFeatures;
        }

        public bool InspectTypeForPart(TypeInfo type, out DiscoveredPart part)
        {
            part = null;

            if (type.IsAbstract || !type.IsClass || _attributeContext.GetDeclaredAttribute<PartNotDiscoverableAttribute>(type.AsType(), type) != null)
                return false;

            foreach (var export in DiscoverExports(type))
            {
                part ??= new DiscoveredPart(type, _attributeContext, _activationFeatures);
                part.AddDiscoveredExport(export);
            }

            return part != null;
        }

        private IEnumerable<DiscoveredExport> DiscoverExports(TypeInfo partType)
        {
            foreach (var export in DiscoverInstanceExports(partType))
                yield return export;

            foreach (var export in DiscoverPropertyExports(partType))
                yield return export;
        }

        private IEnumerable<DiscoveredExport> DiscoverInstanceExports(TypeInfo partType)
        {
            var partTypeAsType = partType.AsType();
            foreach (var export in _attributeContext.GetDeclaredAttributes<ExportAttribute>(partTypeAsType, partType))
            {
                IDictionary<string, object> metadata = new Dictionary<string, object>();
                ReadMetadataAttribute(export, metadata);

                var applied = _attributeContext.GetDeclaredAttributes(partTypeAsType, partType);
                ReadLooseMetadata(applied, metadata);

                var contractType = export.ContractType ?? partTypeAsType;
                CheckInstanceExportCompatibility(partType, contractType.GetTypeInfo());

                var exportKey = new CompositionContract(contractType, export.ContractName);

                if (metadata.Count == 0)
                    metadata = s_noMetadata;

                yield return new DiscoveredInstanceExport(exportKey, metadata);
            }
        }

        private IEnumerable<DiscoveredExport> DiscoverPropertyExports(TypeInfo partType)
        {
            var partTypeAsType = partType.AsType();
            foreach (var property in partTypeAsType.GetRuntimeProperties()
                .Where(pi => pi.CanRead && pi.GetMethod.IsPublic && !pi.GetMethod.IsStatic))
            {
                foreach (var export in _attributeContext.GetDeclaredAttributes<ExportAttribute>(partTypeAsType, property))
                {
                    IDictionary<string, object> metadata = new Dictionary<string, object>();
                    ReadMetadataAttribute(export, metadata);

                    var applied = _attributeContext.GetDeclaredAttributes(partTypeAsType, property);
                    ReadLooseMetadata(applied, metadata);

                    var contractType = export.ContractType ?? property.PropertyType;
                    CheckPropertyExportCompatibility(partType, property, contractType.GetTypeInfo());

                    var exportKey = new CompositionContract(export.ContractType ?? property.PropertyType, export.ContractName);

                    if (metadata.Count == 0)
                        metadata = s_noMetadata;

                    yield return new DiscoveredPropertyExport(exportKey, metadata, property);
                }
            }
        }

        private static void ReadLooseMetadata(object[] appliedAttributes, IDictionary<string, object> metadata)
        {
            foreach (var attribute in appliedAttributes)
            {
                if (attribute is ExportAttribute)
                    continue;

                var ema = attribute as ExportMetadataAttribute;
                if (ema != null)
                {
                    var valueType = ema.Value?.GetType() ?? typeof(object);
                    AddMetadata(metadata, ema.Name, valueType, ema.Value);
                }
                else
                {
                    ReadMetadataAttribute((Attribute)attribute, metadata);
                }
            }
        }

        private static void AddMetadata(IDictionary<string, object> metadata, string name, Type valueType, object value)
        {
            object existingValue;
            if (!metadata.TryGetValue(name, out existingValue))
            {
                metadata.Add(name, value);
                return;
            }

            var existingArray = existingValue as Array;
            if (existingArray != null)
            {
                var newArray = Array.CreateInstance(valueType, existingArray.Length + 1);
                Array.Copy(existingArray, newArray, existingArray.Length);
                newArray.SetValue(value, existingArray.Length);
                metadata[name] = newArray;
            }
            else
            {
                var newArray = Array.CreateInstance(valueType, 2);
                newArray.SetValue(existingValue, 0);
                newArray.SetValue(value, 1);
                metadata[name] = newArray;
            }
        }

        private static void ReadMetadataAttribute(Attribute attribute, IDictionary<string, object> metadata)
        {
            var attrType = attribute.GetType();

            // Note, we don't support ReflectionContext in this scenario as
            if (attrType.GetTypeInfo().GetCustomAttribute<MetadataAttributeAttribute>(true) == null)
                return;

            foreach (var prop in attrType
                .GetRuntimeProperties()
                .Where(p => p.DeclaringType == attrType && p.CanRead))
            {
                AddMetadata(metadata, prop.Name, prop.PropertyType, prop.GetValue(attribute, null));
            }
        }

        private static void CheckPropertyExportCompatibility(TypeInfo partType, PropertyInfo property, TypeInfo contractType)
        {
            if (partType.IsGenericTypeDefinition)
            {
                CheckGenericContractCompatibility(partType, property.PropertyType.GetTypeInfo(), contractType);
            }
            else if (!contractType.IsAssignableFrom(property.PropertyType.GetTypeInfo()))
            {
                string message = SR.Format(SR.TypeInspector_ExportedContractTypeNotAssignable,
                                                contractType.Name, property.Name, partType.Name);
                throw new CompositionFailedException(message);
            }
        }

        private static void CheckGenericContractCompatibility(TypeInfo partType, TypeInfo exportingMemberType, TypeInfo contractType)
        {
            if (!contractType.IsGenericTypeDefinition)
            {
                string message = SR.Format(SR.TypeInspector_NoExportNonGenericContract, partType.Name, contractType.Name);
                throw new CompositionFailedException(message);
            }

            var compatible = false;

            foreach (var ifce in GetAssignableTypes(exportingMemberType))
            {
                if (ifce == contractType || (ifce.IsGenericType && ifce.GetGenericTypeDefinition() == contractType.AsType()))
                {
                    var mappedType = ifce;
                    if (!(mappedType == partType || mappedType.GenericTypeArguments.SequenceEqual(partType.GenericTypeParameters)))
                    {
                        string message = SR.Format(SR.TypeInspector_ArgumentMismatch, contractType.Name, partType.Name);
                        throw new CompositionFailedException(message);
                    }

                    compatible = true;
                    break;
                }
            }

            if (!compatible)
            {
                string message = SR.Format(SR.TypeInspector_ExportNotCompatible, exportingMemberType.Name, partType.Name, contractType.Name);
                throw new CompositionFailedException(message);
            }
        }

        private static IEnumerable<TypeInfo> GetAssignableTypes(TypeInfo exportingMemberType)
        {
            foreach (var ifce in exportingMemberType.ImplementedInterfaces)
                yield return ifce.GetTypeInfo();

            var b = exportingMemberType;
            while (b != null)
            {
                yield return b;
                b = b.BaseType?.GetTypeInfo();
            }
        }

        private static void CheckInstanceExportCompatibility(TypeInfo partType, TypeInfo contractType)
        {
            if (partType.IsGenericTypeDefinition)
            {
                CheckGenericContractCompatibility(partType, partType, contractType);
            }
            else if (!contractType.IsAssignableFrom(partType))
            {
                string message = SR.Format(SR.TypeInspector_ContractNotAssignable, contractType.Name, partType.Name);
                throw new CompositionFailedException(message);
            }
        }
    }
}
