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

#nullable disable

using System;
using System.Collections.Generic;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Shared.Extensions;

namespace Microsoft.CodeAnalysis.CSharp.Debugging;

/// <summary>
/// Given a position in a source file, returns the expressions in close proximity that should
/// show up in the debugger 'autos' window.  In general, the expressions we place into the autos
/// window are things that appear to be 'side effect free'.  Note: because we only use the syntax
/// tree for this, it's possible for us to get this wrong.  However, this should only happen in
/// code that behaves unexpectedly.  For example, we will assume that "a + b" is side effect free
/// (when in practice it may not be).  
/// 
/// The general tactic we take is to add the expressions for the statements on the
/// line the debugger is currently at.  We will also try to find the 'previous' statement as well
/// to add the expressions from that.  The 'previous' statement is a bit of an interesting beast.
/// Consider, for example, if the user has just jumped out of a switch and is the statement
/// directly following it.  What is the previous statement?  Without keeping state, there's no way
/// to know.  So, in this case, we treat all 'exit points' (i.e. the last statement of a switch
/// section) of the switch statement as the 'previous statement'.  There are many cases like this
/// we need to handle.  Basically anything that might have nested statements/blocks might
/// contribute to the 'previous statement'
/// </summary>
[ExportLanguageService(typeof(IProximityExpressionsService), LanguageNames.CSharp), Shared]
internal sealed partial class CSharpProximityExpressionsService : IProximityExpressionsService
{
    [ImportingConstructor]
    [SuppressMessage("RoslynDiagnosticsReliability", "RS0033:Importing constructor should be [Obsolete]", Justification = "Used in test code: https://github.com/dotnet/roslyn/issues/42814")]
    public CSharpProximityExpressionsService()
    {
    }

    public async Task<bool> IsValidAsync(
        Document document,
        int position,
        string expressionValue,
        CancellationToken cancellationToken)
    {
        var expression = SyntaxFactory.ParseExpression(expressionValue);
        var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var token = root.FindToken(position);
        if (token.Kind() == SyntaxKind.CloseBraceToken && token.GetPreviousToken().Kind() != SyntaxKind.None)
        {
            token = token.GetPreviousToken();
        }

        var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        var info = semanticModel.GetSpeculativeSymbolInfo(token.SpanStart, expression, SpeculativeBindingOption.BindAsExpression);
        if (info.Symbol == null)
        {
            return false;
        }

        // We seem to have bound successfully.  However, if it bound to a local, then make
        // sure that that local isn't after the statement that we're currently looking at.  
        if (info.Symbol.Kind == SymbolKind.Local)
        {
            var statement = info.Symbol.Locations.First().FindToken(cancellationToken).GetAncestor<StatementSyntax>();
            if (statement != null && position < statement.SpanStart)
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Returns null indicating a failure.
    /// </summary>
    public async Task<IList<string>> GetProximityExpressionsAsync(
        Document document,
        int position,
        CancellationToken cancellationToken)
    {
        try
        {
            var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
            return GetProximityExpressions(tree, position, cancellationToken);
        }
        catch (Exception e) when (FatalError.ReportAndCatchUnlessCanceled(e, cancellationToken))
        {
            return null;
        }
    }

    public static IList<string> GetProximityExpressions(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken)
        => new Worker(syntaxTree, position).Do(cancellationToken);

    private static void AddRelevantExpressions(
        StatementSyntax statement,
        IList<string> expressions,
        bool includeDeclarations)
    {
        new RelevantExpressionsCollector(includeDeclarations, expressions).Visit(statement);
    }
}
