﻿// 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.

// NOTE: This code is derived from an implementation originally in dotnet/runtime:
// https://github.com/dotnet/runtime/blob/v5.0.2/src/libraries/Common/tests/System/Diagnostics/DebuggerAttributes.cs
//
// See the commentary in https://github.com/dotnet/roslyn/pull/50156 for notes on incorporating changes made to the
// reference implementation.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.UnitTests.Collections
{
    internal class DebuggerAttributeInfo
    {
        public object Instance { get; }
        public IEnumerable<PropertyInfo> Properties { get; }

        public DebuggerAttributeInfo(object instance, IEnumerable<PropertyInfo> properties)
        {
            Instance = instance;
            Properties = properties;
        }
    }

    internal static class DebuggerAttributes
    {
        internal static object? GetFieldValue(object obj, string fieldName)
        {
            var fieldInfo = GetField(obj, fieldName) ?? throw new InvalidOperationException();
            return fieldInfo.GetValue(obj);
        }

        internal static void InvokeDebuggerTypeProxyProperties(object obj)
        {
            DebuggerAttributeInfo info = ValidateDebuggerTypeProxyProperties(obj.GetType(), obj);
            foreach (PropertyInfo pi in info.Properties)
            {
                pi.GetValue(info.Instance, null);
            }
        }

        internal static DebuggerAttributeInfo ValidateDebuggerTypeProxyProperties(object obj)
        {
            return ValidateDebuggerTypeProxyProperties(obj.GetType(), obj);
        }

        internal static DebuggerAttributeInfo ValidateDebuggerTypeProxyProperties(Type type, object obj)
        {
            return ValidateDebuggerTypeProxyProperties(type, type.GenericTypeArguments, obj);
        }

        internal static DebuggerAttributeInfo ValidateDebuggerTypeProxyProperties(Type type, Type[] genericTypeArguments, object obj)
        {
            Type proxyType = GetProxyType(type, genericTypeArguments);

            // Create an instance of the proxy type, and make sure we can access all of the instance properties
            // on the type without exception
            object proxyInstance = Activator.CreateInstance(proxyType, obj) ?? throw ExceptionUtilities.Unreachable();
            IEnumerable<PropertyInfo> properties = GetDebuggerVisibleProperties(proxyType);
            return new DebuggerAttributeInfo(proxyInstance, properties);
        }

        public static DebuggerBrowsableState? GetDebuggerBrowsableState(MemberInfo info)
        {
            CustomAttributeData? debuggerBrowsableAttribute = info.CustomAttributes
                .SingleOrDefault(a => a.AttributeType == typeof(DebuggerBrowsableAttribute));
            // Enums in attribute constructors are boxed as ints, so cast to int? first.
            return (DebuggerBrowsableState?)(int?)debuggerBrowsableAttribute?.ConstructorArguments.Single().Value;
        }

        public static IEnumerable<FieldInfo> GetDebuggerVisibleFields(Type debuggerAttributeType)
        {
            // The debugger doesn't evaluate non-public members of type proxies.
            IEnumerable<FieldInfo> visibleFields = debuggerAttributeType.GetFields()
                .Where(fi => fi.IsPublic && GetDebuggerBrowsableState(fi) != DebuggerBrowsableState.Never);
            return visibleFields;
        }

        public static IEnumerable<PropertyInfo> GetDebuggerVisibleProperties(Type debuggerAttributeType)
        {
            // The debugger doesn't evaluate non-public members of type proxies. GetGetMethod returns null if the getter is non-public.
            IEnumerable<PropertyInfo> visibleProperties = debuggerAttributeType.GetProperties()
                .Where(pi => pi.GetGetMethod() != null && GetDebuggerBrowsableState(pi) != DebuggerBrowsableState.Never);
            return visibleProperties;
        }

        public static object? GetProxyObject(object obj) => Activator.CreateInstance(GetProxyType(obj), obj);

        public static Type GetProxyType(object obj) => GetProxyType(obj.GetType());

        public static Type GetProxyType(Type type) => GetProxyType(type, type.GenericTypeArguments);

        private static Type GetProxyType(Type type, Type[] genericTypeArguments)
        {
            // Get the DebuggerTypeProxyAttribute for obj
            var attrs =
                type.GetTypeInfo().CustomAttributes
                .Where(a => a.AttributeType == typeof(DebuggerTypeProxyAttribute))
                .ToArray();
            if (attrs.Length != 1)
            {
                throw new InvalidOperationException($"Expected one DebuggerTypeProxyAttribute on {type}.");
            }
            CustomAttributeData cad = attrs[0];

            Type? proxyType = cad.ConstructorArguments[0].ArgumentType == typeof(Type) ?
                (Type?)cad.ConstructorArguments[0].Value :
                Type.GetType((string)cad.ConstructorArguments[0].Value!);
            if (proxyType is null)
                throw new InvalidOperationException("Expected a non-null proxy type");

            if (genericTypeArguments.Length > 0)
            {
                proxyType = proxyType.MakeGenericType(genericTypeArguments);
            }

            return proxyType;
        }

        internal static string ValidateDebuggerDisplayReferences(object obj)
        {
            // Get the DebuggerDisplayAttribute for obj
            var objType = obj.GetType();
            var attrs =
                objType.GetTypeInfo().CustomAttributes
                .Where(a => a.AttributeType == typeof(DebuggerDisplayAttribute))
                .ToArray();
            if (attrs.Length != 1)
            {
                throw new InvalidOperationException($"Expected one DebuggerDisplayAttribute on {objType}.");
            }
            var cad = attrs[0];

            // Get the text of the DebuggerDisplayAttribute
            string attrText = (string?)cad.ConstructorArguments[0].Value ?? throw new InvalidOperationException("Expected a non-null text");

            var segments = attrText.Split(new[] { '{', '}' });

            if (segments.Length % 2 == 0)
            {
                throw new InvalidOperationException($"The DebuggerDisplayAttribute for {objType} lacks a closing brace.");
            }

            if (segments.Length == 1)
            {
                throw new InvalidOperationException($"The DebuggerDisplayAttribute for {objType} doesn't reference any expressions.");
            }

            var sb = new StringBuilder();

            for (int i = 0; i < segments.Length; i += 2)
            {
                string literal = segments[i];
                sb.Append(literal);

                if (i + 1 < segments.Length)
                {
                    string reference = segments[i + 1];
                    bool noQuotes = reference.EndsWith(",nq");

                    reference = reference.Replace(",nq", string.Empty);

                    // Evaluate the reference.
                    if (!TryEvaluateReference(obj, reference, out object? member))
                    {
                        throw new InvalidOperationException($"The DebuggerDisplayAttribute for {objType} contains the expression \"{reference}\".");
                    }

                    string? memberString = GetDebuggerMemberString(member, noQuotes);

                    sb.Append(memberString);
                }
            }

            return sb.ToString();
        }

        private static string? GetDebuggerMemberString(object? member, bool noQuotes)
        {
            string? memberString = "null";
            if (member != null)
            {
                memberString = member.ToString();
                if (member is string)
                {
                    if (!noQuotes)
                    {
                        memberString = '"' + memberString + '"';
                    }
                }
                else if (!IsPrimitiveType(member))
                {
                    memberString = '{' + memberString + '}';
                }
            }

            return memberString;
        }

        private static bool IsPrimitiveType(object obj) =>
            obj is byte || obj is sbyte ||
            obj is short || obj is ushort ||
            obj is int || obj is uint ||
            obj is long || obj is ulong ||
            obj is float || obj is double;

        private static bool TryEvaluateReference(object obj, string reference, out object? member)
        {
            PropertyInfo? pi = GetProperty(obj, reference);
            if (pi != null)
            {
                member = pi.GetValue(obj);
                return true;
            }

            FieldInfo? fi = GetField(obj, reference);
            if (fi != null)
            {
                member = fi.GetValue(obj);
                return true;
            }

            member = null;
            return false;
        }

        private static FieldInfo? GetField(object obj, string fieldName)
        {
            for (Type? t = obj.GetType(); t != null; t = t.GetTypeInfo().BaseType)
            {
                FieldInfo? fi = t.GetTypeInfo().GetDeclaredField(fieldName);
                if (fi != null)
                {
                    return fi;
                }
            }
            return null;
        }

        private static PropertyInfo? GetProperty(object obj, string propertyName)
        {
            for (Type? t = obj.GetType(); t != null; t = t.GetTypeInfo().BaseType)
            {
                PropertyInfo? pi = t.GetTypeInfo().GetDeclaredProperty(propertyName);
                if (pi != null)
                {
                    return pi;
                }
            }
            return null;
        }
    }
}
