﻿// 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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.VisualStudio.LanguageServices.Implementation.CodeModel.MethodXml;

namespace Microsoft.VisualStudio.LanguageServices.CSharp.CodeModel.MethodXml
{
    internal class MethodXmlBuilder : AbstractMethodXmlBuilder
    {
        private MethodXmlBuilder(IMethodSymbol symbol, SemanticModel semanticModel)
            : base(symbol, semanticModel)
        {
        }

        private void GenerateBlock(BlockSyntax block)
        {
            using (BlockTag())
            {
                foreach (var statement in block.Statements)
                {
                    GenerateComments(statement.GetLeadingTrivia());
                    GenerateStatement(statement);
                }

                // Handle any additional comments now, but only comments within the extent of this block.
                GenerateComments(block.CloseBraceToken.LeadingTrivia);
            }
        }

        private void GenerateComments(SyntaxTriviaList triviaList)
        {
            foreach (var trivia in triviaList)
            {
                // Multi-line comment forms are ignored.
                if (trivia.Kind() == SyntaxKind.SingleLineCommentTrivia)
                {
                    // In order to be valid, the comment must appear on its own line.
                    var line = Text.Lines.GetLineFromPosition(trivia.SpanStart);
                    var firstNonWhitespacePosition = line.GetFirstNonWhitespacePosition() ?? -1;
                    if (firstNonWhitespacePosition == trivia.SpanStart)
                    {
                        using var tag = CommentTag();

                        // Skip initial slashes
                        var trimmedComment = trivia.ToString()[2..];
                        EncodedText(trimmedComment);
                    }
                }
            }
        }

        private void GenerateStatement(StatementSyntax statement)
        {
            var success = false;
            var mark = GetMark();

            switch (statement.Kind())
            {
                case SyntaxKind.LocalDeclarationStatement:
                    success = TryGenerateLocal((LocalDeclarationStatementSyntax)statement);
                    break;
                case SyntaxKind.Block:
                    success = true;
                    GenerateBlock((BlockSyntax)statement);
                    break;
                case SyntaxKind.ExpressionStatement:
                    success = TryGenerateExpressionStatement((ExpressionStatementSyntax)statement);
                    break;
            }

            if (!success)
            {
                Rewind(mark);
                GenerateUnknown(statement);
            }

            // Just for readability
            LineBreak();
        }

        private bool TryGenerateLocal(LocalDeclarationStatementSyntax localDeclarationStatement)
        {
            /*
              - <ElementType name="Local" content="eltOnly">
                    <attribute type="id" /> 
                    <attribute type="static" /> 
                    <attribute type="instance" /> 
                    <attribute type="implicit" /> 
                    <attribute type="constant" /> 
                - <group order="one">
                        <element type="Type" /> 
                        <element type="ArrayType" /> 
                    </group>
                - <group minOccurs="1" maxOccurs="*" order="seq">
                        <element type="LocalName" /> 
                        <element type="Expression" minOccurs="0" maxOccurs="1" /> 
                    </group>
                </ElementType>
            */

            using (LocalTag(GetLineNumber(localDeclarationStatement)))
            {
                // Spew the type first
                if (!TryGenerateType(localDeclarationStatement.Declaration.Type))
                {
                    return false;
                }

                // Now spew the list of variables
                foreach (var variable in localDeclarationStatement.Declaration.Variables)
                {
                    GenerateName(variable.Identifier.ToString());

                    if (variable.Initializer != null)
                    {
                        if (!TryGenerateExpression(variable.Initializer.Value))
                        {
                            return false;
                        }
                    }
                }
            }

            return true;
        }

        private bool TryGenerateExpressionStatement(ExpressionStatementSyntax expressionStatement)
        {
            using (ExpressionStatementTag(GetLineNumber(expressionStatement)))
            {
                return TryGenerateExpression(expressionStatement.Expression);
            }
        }

        private bool TryGenerateType(TypeSyntax type)
        {
            var typeSymbol = SemanticModel.GetTypeInfo(type).Type;
            if (typeSymbol == null)
            {
                return false;
            }

            GenerateType(typeSymbol);
            return true;
        }

        private bool TryGenerateExpression(ExpressionSyntax expression)
        {
            using (ExpressionTag())
            {
                return TryGenerateExpressionSansTag(expression);
            }
        }

        private bool TryGenerateExpressionSansTag(ExpressionSyntax expression)
        {
            switch (expression.Kind())
            {
                case SyntaxKind.CharacterLiteralExpression:
                    return TryGenerateCharLiteral(expression);

                case SyntaxKind.UnaryMinusExpression:
                case SyntaxKind.NumericLiteralExpression:
                case SyntaxKind.StringLiteralExpression:
                case SyntaxKind.TrueLiteralExpression:
                case SyntaxKind.FalseLiteralExpression:
                    return TryGenerateLiteral(expression);

                case SyntaxKind.NullLiteralExpression:
                    GenerateNullLiteral();
                    return true;

                case SyntaxKind.ParenthesizedExpression:
                    return TryGenerateParentheses((ParenthesizedExpressionSyntax)expression);

                case SyntaxKind.AddExpression:
                case SyntaxKind.BitwiseOrExpression:
                case SyntaxKind.BitwiseAndExpression:
                    return TryGenerateBinaryOperation((BinaryExpressionSyntax)expression);

                case SyntaxKind.SimpleAssignmentExpression:
                case SyntaxKind.AddAssignmentExpression:
                    return TryGenerateAssignment((AssignmentExpressionSyntax)expression);

                case SyntaxKind.CastExpression:
                    return TryGenerateCast((CastExpressionSyntax)expression);

                case SyntaxKind.ObjectCreationExpression:
                    return TryGenerateNewClass((ObjectCreationExpressionSyntax)expression);

                case SyntaxKind.ArrayCreationExpression:
                    return TryGenerateNewArray((ArrayCreationExpressionSyntax)expression);

                case SyntaxKind.ArrayInitializerExpression:
                    return TryGenerateArrayLiteral((InitializerExpressionSyntax)expression);

                case SyntaxKind.SimpleMemberAccessExpression:
                    return TryGenerateNameRef((MemberAccessExpressionSyntax)expression);

                case SyntaxKind.IdentifierName:
                    return TryGenerateNameRef((IdentifierNameSyntax)expression);

                case SyntaxKind.InvocationExpression:
                    return GenerateMethodCall((InvocationExpressionSyntax)expression);

                case SyntaxKind.ElementAccessExpression:
                    return TryGenerateArrayElementAccess((ElementAccessExpressionSyntax)expression);

                case SyntaxKind.TypeOfExpression:
                    return TryGenerateTypeOfExpression((TypeOfExpressionSyntax)expression);

                case SyntaxKind.ThisExpression:
                    GenerateThisReference();
                    return true;

                case SyntaxKind.BaseExpression:
                    GenerateBaseReference();
                    return true;
            }

            return false;
        }

        private bool TryGenerateLiteral(ExpressionSyntax expression)
        {
            /*
                <ElementType name="Literal" content="eltOnly">
                - <group order="one">
                    <element type="Null" /> 
                    <element type="Number" /> 
                    <element type="Boolean" /> 
                    <element type="Char" /> 
                    <element type="String" /> 
                    <element type="Array" /> 
                    <element type="Type" /> 
                    </group>
                </ElementType>
            */

            using (LiteralTag())
            {
                var constantValue = SemanticModel.GetConstantValue(expression);
                if (!constantValue.HasValue)
                {
                    return false;
                }

                var type = SemanticModel.GetTypeInfo(expression).Type;
                if (type == null)
                {
                    return false;
                }

                switch (expression.Kind())
                {
                    case SyntaxKind.UnaryMinusExpression:
                    case SyntaxKind.NumericLiteralExpression:
                        GenerateNumber(constantValue.Value, type);
                        return true;

                    case SyntaxKind.StringLiteralExpression:
                        GenerateString((string)constantValue.Value);
                        return true;

                    case SyntaxKind.TrueLiteralExpression:
                    case SyntaxKind.FalseLiteralExpression:
                        GenerateBoolean((bool)constantValue.Value);
                        return true;
                }

                return false;
            }
        }

        private bool TryGenerateCharLiteral(ExpressionSyntax expression)
        {
            // For non-letters and digits, generate a cast of the numeric value to a char.
            // Otherwise, we might end up generating invalid XML.
            if (expression.Kind() != SyntaxKind.CharacterLiteralExpression)
            {
                return false;
            }

            var constantValue = SemanticModel.GetConstantValue(expression);
            if (!constantValue.HasValue)
            {
                return false;
            }

            var ch = (char)constantValue.Value;

            if (!char.IsLetterOrDigit(ch))
            {
                using (CastTag())
                {
                    GenerateType(SpecialType.System_Char);

                    using (ExpressionTag())
                    using (LiteralTag())
                    {
                        GenerateNumber((ushort)ch, SpecialType.System_UInt16);
                    }
                }
            }
            else
            {
                using (LiteralTag())
                {
                    GenerateChar(ch);
                }
            }

            return true;
        }

        private bool TryGenerateParentheses(ParenthesizedExpressionSyntax parenthesizedExpression)
        {
            using (ParenthesesTag())
            {
                return TryGenerateExpression(parenthesizedExpression.Expression);
            }
        }

        private bool TryGenerateBinaryOperation(BinaryExpressionSyntax binaryExpression)
        {
            BinaryOperatorKind kind;
            switch (binaryExpression.Kind())
            {
                case SyntaxKind.AddExpression:
                    kind = BinaryOperatorKind.Plus;
                    break;
                case SyntaxKind.BitwiseOrExpression:
                    kind = BinaryOperatorKind.BitwiseOr;
                    break;
                case SyntaxKind.BitwiseAndExpression:
                    kind = BinaryOperatorKind.BitwiseAnd;
                    break;
                default:
                    return false;
            }

            using (BinaryOperationTag(kind))
            {
                return TryGenerateExpression(binaryExpression.Left)
                    && TryGenerateExpression(binaryExpression.Right);
            }
        }

        private bool TryGenerateAssignment(AssignmentExpressionSyntax binaryExpression)
        {
            var kind = BinaryOperatorKind.None;
            switch (binaryExpression.Kind())
            {
                case SyntaxKind.AddAssignmentExpression:
                    kind = BinaryOperatorKind.AddDelegate;
                    break;
            }

            using (AssignmentTag(kind))
            {
                return TryGenerateExpression(binaryExpression.Left)
                    && TryGenerateExpression(binaryExpression.Right);
            }
        }

        private bool TryGenerateCast(CastExpressionSyntax castExpression)
        {
            var type = SemanticModel.GetTypeInfo(castExpression.Type).Type;
            if (type == null)
            {
                return false;
            }

            using (CastTag())
            {
                GenerateType(type);

                return TryGenerateExpression(castExpression.Expression);
            }
        }

        private bool TryGenerateNewClass(ObjectCreationExpressionSyntax objectCreationExpression)
        {
            if (SemanticModel.GetSymbolInfo(objectCreationExpression.Type).Symbol is not ITypeSymbol type)
            {
                return false;
            }

            using (NewClassTag())
            {
                GenerateType(type);

                foreach (var argument in objectCreationExpression.ArgumentList.Arguments)
                {
                    if (!TryGenerateArgument(argument))
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        private bool TryGenerateNewArray(ArrayCreationExpressionSyntax arrayCreationExpression)
        {
            var type = SemanticModel.GetTypeInfo(arrayCreationExpression).Type;
            if (type == null)
            {
                return false;
            }

            using (NewArrayTag())
            {
                GenerateType(type);

                if (arrayCreationExpression.Initializer != null)
                {
                    using (BoundTag())
                    using (ExpressionTag())
                    using (LiteralTag())
                    using (NumberTag())
                    {
                        EncodedText(arrayCreationExpression.Initializer.Expressions.Count.ToString());
                    }

                    if (!TryGenerateExpression(arrayCreationExpression.Initializer))
                    {
                        return false;
                    }
                }
                else
                {
                    foreach (var rankSpecifier in arrayCreationExpression.Type.RankSpecifiers)
                    {
                        foreach (var size in rankSpecifier.Sizes)
                        {
                            using (BoundTag())
                            {
                                if (!TryGenerateExpression(size))
                                {
                                    return false;
                                }
                            }
                        }
                    }
                }
            }

            return true;
        }

        private bool TryGenerateArrayLiteral(InitializerExpressionSyntax initializerExpression)
        {
            using (LiteralTag())
            using (ArrayTag())
            {
                foreach (var expression in initializerExpression.Expressions)
                {
                    if (!TryGenerateExpression(expression))
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        private bool TryGenerateNameRef(MemberAccessExpressionSyntax memberAccessExpression)
        {
            var symbol = SemanticModel.GetSymbolInfo(memberAccessExpression).Symbol;

            // No null check for 'symbol' here. If 'symbol' unknown, we'll
            // generate an "unknown" name ref.

            using (NameRefTag(GetVariableKind(symbol)))
            {
                var leftHandSymbol = SemanticModel.GetSymbolInfo(memberAccessExpression.Expression).Symbol;
                if (leftHandSymbol != null)
                {
                    if (leftHandSymbol.Kind == SymbolKind.Alias)
                    {
                        leftHandSymbol = ((IAliasSymbol)leftHandSymbol).Target;
                    }
                }

                // If the left-hand side is a named type, we generate a literal expression
                // with the type name. Otherwise, we generate the expression normally.
                if (leftHandSymbol != null && leftHandSymbol.Kind == SymbolKind.NamedType)
                {
                    using (ExpressionTag())
                    using (LiteralTag())
                    {
                        GenerateType((ITypeSymbol)leftHandSymbol);
                    }
                }
                else if (!TryGenerateExpression(memberAccessExpression.Expression))
                {
                    return false;
                }

                GenerateName(memberAccessExpression.Name.Identifier.ValueText);
            }

            return true;
        }

        private bool TryGenerateNameRef(IdentifierNameSyntax identifierName)
        {
            var symbol = SemanticModel.GetSymbolInfo(identifierName).Symbol;

            // No null check for 'symbol' here. If 'symbol' unknown, we'll
            // generate an "unknown" name ref.

            var variableKind = GetVariableKind(symbol);

            using (NameRefTag(variableKind))
            {
                if (symbol != null && variableKind != VariableKind.Local)
                {
                    using (ExpressionTag())
                    {
                        GenerateThisReference();
                    }
                }

                GenerateName(identifierName.Identifier.ToString());
            }

            return true;
        }

        private bool GenerateMethodCall(InvocationExpressionSyntax invocationExpression)
        {
            using (MethodCallTag())
            {
                if (!TryGenerateExpression(invocationExpression.Expression))
                {
                    return false;
                }

                foreach (var argument in invocationExpression.ArgumentList.Arguments)
                {
                    if (!TryGenerateArgument(argument))
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        private bool TryGenerateTypeOfExpression(TypeOfExpressionSyntax typeOfExpression)
        {
            if (typeOfExpression.Type == null)
            {
                return false;
            }

            var type = SemanticModel.GetTypeInfo(typeOfExpression.Type).Type;
            if (type == null)
            {
                return false;
            }

            GenerateType(type);

            return true;
        }

        private bool TryGenerateArrayElementAccess(ElementAccessExpressionSyntax elementAccessExpression)
        {
            using (ArrayElementAccessTag())
            {
                if (!TryGenerateExpression(elementAccessExpression.Expression))
                {
                    return false;
                }

                foreach (var argument in elementAccessExpression.ArgumentList.Arguments)
                {
                    if (!TryGenerateExpression(argument.Expression))
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        private bool TryGenerateArgument(ArgumentSyntax argument)
        {
            using (ArgumentTag())
            {
                return TryGenerateExpression(argument.Expression);
            }
        }

        public static string Generate(MethodDeclarationSyntax methodDeclaration, SemanticModel semanticModel)
        {
            var symbol = semanticModel.GetDeclaredSymbol(methodDeclaration);
            var builder = new MethodXmlBuilder(symbol, semanticModel);

            builder.GenerateBlock(methodDeclaration.Body);

            return builder.ToString();
        }
    }
}
