// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Cci;
using Microsoft.Cci.Extensions;

namespace Microsoft.Cci.Extensions
{
    public class TypeDependencies
    {
        private ICollection<ITypeReference> _publicDependents;
        private ICollection<ITypeReference> _dependents;
        private ICollection<IMethodReference> _methodDependents;

        public TypeDependencies(ITypeDefinition type, bool unique = true)
        {
            if (unique)
            {
                _publicDependents = new HashSet<ITypeReference>();
                _dependents = new HashSet<ITypeReference>();
                _methodDependents = new HashSet<IMethodReference>();
            }
            else
            {
                _publicDependents = new List<ITypeReference>();
                _dependents = new List<ITypeReference>();
                _methodDependents = new List<IMethodReference>();
            }
            CalculateForType(type);
        }

        public IEnumerable<ITypeReference> PublicTypeDepdencies { get { return _publicDependents; } }
        public IEnumerable<ITypeReference> AllTypeDependencies { get { return _dependents; } }
        public IEnumerable<IMethodReference> MethodDependencies { get { return _methodDependents; } }

        private void CalculateForType(ITypeDefinition type)
        {
            if (type.HasDeclarativeSecurity)
                AddDependency(type.SecurityAttributes);
            AddDependency(type.Attributes);

            AddDependency(type.BaseClasses);
            AddDependency(type.Interfaces);
            //AddDependency(type.ExplicitImplementationOverrides);

            AddDependency(type.Fields);
            AddDependency(type.Events);
            AddDependency(type.Properties);
            AddDependency(type.Methods);

            if (type.IsGeneric)
                AddDependency(type.GenericParameters);

            AddDependencyNestedTypes(type.NestedTypes);
        }

        private void AddDependencyNestedTypes(IEnumerable<INestedTypeDefinition> nestedTypes)
        {
            // TODO: Verify that this works properly for non-public nested types.
            // I'm guessing that it doesn't at this point and we might need to propagate the ispublic flag.
            foreach (INestedTypeDefinition type in nestedTypes)
                CalculateForType(type);
        }

        private void AddDependency(IEnumerable<IMethodDefinition> methods)
        {
            foreach (IMethodDefinition method in methods)
            {
                bool isPublic = method.IsVisibleOutsideAssembly();

                // Attributes
                if (method.HasDeclarativeSecurity)
                    AddDependency(method.SecurityAttributes, isPublic);
                AddDependency(method.Attributes, isPublic);

                // Return type
                if (method.Type.TypeCode != PrimitiveTypeCode.Void)
                    AddDependency(method.Type, isPublic);
                AddDependency(method.ReturnValueAttributes, isPublic);

                // Generic parameters
                if (method.IsGeneric)
                    AddDependency(method.GenericParameters, isPublic);

                // Parameters
                foreach (IParameterDefinition param in method.Parameters)
                {
                    AddDependency(param.Attributes, isPublic);
                    AddDependency(param.Type, isPublic);
                }

                // Method body
                AddDependencyFromMethodBody(method.Body);
            }
        }

        private void AddDependency(IEnumerable<IFieldDefinition> fields)
        {
            foreach (IFieldDefinition field in fields)
            {
                AddDependency(field.Attributes, field.IsVisibleOutsideAssembly());
                AddDependency(field.Type, field.IsVisibleOutsideAssembly());
            }
        }

        private void AddDependency(IEnumerable<IEventDefinition> events)
        {
            foreach (IEventDefinition evnt in events)
            {
                AddDependency(evnt.Attributes, evnt.IsVisibleOutsideAssembly());
                AddDependency(evnt.Type, evnt.IsVisibleOutsideAssembly());
            }
        }

        private void AddDependency(IEnumerable<IPropertyDefinition> properties)
        {
            foreach (IPropertyDefinition property in properties)
            {
                AddDependency(property.Attributes, property.IsVisibleOutsideAssembly());
                AddDependency(property.Type, property.IsVisibleOutsideAssembly());
            }
        }

        private void AddDependency(IEnumerable<ISecurityAttribute> attributes, bool isPublic = true)
        {
            foreach (ISecurityAttribute attribute in attributes)
                AddDependency(attribute.Attributes);
        }

        private void AddDependency(IEnumerable<ICustomAttribute> attributes, bool isPublic = true)
        {
            foreach (ICustomAttribute attribute in attributes)
                AddDependency(attribute.Type, isPublic);
        }

        private void AddDependency(IEnumerable<IGenericParameter> parameters, bool isPublic = true)
        {
            foreach (var genericParam in parameters)
                foreach (var constraint in genericParam.Constraints)
                    AddDependency(constraint, isPublic);
        }

        private void AddDependencyFromMethodBody(IMethodBody methodBody)
        {
            foreach (ILocalDefinition local in methodBody.LocalVariables)
            {
                AddDependency(local.Type);
            }

            foreach (IOperation op in methodBody.Operations)
            {
                switch (op.OperationCode)
                {
                    case OperationCode.Castclass:
                    case OperationCode.Box:
                    case OperationCode.Unbox:
                    case OperationCode.Unbox_Any:
                        AddDependency((ITypeReference)op.Value);
                        break;
                    case OperationCode.Call:
                    //case OperationCode.Calli: Native calls
                    case OperationCode.Callvirt:
                    case OperationCode.Newobj:
                    case OperationCode.Ldftn:
                    case OperationCode.Ldvirtftn:
                        IMethodReference methodRef = (IMethodReference)op.Value;
                        AddDependencyForCalledMethod(methodRef);
                        break;
                }
            }
        }

        private void AddDependency(IEnumerable<ITypeReference> types, bool isPublic = true)
        {
            foreach (ITypeReference type in types)
                AddDependency(type, isPublic);
        }
        private void AddDependency(ITypeReference type, bool isPublic = true)
        {
            if (isPublic)
                _publicDependents.Add(type);

            _dependents.Add(type);
        }
        private void AddDependencyForCalledMethod(IMethodReference method)
        {
            AddDependency(method.ContainingType, false);

            _methodDependents.Add(method);
        }
    }
}
