// 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.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.CSharp.Simplification;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.LanguageService;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.Extensions.ContextQuery;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Snippets;
using Microsoft.CodeAnalysis.Snippets.SnippetProviders;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Snippets;

using static SyntaxFactory;

internal abstract class AbstractCSharpForLoopSnippetProvider : AbstractForLoopSnippetProvider<ForStatementSyntax>
{
    private static readonly string[] s_iteratorBaseNames = ["i", "j", "k"];

    protected abstract SyntaxKind ConditionKind { get; }

    protected abstract SyntaxKind IncrementorKind { get; }

    protected abstract ExpressionSyntax GenerateInitializerValue(SyntaxGenerator generator, SyntaxNode? inlineExpression);

    protected abstract ExpressionSyntax GenerateRightSideOfCondition(SyntaxGenerator generator, SyntaxNode? inlineExpression);

    protected abstract void AddSpecificPlaceholders(MultiDictionary<string, int> placeholderBuilder, ExpressionSyntax initializer, ExpressionSyntax rightOfCondition);

    protected override bool CanInsertStatementAfterToken(SyntaxToken token)
        => token.IsBeginningOfStatementContext() || token.IsBeginningOfGlobalStatementContext();

    protected override ValueTask<ForStatementSyntax> AdjustSnippetExpressionAsync(
        Document document, ForStatementSyntax snippetExpressionNode, CancellationToken cancellationToken)
    {
        var editor = new SyntaxEditor(snippetExpressionNode, document.Project.Solution.Services);
        foreach (var node in snippetExpressionNode.Declaration!.DescendantNodesAndSelf().Reverse())
            editor.ReplaceNode(node, (node, _) => node.WithAdditionalAnnotations(Simplifier.Annotation));

        return new((ForStatementSyntax)editor.GetChangedRoot());
    }

    protected override ForStatementSyntax GenerateStatement(
        SyntaxGenerator generator, SyntaxContext syntaxContext, SimplifierOptions simplifierOptions, InlineExpressionInfo? inlineExpressionInfo)
    {
        var semanticModel = syntaxContext.SemanticModel;
        var compilation = semanticModel.Compilation;
        var csharpSimplifierOptions = (CSharpSimplifierOptions)simplifierOptions;

        var iteratorName = NameGenerator.GenerateUniqueName(s_iteratorBaseNames, n => semanticModel.LookupSymbols(syntaxContext.Position, name: n).IsEmpty);
        var iteratorVariable = generator.Identifier(iteratorName);
        var indexVariable = (ExpressionSyntax)generator.IdentifierName(iteratorName);
        var (iteratorType, inlineExpression) = GetLoopHeaderParts();

        var variableDeclaration = VariableDeclaration(
            iteratorType,
            variables: [VariableDeclarator(iteratorVariable,
                argumentList: null,
                EqualsValueClause(GenerateInitializerValue(generator, inlineExpression)))])
            .NormalizeWhitespace();

        return ForStatement(
            variableDeclaration,
            initializers: [],
            BinaryExpression(ConditionKind, indexVariable, GenerateRightSideOfCondition(generator, inlineExpression)),
            [PostfixUnaryExpression(IncrementorKind, indexVariable)],
            Block());

        (TypeSyntax iteratorType, SyntaxNode? inlineExpression) GetLoopHeaderParts()
        {
            var inlineExpression = inlineExpressionInfo?.Node.WithoutLeadingTrivia();

            if (inlineExpressionInfo is null)
            {
                // Explicit emit 'var' if the user has asked for "var for built-in types" option.  We do that as the
                // implicit 'allowVar' value passed to GenerateTypeSyntax doesn't work here as the inlineExpression will
                // be in error, and thus the simplifier will not approve converting 'int' to 'var' in that scenario.
                var iteratorType = csharpSimplifierOptions.VarForBuiltInTypes.Value
                    ? IdentifierName("var")
                    : compilation.GetSpecialType(SpecialType.System_Int32).GenerateTypeSyntax();
                return (iteratorType, inlineExpression);
            }

            var inlineExpressionType = inlineExpressionInfo.TypeInfo.Type;
            Debug.Assert(inlineExpressionType is not null);

            if (IsSuitableIntegerType(inlineExpressionType))
                return (inlineExpressionType.GenerateTypeSyntax(), inlineExpression);

            var property = FindLengthProperty(inlineExpressionType, compilation) ?? FindCountProperty(inlineExpressionType, compilation);
            Contract.ThrowIfNull(property);
            return (property.Type.GenerateTypeSyntax(), generator.MemberAccessExpression(inlineExpression, property.Name));
        }
    }

    protected override ValueTask<ImmutableArray<SnippetPlaceholder>> GetPlaceHolderLocationsListAsync(
        Document document, ForStatementSyntax forStatement, ISyntaxFacts syntaxFacts, CancellationToken cancellationToken)
    {
        using var _ = ArrayBuilder<SnippetPlaceholder>.GetInstance(out var result);
        var placeholderBuilder = new MultiDictionary<string, int>();
        var declaration = forStatement.Declaration;
        var condition = forStatement.Condition;
        var incrementor = forStatement.Incrementors.Single();

        var variableDeclarator = declaration!.Variables.Single();
        var declaratorIdentifier = variableDeclarator.Identifier;
        placeholderBuilder.Add(declaratorIdentifier.ValueText, declaratorIdentifier.SpanStart);

        var conditionExpression = (BinaryExpressionSyntax)condition!;
        var left = conditionExpression.Left;
        placeholderBuilder.Add(left.ToString(), left.SpanStart);

        AddSpecificPlaceholders(placeholderBuilder, variableDeclarator.Initializer!.Value, conditionExpression.Right);

        var operand = ((PostfixUnaryExpressionSyntax)incrementor!).Operand;
        placeholderBuilder.Add(operand.ToString(), operand.SpanStart);

        foreach (var (key, value) in placeholderBuilder)
            result.Add(new(key, [.. value]));

        return new(result.ToImmutableAndClear());
    }

    protected override int GetTargetCaretPosition(ForStatementSyntax forStatement, SourceText sourceText)
        => CSharpSnippetHelpers.GetTargetCaretPositionInBlock(
            forStatement,
            static s => (BlockSyntax)s.Statement,
            sourceText);

    protected override Task<Document> AddIndentationToDocumentAsync(Document document, ForStatementSyntax forStatement, CancellationToken cancellationToken)
        => CSharpSnippetHelpers.AddBlockIndentationToDocumentAsync(
            document,
            forStatement,
            static s => (BlockSyntax)s.Statement,
            cancellationToken);
}
