// <copyright file="DuckType.cs" company="OpenTelemetry Authors">
// Copyright The OpenTelemetry Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// </copyright>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;

namespace OpenTelemetry.AutoInstrumentation.DuckTyping;

/// <summary>
/// Create struct proxy instance delegate
/// </summary>
/// <typeparam name="T">Type of struct</typeparam>
/// <param name="instance">Object instance</param>
/// <returns>Proxy instance</returns>
public delegate T CreateProxyInstance<T>(object instance);

/// <summary>
/// Duck Type
/// </summary>
public static partial class DuckType
{
    /// <summary>
    /// Create duck type proxy using a base type
    /// </summary>
    /// <param name="instance">Instance object</param>
    /// <typeparam name="T">Duck type</typeparam>
    /// <returns>Duck type proxy</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static T Create<T>(object instance)
    {
        return CreateCache<T>.Create(instance);
    }

    /// <summary>
    /// Create duck type proxy using a base type
    /// </summary>
    /// <param name="proxyType">Duck type</param>
    /// <param name="instance">Instance object</param>
    /// <returns>Duck Type proxy</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static IDuckType Create(Type proxyType, object instance)
    {
        // Validate arguments
        EnsureArguments(proxyType, instance);

        // Create Type
        CreateTypeResult result = GetOrCreateProxyType(proxyType, instance.GetType());

        // Create instance
        return result.CreateInstance(instance);
    }

    /// <summary>
    /// Gets if a proxy can be created
    /// </summary>
    /// <param name="instance">Instance object</param>
    /// <typeparam name="T">Duck type</typeparam>
    /// <returns>true if the proxy can be created; otherwise, false</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool CanCreate<T>(object instance)
    {
        return CreateCache<T>.CanCreate(instance);
    }

    /// <summary>
    /// Gets if a proxy can be created
    /// </summary>
    /// <param name="proxyType">Duck type</param>
    /// <param name="instance">Instance object</param>
    /// <returns>true if the proxy can be created; otherwise, false</returns>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static bool CanCreate(Type proxyType, object instance)
    {
        // Validate arguments
        EnsureArguments(proxyType, instance);

        // Create Type
        CreateTypeResult result = GetOrCreateProxyType(proxyType, instance.GetType());

        // Create instance
        return result.CanCreate();
    }

    /// <summary>
    /// Gets or create a new proxy type for ducktyping
    /// </summary>
    /// <param name="proxyType">ProxyType interface</param>
    /// <param name="targetType">Target type</param>
    /// <returns>CreateTypeResult instance</returns>
    public static CreateTypeResult GetOrCreateProxyType(Type proxyType, Type targetType)
    {
        return DuckTypeCache.GetOrAdd(
                new TypesTuple(proxyType, targetType),
                key => new Lazy<CreateTypeResult>(() => CreateProxyType(key.ProxyDefinitionType, key.TargetType)))
            .Value;
    }

    private static CreateTypeResult CreateProxyType(Type proxyDefinitionType, Type targetType)
    {
        lock (_locker)
        {
            try
            {
                // Define parent type, interface types
                Type parentType;
                TypeAttributes typeAttributes;
                Type[] interfaceTypes;
                if (proxyDefinitionType.IsInterface || proxyDefinitionType.IsValueType)
                {
                    // If the proxy type definition is an interface we create an struct proxy
                    // If the proxy type definition is an struct then we use that struct to copy the values from the target type
                    parentType = typeof(ValueType);
                    typeAttributes = TypeAttributes.Public | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.SequentialLayout | TypeAttributes.Sealed | TypeAttributes.Serializable;
                    if (proxyDefinitionType.IsInterface)
                    {
                        interfaceTypes = new[] { proxyDefinitionType, typeof(IDuckType) };
                    }
                    else
                    {
                        interfaceTypes = new[] { typeof(IDuckType) };
                    }
                }
                else
                {
                    // If the proxy type definition is a class then we create a class proxy
                    parentType = proxyDefinitionType;
                    typeAttributes = TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout | TypeAttributes.Sealed;
                    interfaceTypes = new[] { typeof(IDuckType) };
                }

                // Gets the module builder
                var moduleBuilder = GetModuleBuilder(targetType, (targetType.IsPublic || targetType.IsNestedPublic) && (proxyDefinitionType.IsPublic || proxyDefinitionType.IsNestedPublic));

                // Ensure visibility
                EnsureTypeVisibility(moduleBuilder, targetType);
                EnsureTypeVisibility(moduleBuilder, proxyDefinitionType);

                string assembly = string.Empty;
                if (targetType.Assembly != null)
                {
                    // Include target assembly name and public token.
                    AssemblyName asmName = targetType.Assembly.GetName();
                    assembly = asmName.Name;
                    byte[] pbToken = asmName.GetPublicKeyToken();
                    assembly += "__" + BitConverter.ToString(pbToken).Replace("-", string.Empty);
                    assembly = assembly.Replace(".", "_").Replace("+", "__");
                }

                // Create a valid type name that can be used as a member of a class. (BenchmarkDotNet fails if is an invalid name)
                string proxyTypeName = $"{assembly}.{targetType.FullName.Replace(".", "_").Replace("+", "__")}.{proxyDefinitionType.FullName.Replace(".", "_").Replace("+", "__")}_{++_typeCount}";

                // Create Type
                TypeBuilder proxyTypeBuilder = moduleBuilder.DefineType(
                    proxyTypeName,
                    typeAttributes,
                    parentType,
                    interfaceTypes);

                // Create IDuckType and IDuckTypeSetter implementations
                FieldInfo instanceField = CreateIDuckTypeImplementation(proxyTypeBuilder, targetType);

                // Define .ctor to store the instance field
                ConstructorBuilder ctorBuilder = proxyTypeBuilder.DefineConstructor(
                    MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
                    CallingConventions.Standard,
                    new[] { instanceField.FieldType });
                ILGenerator ctorIL = ctorBuilder.GetILGenerator();
                ctorIL.Emit(OpCodes.Ldarg_0);
                ctorIL.Emit(OpCodes.Ldarg_1);
                ctorIL.Emit(OpCodes.Stfld, instanceField);
                ctorIL.Emit(OpCodes.Ret);

                if (proxyDefinitionType.IsValueType)
                {
                    // Create Fields and Properties from the struct information
                    CreatePropertiesFromStruct(proxyTypeBuilder, proxyDefinitionType, targetType, instanceField);

                    // Create Type
                    Type proxyType = proxyTypeBuilder.CreateTypeInfo().AsType();
                    return new CreateTypeResult(proxyDefinitionType, proxyType, targetType, CreateStructCopyMethod(moduleBuilder, proxyDefinitionType, proxyType, targetType), null);
                }
                else
                {
                    // Create Fields and Properties
                    CreateProperties(proxyTypeBuilder, proxyDefinitionType, targetType, instanceField);

                    // Create Methods
                    CreateMethods(proxyTypeBuilder, proxyDefinitionType, targetType, instanceField);

                    // Create Type
                    Type proxyType = proxyTypeBuilder.CreateTypeInfo().AsType();
                    return new CreateTypeResult(proxyDefinitionType, proxyType, targetType, GetCreateProxyInstanceDelegate(moduleBuilder, proxyDefinitionType, proxyType, targetType), null);
                }
            }
            catch (Exception ex)
            {
                return new CreateTypeResult(proxyDefinitionType, null, targetType, null, ExceptionDispatchInfo.Capture(ex));
            }
        }
    }

    private static FieldInfo CreateIDuckTypeImplementation(TypeBuilder proxyTypeBuilder, Type targetType)
    {
        Type instanceType = targetType;
        if (!UseDirectAccessTo(proxyTypeBuilder, targetType))
        {
            instanceType = typeof(object);
        }

        FieldBuilder instanceField = proxyTypeBuilder.DefineField("_currentInstance", instanceType, FieldAttributes.Private | FieldAttributes.InitOnly);

        PropertyBuilder propInstance = proxyTypeBuilder.DefineProperty("Instance", PropertyAttributes.None, typeof(object), null);
        MethodBuilder getPropInstance = proxyTypeBuilder.DefineMethod(
            "get_Instance",
            MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot,
            typeof(object),
            Type.EmptyTypes);
        ILGenerator il = getPropInstance.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldfld, instanceField);
        if (instanceType.IsValueType)
        {
            il.Emit(OpCodes.Box, instanceType);
        }

        il.Emit(OpCodes.Ret);
        propInstance.SetGetMethod(getPropInstance);

        PropertyBuilder propType = proxyTypeBuilder.DefineProperty("Type", PropertyAttributes.None, typeof(Type), null);
        MethodBuilder getPropType = proxyTypeBuilder.DefineMethod(
            "get_Type",
            MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.NewSlot,
            typeof(Type),
            Type.EmptyTypes);
        il = getPropType.GetILGenerator();
        il.Emit(OpCodes.Ldtoken, targetType);
        il.EmitCall(OpCodes.Call, GetTypeFromHandleMethodInfo, null);
        il.Emit(OpCodes.Ret);
        propType.SetGetMethod(getPropType);

        return instanceField;
    }

    private static List<PropertyInfo> GetProperties(Type proxyDefinitionType)
    {
        List<PropertyInfo> selectedProperties = new List<PropertyInfo>(proxyDefinitionType.IsInterface ? proxyDefinitionType.GetProperties() : GetBaseProperties(proxyDefinitionType));
        Type[] implementedInterfaces = proxyDefinitionType.GetInterfaces();
        foreach (Type imInterface in implementedInterfaces)
        {
            if (imInterface == typeof(IDuckType))
            {
                continue;
            }

            IEnumerable<PropertyInfo> newProps = imInterface.GetProperties().Where(p => selectedProperties.All(i => i.Name != p.Name));
            selectedProperties.AddRange(newProps);
        }

        return selectedProperties;

        static IEnumerable<PropertyInfo> GetBaseProperties(Type baseType)
        {
            foreach (PropertyInfo prop in baseType.GetProperties())
            {
                if (prop.CanRead && (prop.GetMethod.IsAbstract || prop.GetMethod.IsVirtual))
                {
                    yield return prop;
                }
                else if (prop.CanWrite && (prop.SetMethod.IsAbstract || prop.SetMethod.IsVirtual))
                {
                    yield return prop;
                }
            }
        }
    }

    private static void CreateProperties(TypeBuilder proxyTypeBuilder, Type proxyDefinitionType, Type targetType, FieldInfo instanceField)
    {
        // Gets all properties to be implemented
        List<PropertyInfo> proxyTypeProperties = GetProperties(proxyDefinitionType);

        foreach (PropertyInfo proxyProperty in proxyTypeProperties)
        {
            // Ignore the properties marked with `DuckIgnore` attribute
            if (proxyProperty.GetCustomAttribute<DuckIgnoreAttribute>(true) is not null)
            {
                continue;
            }

            PropertyBuilder propertyBuilder = null;

            // If the property is abstract or interface we make sure that we have the property defined in the new class
            if ((proxyProperty.CanRead && proxyProperty.GetMethod.IsAbstract) || (proxyProperty.CanWrite && proxyProperty.SetMethod.IsAbstract))
            {
                propertyBuilder = proxyTypeBuilder.DefineProperty(proxyProperty.Name, PropertyAttributes.None, proxyProperty.PropertyType, null);
            }

            DuckAttribute duckAttribute = proxyProperty.GetCustomAttribute<DuckAttribute>(true) ?? new DuckAttribute();
            duckAttribute.Name ??= proxyProperty.Name;

            switch (duckAttribute.Kind)
            {
                case DuckKind.Property:
                    PropertyInfo targetProperty = null;
                    try
                    {
                        targetProperty = targetType.GetProperty(duckAttribute.Name, duckAttribute.BindingFlags);
                    }
                    catch
                    {
                        // This will run only when multiple indexers are defined in a class, that way we can end up with multiple properties with the same name.
                        // In this case we make sure we select the indexer we want
                        targetProperty = targetType.GetProperty(duckAttribute.Name, proxyProperty.PropertyType, proxyProperty.GetIndexParameters().Select(i => i.ParameterType).ToArray());
                    }

                    if (targetProperty is null)
                    {
                        break;
                    }

                    propertyBuilder ??= proxyTypeBuilder.DefineProperty(proxyProperty.Name, PropertyAttributes.None, proxyProperty.PropertyType, null);

                    if (proxyProperty.CanRead)
                    {
                        // Check if the target property can be read
                        if (!targetProperty.CanRead)
                        {
                            DuckTypePropertyCantBeReadException.Throw(targetProperty);
                        }

                        propertyBuilder.SetGetMethod(GetPropertyGetMethod(proxyTypeBuilder, targetType, proxyProperty, targetProperty, instanceField));
                    }

                    if (proxyProperty.CanWrite)
                    {
                        // Check if the target property can be written
                        if (!targetProperty.CanWrite)
                        {
                            DuckTypePropertyCantBeWrittenException.Throw(targetProperty);
                        }

                        // Check if the target property declaring type is an struct (structs modification is not supported)
                        if (targetProperty.DeclaringType.IsValueType)
                        {
                            DuckTypeStructMembersCannotBeChangedException.Throw(targetProperty.DeclaringType);
                        }

                        propertyBuilder.SetSetMethod(GetPropertySetMethod(proxyTypeBuilder, targetType, proxyProperty, targetProperty, instanceField));
                    }

                    break;

                case DuckKind.Field:
                    FieldInfo targetField = targetType.GetField(duckAttribute.Name, duckAttribute.BindingFlags);
                    if (targetField is null)
                    {
                        break;
                    }

                    propertyBuilder ??= proxyTypeBuilder.DefineProperty(proxyProperty.Name, PropertyAttributes.None, proxyProperty.PropertyType, null);

                    if (proxyProperty.CanRead)
                    {
                        propertyBuilder.SetGetMethod(GetFieldGetMethod(proxyTypeBuilder, targetType, proxyProperty, targetField, instanceField));
                    }

                    if (proxyProperty.CanWrite)
                    {
                        // Check if the target field is marked as InitOnly (readonly) and throw an exception in that case
                        if ((targetField.Attributes & FieldAttributes.InitOnly) != 0)
                        {
                            DuckTypeFieldIsReadonlyException.Throw(targetField);
                        }

                        // Check if the target field declaring type is an struct (structs modification is not supported)
                        if (targetField.DeclaringType.IsValueType)
                        {
                            DuckTypeStructMembersCannotBeChangedException.Throw(targetField.DeclaringType);
                        }

                        propertyBuilder.SetSetMethod(GetFieldSetMethod(proxyTypeBuilder, targetType, proxyProperty, targetField, instanceField));
                    }

                    break;
            }

            if (propertyBuilder is null)
            {
                continue;
            }

            if (proxyProperty.CanRead && propertyBuilder.GetMethod is null)
            {
                DuckTypePropertyOrFieldNotFoundException.Throw(proxyProperty.Name, duckAttribute.Name);
            }

            if (proxyProperty.CanWrite && propertyBuilder.SetMethod is null)
            {
                DuckTypePropertyOrFieldNotFoundException.Throw(proxyProperty.Name, duckAttribute.Name);
            }
        }
    }

    private static void CreatePropertiesFromStruct(TypeBuilder proxyTypeBuilder, Type proxyDefinitionType, Type targetType, FieldInfo instanceField)
    {
        // Gets all fields to be copied
        foreach (FieldInfo proxyFieldInfo in proxyDefinitionType.GetFields())
        {
            // Skip readonly fields
            if ((proxyFieldInfo.Attributes & FieldAttributes.InitOnly) != 0)
            {
                continue;
            }

            // Ignore the fields marked with `DuckIgnore` attribute
            if (proxyFieldInfo.GetCustomAttribute<DuckIgnoreAttribute>(true) is not null)
            {
                continue;
            }

            PropertyBuilder propertyBuilder = null;

            DuckAttribute duckAttribute = proxyFieldInfo.GetCustomAttribute<DuckAttribute>(true) ?? new DuckAttribute();
            duckAttribute.Name ??= proxyFieldInfo.Name;

            switch (duckAttribute.Kind)
            {
                case DuckKind.Property:
                    PropertyInfo targetProperty = targetType.GetProperty(duckAttribute.Name, duckAttribute.BindingFlags);
                    if (targetProperty is null)
                    {
                        break;
                    }

                    // Check if the target property can be read
                    if (!targetProperty.CanRead)
                    {
                        DuckTypePropertyCantBeReadException.Throw(targetProperty);
                    }

                    propertyBuilder = proxyTypeBuilder.DefineProperty(proxyFieldInfo.Name, PropertyAttributes.None, proxyFieldInfo.FieldType, null);
                    propertyBuilder.SetGetMethod(GetPropertyGetMethod(proxyTypeBuilder, targetType, proxyFieldInfo, targetProperty, instanceField));
                    break;

                case DuckKind.Field:
                    FieldInfo targetField = targetType.GetField(duckAttribute.Name, duckAttribute.BindingFlags);
                    if (targetField is null)
                    {
                        break;
                    }

                    propertyBuilder = proxyTypeBuilder.DefineProperty(proxyFieldInfo.Name, PropertyAttributes.None, proxyFieldInfo.FieldType, null);
                    propertyBuilder.SetGetMethod(GetFieldGetMethod(proxyTypeBuilder, targetType, proxyFieldInfo, targetField, instanceField));
                    break;
            }

            if (propertyBuilder is null)
            {
                DuckTypePropertyOrFieldNotFoundException.Throw(proxyFieldInfo.Name, duckAttribute.Name);
            }
        }
    }

    private static Delegate GetCreateProxyInstanceDelegate(ModuleBuilder moduleBuilder, Type proxyDefinitionType, Type proxyType, Type targetType)
    {
        ConstructorInfo ctor = proxyType.GetConstructors()[0];

        DynamicMethod createProxyMethod = new DynamicMethod(
            $"CreateProxyInstance<{proxyType.Name}>",
            proxyDefinitionType,
            new[] { typeof(object) },
            typeof(DuckType).Module,
            true);
        ILGenerator il = createProxyMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        if (UseDirectAccessTo(moduleBuilder, targetType))
        {
            if (targetType.IsValueType)
            {
                il.Emit(OpCodes.Unbox_Any, targetType);
            }
            else
            {
                il.Emit(OpCodes.Castclass, targetType);
            }
        }

        il.Emit(OpCodes.Newobj, ctor);

        if (proxyType.IsValueType)
        {
            il.Emit(OpCodes.Box, proxyType);
        }

        il.Emit(OpCodes.Ret);
        Type delegateType = typeof(CreateProxyInstance<>).MakeGenericType(proxyDefinitionType);
        return createProxyMethod.CreateDelegate(delegateType);
    }

    private static Delegate CreateStructCopyMethod(ModuleBuilder moduleBuilder, Type proxyDefinitionType, Type proxyType, Type targetType)
    {
        ConstructorInfo ctor = proxyType.GetConstructors()[0];

        DynamicMethod createStructMethod = new DynamicMethod(
            $"CreateStructInstance<{proxyType.Name}>",
            proxyDefinitionType,
            new[] { typeof(object) },
            typeof(DuckType).Module,
            true);
        ILGenerator il = createStructMethod.GetILGenerator();

        // First we declare the locals
        LocalBuilder proxyLocal = il.DeclareLocal(proxyType);
        LocalBuilder structLocal = il.DeclareLocal(proxyDefinitionType);

        // We create an instance of the proxy type
        il.Emit(OpCodes.Ldloca_S, proxyLocal.LocalIndex);
        il.Emit(OpCodes.Ldarg_0);
        if (UseDirectAccessTo(moduleBuilder, targetType))
        {
            if (targetType.IsValueType)
            {
                il.Emit(OpCodes.Unbox_Any, targetType);
            }
            else
            {
                il.Emit(OpCodes.Castclass, targetType);
            }
        }

        il.Emit(OpCodes.Call, ctor);

        // Create the destination structure
        il.Emit(OpCodes.Ldloca_S, structLocal.LocalIndex);
        il.Emit(OpCodes.Initobj, proxyDefinitionType);

        // Start copy properties from the proxy to the structure
        foreach (FieldInfo finfo in proxyDefinitionType.GetFields())
        {
            // Skip readonly fields
            if ((finfo.Attributes & FieldAttributes.InitOnly) != 0)
            {
                continue;
            }

            // Ignore the fields marked with `DuckIgnore` attribute
            if (finfo.GetCustomAttribute<DuckIgnoreAttribute>(true) is not null)
            {
                continue;
            }

            PropertyInfo prop = proxyType.GetProperty(finfo.Name);
            il.Emit(OpCodes.Ldloca_S, structLocal.LocalIndex);
            il.Emit(OpCodes.Ldloca_S, proxyLocal.LocalIndex);
            il.EmitCall(OpCodes.Call, prop.GetMethod, null);
            il.Emit(OpCodes.Stfld, finfo);
        }

        // Return
        il.WriteLoadLocal(structLocal.LocalIndex);
        il.Emit(OpCodes.Ret);

        Type delegateType = typeof(CreateProxyInstance<>).MakeGenericType(proxyDefinitionType);
        return createStructMethod.CreateDelegate(delegateType);
    }

    /// <summary>
    /// Struct to store the result of creating a proxy type
    /// </summary>
    public readonly struct CreateTypeResult
    {
        /// <summary>
        /// Gets if the proxy type creation was successful
        /// </summary>
        public readonly bool Success;

        /// <summary>
        /// Target type
        /// </summary>
        public readonly Type TargetType;

        private readonly Type _proxyType;
        private readonly ExceptionDispatchInfo _exceptionInfo;
        private readonly Delegate _activator;

        /// <summary>
        /// Initializes a new instance of the <see cref="CreateTypeResult"/> struct.
        /// </summary>
        /// <param name="proxyTypeDefinition">Proxy type definition</param>
        /// <param name="proxyType">Proxy type</param>
        /// <param name="targetType">Target type</param>
        /// <param name="activator">Proxy activator</param>
        /// <param name="exceptionInfo">Exception dispatch info instance</param>
        internal CreateTypeResult(Type proxyTypeDefinition, Type proxyType, Type targetType, Delegate activator, ExceptionDispatchInfo exceptionInfo)
        {
            TargetType = targetType;
            _proxyType = proxyType;
            _activator = activator;
            _exceptionInfo = exceptionInfo;
            Success = proxyType != null && exceptionInfo == null;
            if (exceptionInfo != null)
            {
                MethodInfo methodInfo = typeof(CreateTypeResult).GetMethod(nameof(ThrowOnError), BindingFlags.NonPublic | BindingFlags.Instance);
                _activator = methodInfo
                    .MakeGenericMethod(proxyTypeDefinition)
                    .CreateDelegate(
                        typeof(CreateProxyInstance<>).MakeGenericType(proxyTypeDefinition),
                        this);
            }
        }

        /// <summary>
        /// Gets the Proxy type
        /// </summary>
        public Type ProxyType
        {
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            get
            {
                _exceptionInfo?.Throw();
                return _proxyType;
            }
        }

        /// <summary>
        /// Create a new proxy instance from a target instance
        /// </summary>
        /// <typeparam name="T">Type of the return value</typeparam>
        /// <param name="instance">Target instance value</param>
        /// <returns>Proxy instance</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public T CreateInstance<T>(object instance)
        {
            return ((CreateProxyInstance<T>)_activator)(instance);
        }

        /// <summary>
        /// Get if the proxy instance can be created
        /// </summary>
        /// <returns>true if the proxy can be created; otherwise, false.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool CanCreate()
        {
            return _exceptionInfo == null;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal IDuckType CreateInstance(object instance)
        {
            return (IDuckType)_activator.DynamicInvoke(instance);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private T ThrowOnError<T>(object instance)
        {
            _exceptionInfo.Throw();
            return default;
        }
    }

    /// <summary>
    /// Generics Create Cache FastPath
    /// </summary>
    /// <typeparam name="T">Type of proxy definition</typeparam>
    public static class CreateCache<T>
    {
        /// <summary>
        /// Gets the type of T
        /// </summary>
        public static readonly Type Type = typeof(T);

        /// <summary>
        /// Gets if the T type is visible
        /// </summary>
        public static readonly bool IsVisible = Type.IsPublic || Type.IsNestedPublic;

        private static CreateTypeResult _fastPath = default;

        /// <summary>
        /// Gets the proxy type for a target type using the T proxy definition
        /// </summary>
        /// <param name="targetType">Target type</param>
        /// <returns>CreateTypeResult instance</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static CreateTypeResult GetProxy(Type targetType)
        {
            // We set a fast path for the first proxy type for a proxy definition. (It's likely to have a proxy definition just for one target type)
            CreateTypeResult fastPath = _fastPath;
            if (fastPath.TargetType == targetType)
            {
                return fastPath;
            }

            CreateTypeResult result = GetProxySlow(targetType);

            fastPath = _fastPath;
            if (fastPath.TargetType is null)
            {
                _fastPath = result;
            }

            return result;
        }

        /// <summary>
        /// Create a new instance of a proxy type for a target instance using the T proxy definition
        /// </summary>
        /// <param name="instance">Object instance</param>
        /// <returns>Proxy instance</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static T Create(object instance)
        {
            if (instance is null)
            {
                return default;
            }

            return GetProxy(instance.GetType()).CreateInstance<T>(instance);
        }

        /// <summary>
        /// Get if the proxy instance can be created
        /// </summary>
        /// <param name="instance">Object instance</param>
        /// <returns>true if a proxy can be created; otherwise, false.</returns>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool CanCreate(object instance)
        {
            if (instance is null)
            {
                return false;
            }

            return GetProxy(instance.GetType()).CanCreate();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static CreateTypeResult GetProxySlow(Type targetType)
        {
#if NET45
                if (!Type.IsValueType && !IsVisible)
                {
                    DuckTypeTypeIsNotPublicException.Throw(Type, nameof(Type));
                }
#endif
            return GetOrCreateProxyType(Type, targetType);
        }
    }
}
