text: ﻿// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
  
  using System.Collections.Generic;
  using System.Diagnostics;
  using System.Linq;
  using System.Text;
  using System.Threading;
  using System.Threading.Tasks;
  using Microsoft.CodeAnalysis;
  using Microsoft.CodeAnalysis.CSharp;
  using Microsoft.CodeAnalysis.CSharp.Syntax;
  using Roslynator.CSharp;
  using Roslynator.CSharp.Analysis.Documentation;
  
  namespace Roslynator.CSharp.Refactorings.Documentation
  {
      internal abstract class DocumentationCommentRefactoring<TNode> where TNode : SyntaxNode
      {
          public abstract XmlTag Tag { get; }
  
          public abstract bool ShouldBeBefore(XmlTag tag);
  
          public abstract string GetName(TNode node);
  
          public abstract ElementInfo<TNode> CreateInfo(TNode node, int insertIndex, NewLinePosition newLinePosition);
  
          public async Task<Document> RefactorAsync(
              Document document,
              DocumentationCommentTriviaSyntax comment,
              CancellationToken cancellationToken = default)
          {
              cancellationToken.ThrowIfCancellationRequested();
  
              MemberDeclarationSyntax memberDeclaration = comment.FirstAncestor<MemberDeclarationSyntax>();
  
              SeparatedSyntaxList<TNode> typeParameters = GetSyntaxList(memberDeclaration);
  
              List<ElementInfo<TNode>> infos = GetElementInfos(comment, typeParameters);
  
              string newTrivia = GetNewTrivia(comment, infos);
  
              SyntaxTriviaList triviaList = SyntaxFactory.ParseLeadingTrivia(newTrivia);
  
              if (triviaList.Any())
              {
                  SyntaxTrivia firstTrivia = triviaList[0];
  
                  if (firstTrivia.HasStructure
                      && (firstTrivia.GetStructure() is DocumentationCommentTriviaSyntax newComment))
                  {
                      newComment = newComment.WithFormatterAnnotation();
  
                      return await document.ReplaceNodeAsync(comment, newComment, cancellationToken).ConfigureAwait(false);
                  }
              }
  
              Debug.Fail("");
  
              return document;
          }
  
          protected abstract SeparatedSyntaxList<TNode> GetSyntaxList(SyntaxNode node);
  
          private string GetNewTrivia(
              DocumentationCommentTriviaSyntax comment,
              List<ElementInfo<TNode>> elementInfos)
          {
              var sb = new StringBuilder();
              string text = comment.ToFullString();
              int start = comment.FullSpan.Start;
              int startIndex = 0;
  
              string elementName = XmlTagMapper.GetName(Tag);
  
              foreach (IGrouping<int, ElementInfo<TNode>> grouping in elementInfos
                  .OrderBy(f => f.InsertIndex)
                  .GroupBy(f => f.InsertIndex))
              {
                  int endIndex = grouping.Key - start;
  
                  sb.Append(text, startIndex, endIndex - startIndex);
  
                  foreach (ElementInfo<TNode> elementInfo in grouping)
                  {
                      if (elementInfo.NewLinePosition == NewLinePosition.Beginning)
                          sb.AppendLine();
  
                      sb.Append("/// <")
                          .Append(elementName)
                          .Append(" name=\"")
                          .Append(elementInfo.Name)
                          .Append("\"></")
                          .Append(elementName)
                          .Append(">");
  
                      if (elementInfo.NewLinePosition == NewLinePosition.End)
                          sb.AppendLine();
                  }
  
                  startIndex = endIndex;
              }
  
              sb.Append(text, startIndex, text.Length - startIndex);
  
              return sb.ToString();
          }
  
          private List<ElementInfo<TNode>> GetElementInfos(
              DocumentationCommentTriviaSyntax comment,
              SeparatedSyntaxList<TNode> nodes)
          {
              Dictionary<string, XmlElementSyntax> dic = CreateNameElementMap(comment);
  
              var elementInfos = new List<ElementInfo<TNode>>();
  
              for (int i = 0; i < nodes.Count; i++)
              {
                  if (!dic.ContainsKey(GetName(nodes[i])))
                  {
                      int insertIndex = -1;
                      var newLinePosition = NewLinePosition.Beginning;
  
                      for (int j = i - 1; j >= 0; j--)
                      {
                          if (dic.TryGetValue(GetName(nodes[j]), out XmlElementSyntax element))
                          {
                              insertIndex = element.FullSpan.End;
                              break;
                          }
                      }
  
                      if (insertIndex == -1)
                      {
                          for (int j = i + 1; j < nodes.Count; j++)
                          {
                              if (dic.TryGetValue(GetName(nodes[j]), out XmlElementSyntax element))
                              {
                                  XmlElementSyntax previousElement = GetPreviousElement(comment, element);
  
                                  if (previousElement != null)
                                  {
                                      insertIndex = previousElement.FullSpan.End;
                                  }
                                  else
                                  {
                                      insertIndex = comment.FullSpan.Start;
                                      newLinePosition = NewLinePosition.End;
                                  }
  
                                  break;
                              }
                          }
                      }
  
                      if (insertIndex == -1)
                      {
                          insertIndex = GetDefaultIndex(comment);
  
                          if (insertIndex == comment.FullSpan.Start)
                              newLinePosition = NewLinePosition.End;
                      }
  
                      ElementInfo<TNode> elementInfo = CreateInfo(nodes[i], insertIndex, newLinePosition);
  
                      elementInfos.Add(elementInfo);
                  }
              }
  
              return elementInfos;
          }
  
          private static XmlElementSyntax GetPreviousElement(DocumentationCommentTriviaSyntax comment, XmlElementSyntax element)
          {
              SyntaxList<XmlNodeSyntax> content = comment.Content;
  
              int index = content.IndexOf(element);
  
              for (int i = index - 1; i >= 0; i--)
              {
                  if (content[i].IsKind(SyntaxKind.XmlElement))
                      return (XmlElementSyntax)content[i];
              }
  
              return null;
          }
  
          public Dictionary<string, XmlElementSyntax> CreateNameElementMap(DocumentationCommentTriviaSyntax comment)
          {
              var dic = new Dictionary<string, XmlElementSyntax>();
  
              foreach (XmlElementSyntax element in comment.Elements(Tag))
              {
                  string name = element.GetAttributeValue("name");
  
                  if (!dic.ContainsKey(name))
                      dic.Add(name, element);
              }
  
              return dic;
          }
  
          private int GetDefaultIndex(DocumentationCommentTriviaSyntax comment)
          {
              SyntaxList<XmlNodeSyntax> content = comment.Content;
  
              for (int i = content.Count - 1; i >= 0; i--)
              {
                  if (content[i] is XmlElementSyntax xmlElement)
                  {
                      XmlTag tag = xmlElement.GetTag();
  
                      if (tag == Tag
                          || ShouldBeBefore(tag))
                      {
                          return content[i].FullSpan.End;
                      }
                  }
              }
  
              return comment.FullSpan.Start;
          }
      }
  }