namespace BizHawk.Analyzers;

using System.Collections.Immutable;
using System.Linq;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class ExprBodiedMemberFlowAnalyzer : DiagnosticAnalyzer
{
	private static readonly DiagnosticDescriptor DiagExprBodiedMemberFlow = new(
		id: "BHI1120",
		title: "Expression-bodied member should be flowed to next line correctly",
		messageFormat: "{0}",
		category: "Usage",
		defaultSeverity: DiagnosticSeverity.Warning,
		isEnabledByDefault: true);

	public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(DiagExprBodiedMemberFlow);

	public override void Initialize(AnalysisContext context)
	{
		context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
		context.EnableConcurrentExecution();
		context.RegisterCompilationStartAction(initContext =>
		{
			var ARROW_ONE_LINE = (' ', ' ');
//			var ARROW_POST_SIG = (' ', '\n');
			var ARROW_PRE_BODY = ('\n', ' ');
			initContext.RegisterSyntaxNodeAction(
				snac =>
				{
					var aecs = (ArrowExpressionClauseSyntax) snac.Node;
					(char Before, char After) expectedWhitespace;
					string kind;
					var parent = aecs.Parent;
					if (parent is null)
					{
						snac.ReportDiagnostic(Diagnostic.Create(DiagExprBodiedMemberFlow, aecs.GetLocation(), "Syntax node for expression-bodied member was orphaned?"));
						return;
					}
					void Flag(string message)
						=> snac.ReportDiagnostic(Diagnostic.Create(DiagExprBodiedMemberFlow, parent.GetLocation(), message));
					switch (parent)
					{
						case MethodDeclarationSyntax:
							expectedWhitespace = ARROW_PRE_BODY;
							kind = "method";
							break;
						case PropertyDeclarationSyntax:
							expectedWhitespace = ARROW_PRE_BODY;
							kind = "get-only prop";
							break;
						case AccessorDeclarationSyntax ads:
							expectedWhitespace = ARROW_ONE_LINE;
							switch (ads.Keyword.Text)
							{
								case "get":
									kind = ads.Parent?.Parent is IndexerDeclarationSyntax ? "get-indexer" : "getter";
									break;
								case "set":
									kind = ads.Parent?.Parent is IndexerDeclarationSyntax ? "set-indexer" : "setter";
									break;
								case "init":
									kind = "setter";
									break;
								case "add":
									kind = "event sub";
									break;
								case "remove":
									kind = "event unsub";
									break;
								default:
									Flag($"Expression-bodied accessor was of an unexpected kind: {ads.Parent!.Parent!.GetType().FullName}");
									return;
							}
							break;
						case ConstructorDeclarationSyntax:
							expectedWhitespace = ARROW_PRE_BODY;
							kind = "constructor";
							break;
						case LocalFunctionStatementSyntax:
							expectedWhitespace = ARROW_PRE_BODY;
							kind = "local method";
							break;
						case IndexerDeclarationSyntax:
							expectedWhitespace = ARROW_PRE_BODY;
							kind = "get-only indexer";
							break;
						case OperatorDeclarationSyntax:
							expectedWhitespace = ARROW_PRE_BODY;
							kind = "overloaded operator";
							break;
						case ConversionOperatorDeclarationSyntax:
							expectedWhitespace = ARROW_PRE_BODY;
							kind = "overloaded cast operator";
							break;
						case DestructorDeclarationSyntax:
							expectedWhitespace = ARROW_PRE_BODY;
							kind = "finalizer";
							break;
						default:
							Flag($"Expression-bodied member was of an unexpected kind: {parent.GetType().FullName}");
							return;
					}
					static string EscapeChar(char c)
						=> c is '\n' ? "\\n" : c.ToString();
					void Fail()
						=> Flag($"Whitespace around {kind} arrow syntax should be `{EscapeChar(expectedWhitespace.Before)}=>{EscapeChar(expectedWhitespace.After)}`");
					if ((aecs.ArrowToken.HasLeadingTrivia ? '\n' : ' ') != expectedWhitespace.Before)
					{
						Fail();
						return;
					}
					var hasLineBreakAfterArrow = aecs.ArrowToken.HasTrailingTrivia && aecs.ArrowToken.TrailingTrivia.ToFullString().Contains('\n');
					if ((hasLineBreakAfterArrow ? '\n' : ' ') != expectedWhitespace.After) Fail();
				},
				SyntaxKind.ArrowExpressionClause);
		});
	}
}