// 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.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.CSharp.UseIndexOrRangeOperator
{
    using static Helpers;

    internal partial class CSharpUseRangeOperatorDiagnosticAnalyzer
    {
        /// <summary>
        /// Helper type to cache information about types while analyzing the compilation.
        /// </summary>
        public class InfoCache
        {
            /// <summary>
            /// The <see cref="T:System.Range"/> type.  Needed so that we only fixup code if we see the type
            /// we're using has an indexer that takes a <see cref="T:System.Range"/>.
            /// </summary>
            [SuppressMessage("Documentation", "CA1200:Avoid using cref tags with a prefix", Justification = "Required to avoid ambiguous reference warnings.")]
            public readonly INamedTypeSymbol RangeType;
            public readonly INamedTypeSymbol? ExpressionOfTType;

            private readonly ConcurrentDictionary<IMethodSymbol, MemberInfo> _methodToMemberInfo = new();

            private InfoCache(INamedTypeSymbol rangeType, INamedTypeSymbol stringType, INamedTypeSymbol? expressionOfTType)
            {
                RangeType = rangeType;
                ExpressionOfTType = expressionOfTType;

                // Always allow using System.Range indexers with System.String.Substring.  The
                // compiler has hard-coded knowledge on how to use this type, even if there is no
                // this[Range] indexer declared on it directly.
                //
                // Ensure that we can actually get the 'string' type. We may fail if there is no
                // proper mscorlib reference (for example, while a project is loading).
                if (!stringType.IsErrorType())
                {
                    var substringMethod = stringType.GetMembers(nameof(string.Substring))
                                                    .OfType<IMethodSymbol>()
                                                    .FirstOrDefault(m => IsTwoArgumentSliceLikeMethod(m));

                    if (substringMethod is not null)
                        _methodToMemberInfo[substringMethod] = ComputeMemberInfo(substringMethod, requireRangeMember: false);
                }
            }

            public static bool TryCreate(Compilation compilation, [NotNullWhen(true)] out InfoCache? infoCache)
            {
                var rangeType = compilation.GetBestTypeByMetadataName(typeof(Range).FullName!);
                if (rangeType == null || !rangeType.IsAccessibleWithin(compilation.Assembly))
                {
                    infoCache = null;
                    return false;
                }

                var stringType = compilation.GetSpecialType(SpecialType.System_String);
                infoCache = new InfoCache(rangeType, stringType, compilation.ExpressionOfTType());
                return true;
            }

            private static IMethodSymbol? GetSliceLikeMethod(INamedTypeSymbol namedType)
                => namedType.GetMembers()
                            .OfType<IMethodSymbol>()
                            .Where(m => IsTwoArgumentSliceLikeMethod(m))
                            .FirstOrDefault();

            public bool TryGetMemberInfo(IMethodSymbol method, out MemberInfo memberInfo)
            {
                if (!IsTwoArgumentSliceLikeMethod(method))
                {
                    memberInfo = default;
                    return false;
                }

                memberInfo = _methodToMemberInfo.GetOrAdd(method, m => ComputeMemberInfo(m, requireRangeMember: true));
                return memberInfo.LengthLikeProperty != null;
            }

            public bool TryGetMemberInfoOneArgument(IMethodSymbol method, out MemberInfo memberInfo)
            {
                if (!IsOneArgumentSliceLikeMethod(method))
                {
                    memberInfo = default;
                    return false;
                }

                if (!_methodToMemberInfo.TryGetValue(method, out memberInfo))
                {
                    // Find overload of our method that is a slice-like method with two arguments.
                    // Computing member info for this method will also check that the containing type
                    // has an int32 'Length' or 'Count' property, and has a suitable indexer,
                    // so we don't have to.
                    var overloadWithTwoArguments = method.ContainingType
                        .GetMembers(method.Name)
                        .OfType<IMethodSymbol>()
                        .FirstOrDefault(s => IsTwoArgumentSliceLikeMethod(s));
                    if (overloadWithTwoArguments is null)
                    {
                        memberInfo = default;
                        return false;
                    }

                    // Since the search is expensive, we keep both the original one-argument and
                    // two-arguments overload as keys in the cache, pointing to the same
                    // member information object.
                    var newMemberInfo = _methodToMemberInfo.GetOrAdd(overloadWithTwoArguments, _ => ComputeMemberInfo(overloadWithTwoArguments, requireRangeMember: true));
                    _methodToMemberInfo.GetOrAdd(method, _ => newMemberInfo);
                    memberInfo = newMemberInfo;
                }

                return memberInfo.LengthLikeProperty != null;
            }

            private MemberInfo ComputeMemberInfo(IMethodSymbol sliceLikeMethod, bool requireRangeMember)
            {
                Debug.Assert(IsTwoArgumentSliceLikeMethod(sliceLikeMethod));

                // Check that the type has an int32 'Length' or 'Count' property. If not, we don't
                // consider it something indexable.
                var containingType = sliceLikeMethod.ContainingType;
                var lengthLikeProperty = TryGetLengthOrCountProperty(containingType);
                if (lengthLikeProperty == null)
                {
                    return default;
                }

                if (!requireRangeMember)
                {
                    return new MemberInfo(lengthLikeProperty, overloadedMethodOpt: null);
                }

                // A Slice method can either be paired with an Range-taking indexer on the type, or
                // an Range-taking overload, or an explicit method called .Slice that takes two ints:
                //
                // https://github.com/dotnet/csharplang/blob/main/proposals/csharp-8.0/ranges.md#implicit-range-support
                if (sliceLikeMethod.ReturnType.Equals(containingType))
                {
                    // it's a method like:  MyType MyType.Get(int start, int length).  Look for an
                    // indexer like  `MyType MyType.this[Range range]`.
                    var indexer = GetIndexer(containingType, RangeType, containingType);
                    if (indexer != null)
                    {
                        return new MemberInfo(lengthLikeProperty, overloadedMethodOpt: null);
                    }

                    // Also, look to see if the type has a `.Slice(int start, int length)` method.
                    // This is also a method the compiler knows to look for when a user writes `x[a..b]`
                    var actualSliceMethod =
                        sliceLikeMethod.ContainingType.GetMembers(nameof(Span<int>.Slice))
                                                      .OfType<IMethodSymbol>()
                                                      .FirstOrDefault(s => IsTwoArgumentSliceLikeMethod(s));
                    if (actualSliceMethod != null)
                    {
                        return new MemberInfo(lengthLikeProperty, overloadedMethodOpt: null);
                    }
                }

                // it's a method like:   `SomeType MyType.Get(int start, int length)`.  Look 
                // for an overload like: `SomeType MyType.Get(Range)`
                var overloadedRangeMethod = GetOverload(sliceLikeMethod, RangeType);
                if (overloadedRangeMethod != null)
                {
                    return new MemberInfo(lengthLikeProperty, overloadedRangeMethod);
                }

                // A slice-like method that we can't convert.
                return default;
            }
        }
    }
}
