using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

namespace SourceGeneratorSamples
{
	[Generator]
	public class NotifyPropertyGenerator : ISourceGenerator
	{
		const string attributeText = @"
using System;

namespace AutoNotify
{
    [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
    sealed class AutoNotifyAttribute : Attribute
    {
        public AutoNotifyAttribute()
        { }

        public string PropertyName { get; set; }
    }
}
";

		public void Initialize(GeneratorInitializationContext context)
		{
			context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
		}

		public void Execute(GeneratorExecutionContext context)
		{
			context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8));

			if (context.SyntaxReceiver is not SyntaxReceiver receiver)
				return;

			var parseOptions = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions;
			var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), parseOptions));

			// get the newly bound attribute, and INotifyPropertyChanged
			var attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute");
			var notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");

			// loop over the candidate fields, and keep the ones that are actually annotated
			var fieldSymbols = new List<IFieldSymbol>();
			foreach (var field in receiver.CandidateFields)
			{
				var model = compilation.GetSemanticModel(field.SyntaxTree);
				foreach (var variable in field.Declaration.Variables)
				{
					// Get the symbol being decleared by the field, and keep it if its annotated
					var fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
					if (fieldSymbol.GetAttributes().Any(x => x.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
					{
						fieldSymbols.Add(fieldSymbol);
					}
				}
			}

			// group the fields by class, and generate the source
			foreach (IGrouping<INamedTypeSymbol, IFieldSymbol> group in fieldSymbols.GroupBy(x => x.ContainingType))
			{
				var classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context);
				context.AddSource($"{group.Key.Name}_AutoNotify.cs", SourceText.From(classSource, Encoding.UTF8));
			}
		}

		string ProcessClass(INamedTypeSymbol classSymbol, List<IFieldSymbol> fields, ISymbol attributeSymbol, ISymbol notifySymbol, GeneratorExecutionContext context)
		{
			if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
			{
				return null; //TODO: issue a diagnostic that it must be top level
			}

			var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();

			var source = new StringBuilder($@"
namespace {namespaceName}
{{
    public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()}
    {{
");

			// if the class doesn't implement INotifyPropertyChanged already, add it
			if (!classSymbol.Interfaces.Contains(notifySymbol))
			{
				source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;");
			}

			// create properties for each field 
			foreach (var fieldSymbol in fields)
			{
				ProcessField(source, fieldSymbol, attributeSymbol);
			}

			source.Append("} }");
			return source.ToString();
		}

		void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol)
		{
			var fieldName = fieldSymbol.Name;
			var fieldType = fieldSymbol.Type;

			var attributeData = fieldSymbol.GetAttributes().Single(x => x.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
			var overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(x => x.Key == "PropertyName").Value;

			var propertyName = ChooseName(fieldName, overridenNameOpt);
			if (propertyName == string.Empty || propertyName == fieldName)
			{
				//TODO: issue a diagnostic that we can't process this field
				return;
			}

			source.Append($@"
public {fieldType} {propertyName} 
{{
    get 
    {{
        return this.{fieldName};
    }}

    set
    {{
        this.{fieldName} = value;
        this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName})));
    }}
}}

");

			string ChooseName(string fieldName, TypedConstant overridenNameOpt)
			{
				if (!overridenNameOpt.IsNull)
				{
					return overridenNameOpt.Value.ToString();
				}

				fieldName = fieldName.TrimStart('_');
				if (fieldName == string.Empty)
					return string.Empty;

				if (fieldName.Length == 1)
					return fieldName.ToUpper();

				return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1);
			}

		}

		class SyntaxReceiver : ISyntaxReceiver
		{
			public List<FieldDeclarationSyntax> CandidateFields { get; } = new List<FieldDeclarationSyntax>();

			public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
			{
				if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax && fieldDeclarationSyntax.AttributeLists.Any())
				{
					CandidateFields.Add(fieldDeclarationSyntax);
				}
			}
		}
	}
}
