using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ModestTree;
using Zenject.ReflectionBaking.Mono.Cecil;
using Zenject.ReflectionBaking.Mono.Collections.Generic;
using ICustomAttributeProvider = Zenject.ReflectionBaking.Mono.Cecil.ICustomAttributeProvider;

namespace Zenject.ReflectionBaking
{
    public static class CecilExtensions
    {
        public static Type TryGetActualType(this TypeReference typeRef, Assembly assembly)
        {
            var reflectionName = GetReflectionName(typeRef);
            return assembly.GetType(reflectionName);
        }

        static string GetReflectionName(TypeReference type)
        {
            if (type.IsGenericInstance)
            {
                var genericInstance = (GenericInstanceType)type;

                return string.Format(
                    "{0}.{1}[{2}]", genericInstance.Namespace, type.Name,
                    String.Join(",", genericInstance.GenericArguments.Select(p => GetReflectionName(p)).ToArray()));
            }

            return type.FullName;
        }

        public static List<TypeDefinition> LookupAllTypes(this ModuleDefinition module)
        {
            var allTypes = new List<TypeDefinition>();

            foreach (var type in module.Types)
            {
                LookupAllTypesInternal(type, allTypes);
            }

            return allTypes;
        }

        static void LookupAllTypesInternal(TypeDefinition type, List<TypeDefinition> buffer)
        {
            buffer.Add(type);

            foreach (var nestedType in type.NestedTypes)
            {
                LookupAllTypesInternal(nestedType, buffer);
            }
        }

        public static TypeReference ImportType<T>(this ModuleDefinition module)
        {
            return module.ImportType(typeof(T));
        }

        public static TypeReference ImportType(this ModuleDefinition module, Type type)
        {
            return module.Import(type);
        }

        public static MethodReference ImportMethod<T>(this ModuleDefinition module, string methodName)
        {
            return module.ImportMethod(typeof(T), methodName);
        }

        public static MethodReference ImportMethod(
            this ModuleDefinition module, Type type, string methodName)
        {
            return module.Import(
                module.ImportType(type).Resolve().GetMethod(methodName));
        }

        public static MethodReference ImportMethod<T>(
            this ModuleDefinition module, string methodName, int numArgs)
        {
            return module.ImportMethod(typeof(T), methodName, numArgs);
        }

        public static MethodReference ImportMethod(
            this ModuleDefinition module, Type type, string methodName, int numArgs)
        {
            return module.Import(
                module.ImportType(type).Resolve().GetMethod(methodName, numArgs));
        }

        public static MethodDefinition GetMethod(this TypeDefinition instance, string name)
        {
            for (int i = 0; i < instance.Methods.Count; i++)
            {
                MethodDefinition methodDef = instance.Methods[i];

                if (string.CompareOrdinal(methodDef.Name, name) == 0)
                {
                    return methodDef;
                }
            }
            return null;
        }

        public static MethodDefinition GetMethod(this TypeDefinition instance, string name, params Type[] parameterTypes)
        {
            for (int i = 0; i < instance.Methods.Count; i++)
            {
                MethodDefinition methodDefinition = instance.Methods[i];

                if (!string.Equals(methodDefinition.Name, name, StringComparison.Ordinal) ||
                    parameterTypes.Length != methodDefinition.Parameters.Count)
                {
                    continue;
                }

                MethodDefinition result = methodDefinition;
                for (int x = methodDefinition.Parameters.Count - 1; x >= 0; x--)
                {
                    ParameterDefinition parameter = methodDefinition.Parameters[x];
                    if (!string.Equals(parameter.ParameterType.Name, parameterTypes[x].Name, StringComparison.Ordinal))
                    {
                        break;
                    }

                    if (x == 0)
                    {
                        return result;
                    }
                }
            }
            return null;
        }

        public static MethodDefinition GetMethod(this TypeDefinition instance, string name, params TypeReference[] parameterTypes)
        {
            if (instance.Methods != null)
            {
                for (int i = 0; i < instance.Methods.Count; i++)
                {
                    MethodDefinition methodDefinition = instance.Methods[i];
                    if (string.Equals(methodDefinition.Name, name, StringComparison.Ordinal) // Names Match
                        && parameterTypes.Length == methodDefinition.Parameters.Count) // The same number of parameters
                    {
                        MethodDefinition result = methodDefinition;
                        for (int x = methodDefinition.Parameters.Count - 1; x >= 0; x--)
                        {
                            ParameterDefinition parameter = methodDefinition.Parameters[x];
                            if (!string.Equals(parameter.ParameterType.Name, parameterTypes[x].Name, StringComparison.Ordinal))
                            {
                                break;
                            }

                            if (x == 0)
                            {
                                return result;
                            }
                        }
                    }
                }
            }
            return null;
        }

        public static MethodDefinition GetMethod(this TypeDefinition instance, string name, int argCount)
        {
            for (int i = 0; i < instance.Methods.Count; i++)
            {
                MethodDefinition methodDef = instance.Methods[i];

                if (string.CompareOrdinal(methodDef.Name, name) == 0 && methodDef.Parameters.Count == argCount)
                {
                    return methodDef;
                }
            }
            return null;
        }

        public static PropertyDefinition GetPropertyDefinition(this TypeDefinition instance, string name)
        {
            for (int i = 0; i < instance.Properties.Count; i++)
            {
                PropertyDefinition preopertyDef = instance.Properties[i];

                // Properties can only have one argument or they are an indexer.
                if (string.CompareOrdinal(preopertyDef.Name, name) == 0 && preopertyDef.Parameters.Count == 0)
                {
                    return preopertyDef;
                }
            }
            return null;
        }

        public static bool HasCustomAttribute<T>(this ICustomAttributeProvider instance)
        {
            if (!instance.HasCustomAttributes)
            {
                return false;
            }

            Collection<CustomAttribute> attributes = instance.CustomAttributes;

            for(int i = 0;  i < attributes.Count; i++)
            {
                if (attributes[i].AttributeType.FullName.Equals(typeof(T).FullName, StringComparison.Ordinal))
                {
                    return true;
                }
            }

            return false;
        }

        public static MethodReference ChangeDeclaringType(
            this MethodReference methodDef, TypeReference typeRef)
        {
            var newMethodRef = new MethodReference(
                methodDef.Name, methodDef.ReturnType, typeRef);

            newMethodRef.HasThis = methodDef.HasThis;

            foreach (var arg in methodDef.Parameters)
            {
                var paramDef = new ParameterDefinition(arg.ParameterType);

                newMethodRef.Parameters.Add(paramDef);
            }

            return newMethodRef;
        }

        public static FieldReference ChangeDeclaringType(
            this FieldReference fieldDef, TypeReference typeRef)
        {
            return new FieldReference(
                fieldDef.Name, fieldDef.FieldType, typeRef);
        }

        public static CustomAttribute GetCustomAttribute<T>(this ICustomAttributeProvider instance)
        {
            if (!instance.HasCustomAttributes)
            {
                return null;
            }

            Collection<CustomAttribute> attributes = instance.CustomAttributes;

            for (int i = 0; i < attributes.Count; i++)
            {
                if (attributes[i].AttributeType.FullName.Equals(typeof(T).FullName, StringComparison.Ordinal))
                {
                    return attributes[i];
                }
            }
            return null;
        }

        public static IEnumerable<TypeReference> GetSpecificBaseTypesAndSelf(
            this TypeReference specificTypeRef)
        {
            yield return specificTypeRef;

            foreach (var ancestor in specificTypeRef.GetSpecificBaseTypesAndSelf())
            {
                yield return ancestor;
            }
        }

        public static IEnumerable<TypeReference> GetSpecificBaseTypes(
            this TypeReference specificTypeRef)
        {
            var specificBaseTypeRef = specificTypeRef.TryGetSpecificBaseType();

            if (specificBaseTypeRef != null)
            {
                yield return specificBaseTypeRef;

                foreach (var ancestor in GetSpecificBaseTypes(specificBaseTypeRef))
                {
                    yield return ancestor;
                }
            }
        }

        public static IEnumerable<TypeReference> AllNestParentsAndSelf(this TypeReference specificTypeRef)
        {
            yield return specificTypeRef;

            foreach (var ancestor in specificTypeRef.AllNestParents())
            {
                yield return ancestor;
            }
        }

        public static IEnumerable<TypeReference> AllNestParents(this TypeReference specificTypeRef)
        {
            if (specificTypeRef.DeclaringType != null)
            {
                yield return specificTypeRef.DeclaringType;

                foreach (var ancestor in specificTypeRef.DeclaringType.AllNestParents())
                {
                    yield return ancestor;
                }
            }
        }

        public static TypeReference TryResolve(this TypeReference typeRef)
        {
            try
            {
                return typeRef.Resolve();
            }
            catch
            {
                return null;
            }
        }

        public static TypeReference TryGetSpecificBaseType(this TypeReference specificTypeRef)
        {
            var typeDef = specificTypeRef.Resolve();

            if (typeDef.BaseType == null
                || typeDef.BaseType.FullName == "System.Object")
            {
                return null;
            }

            var specificBaseTypeRef = typeDef.BaseType;

            if (specificBaseTypeRef.ContainsGenericParameter)
            {
                var genericArgMap = new Dictionary<string, TypeReference>();

                foreach (var ancestor in specificTypeRef.AllNestParentsAndSelf())
                {
                    var specificTypeRefGenericInstance = ancestor as GenericInstanceType;

                    if (specificTypeRefGenericInstance != null)
                    {
                        for (int i = 0; i < typeDef.GenericParameters.Count; i++)
                        {
                            genericArgMap[typeDef.GenericParameters[i].Name] = specificTypeRefGenericInstance.GenericArguments[i];
                        }
                    }
                }

                specificBaseTypeRef = FillInGenericParameters(specificBaseTypeRef, genericArgMap);
            }

            return specificBaseTypeRef;
        }

        public static TypeReference FillInGenericParameters(
            TypeReference type, Dictionary<string, TypeReference> genericArgMap)
        {
            var genericType = type as GenericInstanceType;
            Assert.IsNotNull(genericType);

            var genericTypeClone = new GenericInstanceType(type.Resolve());

            for (int i = 0; i < genericType.GenericArguments.Count; i++)
            {
                var arg = genericType.GenericArguments[i];

                if (arg.IsGenericParameter)
                {
                    Assert.That(genericArgMap.ContainsKey(arg.Name), "Could not find key '{0}' for type '{1}'", arg.Name, type.FullName);

                    genericTypeClone.GenericArguments.Add(genericArgMap[arg.Name]);
                }
                else
                {
                    genericTypeClone.GenericArguments.Add(arg);
                }
            }

            return genericTypeClone;
        }
    }
}
