#nullable enable
using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Robust.Roslyn.Shared;
using Robust.Shared.Prototypes;

namespace Robust.Analyzers;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class PrototypeAnalyzer : DiagnosticAnalyzer
{
    public static readonly DiagnosticDescriptor PrototypeRedundantTypeRule = new(
        Diagnostics.IdPrototypeRedundantType,
        "Redundant Prototype Type specification",
        "Prototype {0} has explicitly set type \"{1}\" that matches autogenerated value",
        "Usage",
        DiagnosticSeverity.Warning,
        true,
        "Remove the redundant type specification."
    );

    public static readonly DiagnosticDescriptor PrototypeEndsWithPrototypeRule = new(
        Diagnostics.IdPrototypeEndsWithPrototype,
        "Prototype name must end with the word Prototype",
        "Prototype {0} does not end with the word Prototype",
        "Usage",
        DiagnosticSeverity.Error,
        true,
        "Add the word Prototype to the end of the class name or manually specify a name in the Prototype attribute."
    );

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
        [PrototypeRedundantTypeRule, PrototypeEndsWithPrototypeRule];

    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze |
                                               GeneratedCodeAnalysisFlags.ReportDiagnostics);
        context.EnableConcurrentExecution();

        context.RegisterCompilationStartAction(static ctx =>
        {
            var prototypeAttribute =
                ctx.Compilation.GetTypeByMetadataName("Robust.Shared.Prototypes.PrototypeAttribute");

            // No attribute, no analyzer.
            if (prototypeAttribute is null)
                return;

            ctx.RegisterSyntaxNodeAction(
                symCtx => AnalyzePrototype(symCtx, prototypeAttribute),
                SyntaxKind.ClassDeclaration);
        });
    }

    private static void AnalyzePrototype(SyntaxNodeAnalysisContext context, INamedTypeSymbol prototypeAttributeSymbol)
    {
        if (context.Node is not ClassDeclarationSyntax classDeclarationSyntax)
            return;

        var classSymbol = context.SemanticModel.GetDeclaredSymbol(classDeclarationSyntax);
        if (classSymbol is null)
            return;

        var className = classSymbol.Name;

        if (!AttributeHelper.HasAttribute(classSymbol, prototypeAttributeSymbol, out var attributeData))
            return;

        var prototypeAttribute = GetAttributeSyntax(attributeData, classDeclarationSyntax);
        if (prototypeAttribute == null)
            return;

        // Check for autogenerated type
        if (prototypeAttribute.ArgumentList?.Arguments[0] is not { } argumentSyntax)
        {
            if (!className.EndsWith(PrototypeUtility.PrototypeNameEnding))
            {
                context.ReportDiagnostic(Diagnostic.Create(PrototypeEndsWithPrototypeRule,
                    classDeclarationSyntax.Identifier.GetLocation(),
                    className));
            }

            return;
        }

        // We only care about redundancy if the argument is a string literal.
        // Passing in a value that resolves to a redundant string is fine.
        if (argumentSyntax.Expression is not LiteralExpressionSyntax literalSyntax)
            return;

        var literalValue = context.SemanticModel.GetConstantValue(literalSyntax);
        if (literalValue.Value is not string specifiedName)
            return;

        var autoName = PrototypeUtility.CalculatePrototypeName(className);

        // Check for name redundancy
        if (autoName == specifiedName)
        {
            // If the class name does not end with "Prototype", allow the redundancy
            if (!className.EndsWith(PrototypeUtility.PrototypeNameEnding))
                return;

            var location = argumentSyntax.GetLocation();
            context.ReportDiagnostic(Diagnostic.Create(PrototypeRedundantTypeRule,
                location,
                className,
                specifiedName));
        }
    }

    private static AttributeSyntax? GetAttributeSyntax(
        AttributeData attributeData,
        ClassDeclarationSyntax classSyntax)
    {
        if (attributeData.ApplicationSyntaxReference is not { } syntaxReference)
            return null;

        foreach (var attributeList in classSyntax.AttributeLists)
        {
            foreach (var attribute in attributeList.Attributes)
            {
                if (syntaxReference.SyntaxTree != attribute.SyntaxTree)
                    continue;

                if (!syntaxReference.Span.OverlapsWith(attribute.Span))
                    continue;

                return attribute;
            }
        }

        return null;
    }
}
