using System.Diagnostics;
using Mono.Cecil;
using Mono.Cecil.Rocks;
namespace Burst.Compiler.IL.Syntax
{
    /// 
    /// A generic context contains a mapping between GenericParameter ({T}) and resolved TypeReference (int, float, MyStruct<float>)
    /// 
#if BURST_INTERNAL || BURST_COMPILER_SHARED
    public
#else
    internal
#endif
    readonly struct GenericContext
    {
        private readonly GenericInstanceType _typeContext;
        private readonly GenericInstanceMethod _methodContext;
        /// 
        /// An empty 
        /// 
        public static readonly GenericContext None = new GenericContext();
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The generic method instance.
        private GenericContext(GenericInstanceMethod genericMethod, GenericInstanceType genericType)
        {
            _methodContext = genericMethod;
            _typeContext = genericType;
        }
        /// 
        /// Is there no generics in this context?
        /// 
        /// 
        public bool IsEmpty()
        {
            return _typeContext == null && _methodContext == null;
        }
        /// 
        /// Resolve the generics of the given 
        /// 
        /// 
        /// 
        public MethodReference Resolve(MethodReference unresolvedMethod)
        {
            Debug.Assert(unresolvedMethod != null);
            // The following code was originally derived from IL2CPP.
            var resolvedMethod = unresolvedMethod;
            if (IsEmpty())
            {
                return resolvedMethod;
            }
            var declaringType = Resolve(unresolvedMethod.DeclaringType);
            if (unresolvedMethod is GenericInstanceMethod genericInstanceMethod)
            {
                resolvedMethod = new MethodReference(unresolvedMethod.Name, unresolvedMethod.ReturnType, declaringType);
                foreach (var p in unresolvedMethod.Parameters)
                {
                    resolvedMethod.Parameters.Add(new ParameterDefinition(p.Name, p.Attributes, p.ParameterType));
                }
                foreach (var gp in genericInstanceMethod.ElementMethod.GenericParameters)
                {
                    resolvedMethod.GenericParameters.Add(new GenericParameter(gp.Name, resolvedMethod));
                }
                resolvedMethod.HasThis = unresolvedMethod.HasThis;
                var m = new GenericInstanceMethod(resolvedMethod);
                foreach (var ga in genericInstanceMethod.GenericArguments)
                {
                    m.GenericArguments.Add(Resolve(ga));
                }
                resolvedMethod = m;
            }
            else
            {
                if (unresolvedMethod.HasGenericParameters)
                {
                    var newGenericInstanceMethod = new GenericInstanceMethod(unresolvedMethod);
                    foreach (var gp in unresolvedMethod.GenericParameters)
                    {
                        newGenericInstanceMethod.GenericArguments.Add(Resolve(gp));
                    }
                    resolvedMethod = newGenericInstanceMethod;
                }
                else
                {
                    resolvedMethod = new MethodReference(unresolvedMethod.Name, unresolvedMethod.ReturnType, declaringType);
                    foreach (var p in unresolvedMethod.Parameters)
                    {
                        resolvedMethod.Parameters.Add(new ParameterDefinition(p.Name, p.Attributes, p.ParameterType));
                    }
                    resolvedMethod.HasThis = unresolvedMethod.HasThis;
                    resolvedMethod.MetadataToken = unresolvedMethod.MetadataToken;
                }
            }
            return resolvedMethod;
        }
        /// 
        /// Expands the specified  if it is either a  or a partially expanded 
        /// 
        /// The type reference.
        /// TypeReference.
        public TypeReference Resolve(TypeReference typeReference)
        {
            Debug.Assert(typeReference != null);
            if (IsEmpty())
            {
                return typeReference;
            }
            switch (typeReference)
            {
                case GenericParameter genericParam:
                    Debug.Assert(genericParam.Owner != null);
                    if (genericParam.Owner.GenericParameterType == GenericParameterType.Type)
                    {
                        Debug.Assert(_typeContext != null);
                        return _typeContext.GenericArguments[genericParam.Position];
                    }
                    else
                    {
                        Debug.Assert(_methodContext != null);
                        return _methodContext.GenericArguments[genericParam.Position];
                    }
                case ArrayType arrayType:
                    return new ArrayType(Resolve(arrayType.ElementType), arrayType.Rank);
                case PointerType pointerType:
                    return Resolve(pointerType.ElementType).MakePointerType();
                case PinnedType pinnedType:
                    return Resolve(pinnedType.ElementType).MakePointerType();
                case ByReferenceType byRefType:
                    return Resolve(byRefType.ElementType).MakeByReferenceType();
                case RequiredModifierType requiredModType:
                    return new RequiredModifierType(requiredModType.ModifierType, Resolve(requiredModType.ElementType));
                case OptionalModifierType optionalModType:
                    return Resolve(optionalModType.ElementType);
            }
            if (ContainsGenericParameters(typeReference))
            {
                if (typeReference is GenericInstanceType partialGenericInstance)
                {
                    // TODO: Ideally, we should cache this GenericInstanceType once it has been resolved
                    var genericInstance = new GenericInstanceType(partialGenericInstance.ElementType);
                    foreach (var genericArgument in partialGenericInstance.GenericArguments)
                    {
                        genericInstance.GenericArguments.Add(Resolve(genericArgument));
                    }
                    return genericInstance;
                }
                else
                {
                    // Sometimes we can have a TypeDefinition with HasGenericParameters false, but GenericParameters.Count > 0
                    var typeDefinition = typeReference as TypeDefinition;
                    if (typeDefinition?.GenericParameters.Count > 0)
                    {
                        var genericInstance = new GenericInstanceType(typeDefinition);
                        foreach (var genericArgument in typeDefinition.GenericParameters)
                        {
                            genericInstance.GenericArguments.Add(Resolve(genericArgument));
                        }
                        return genericInstance;
                    }
                }
            }
            return typeReference;
        }
        /// 
        /// If the given type is a reference or pointer type, the underlying type is returned
        /// 
        /// 
        /// 
        public static TypeReference GetTypeReferenceForPointerOrReference(TypeReference typeReference)
        {
            while (true)
            {
                switch (typeReference)
                {
                    case PointerType pointerType:
                        typeReference = pointerType.ElementType;
                        break;
                    case ByReferenceType byRefType:
                        typeReference = byRefType.ElementType;
                        break;
                    default:
                        return typeReference;
                }
            }
        }
        /// 
        /// Create  from a 
        /// 
        /// 
        /// 
        public static GenericContext From(TypeReference typeReference)
        {
            Debug.Assert(typeReference != null);
            if (typeReference is PinnedType pinnedType)
            {
                typeReference = pinnedType.ElementType;
            }
            typeReference = GetTypeReferenceForPointerOrReference(typeReference);
            if (typeReference is ArrayType arrayType)
            {
                typeReference = arrayType.ElementType;
            }
            return new GenericContext(null, typeReference as GenericInstanceType);
        }
        /// 
        /// Create  from a  and a 
        /// 
        /// 
        /// 
        /// 
        public static GenericContext From(MethodReference methodReference, TypeReference typeReference)
        {
            Debug.Assert(methodReference != null);
            Debug.Assert(typeReference != null);
            typeReference = GetTypeReferenceForPointerOrReference(typeReference);
            return new GenericContext(methodReference as GenericInstanceMethod, typeReference as GenericInstanceType);
        }
        /// 
        /// Checks if the specified TypeReference contains generic parameters that need type expansion
        /// 
        /// The type reference.
        /// true if the specified TypeReference contains generic arguments that need type expansion, false otherwise.
        public static bool ContainsGenericParameters(TypeReference typeReference)
        {
            switch (typeReference)
            {
                case GenericParameter genericParam:
                    return true;
                case ArrayType arrayType:
                    return ContainsGenericParameters(arrayType.ElementType);
                case PointerType pointerType:
                    return ContainsGenericParameters(pointerType.ElementType);
                case PinnedType pinnedType:
                    return ContainsGenericParameters(pinnedType.ElementType);
                case ByReferenceType byRefType:
                    return ContainsGenericParameters(byRefType.ElementType);
                case RequiredModifierType requiredModType:
                    return ContainsGenericParameters(requiredModType.ModifierType);
                case OptionalModifierType optionalModType:
                    return ContainsGenericParameters(optionalModType.ElementType);
                case GenericInstanceType partialGenericInstance:
                {
                    foreach (var genericArgument in partialGenericInstance.GenericArguments)
                    {
                        if (ContainsGenericParameters(genericArgument))
                        {
                            return true;
                        }
                    }
                    break;
                }
                case TypeDefinition typeDefinition:
                {
                    // Sometimes we can have a TypeDefinition with HasGenericParameters false, but GenericParameters.Count > 0
                    return typeDefinition.GenericParameters.Count > 0;
                }
            }
            return false;
        }
    }
}