using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Util;
using CppSharp.AST;

namespace CppSharp.Generators.CSharp
{
    public static class CSharpCommentPrinter
    {
        public static void Print(this ITextGenerator textGenerator, Comment comment, CommentKind kind)
        {
            var sections = new List<Section>();
            GetCommentSections(comment, sections);
            foreach (var section in sections)
                TrimSection(section);
            FormatComment(textGenerator, sections, kind);
        }

        private static void GetCommentSections(this Comment comment, List<Section> sections)
        {
            switch (comment.Kind)
            {
                case DocumentationCommentKind.FullComment:
                {
                    foreach (var block in ((FullComment)comment).Blocks)
                        block.GetCommentSections(sections);
                    break;
                }
                case DocumentationCommentKind.BlockCommandComment:
                {
                    var blockCommandComment = (BlockCommandComment)comment;
                    if (blockCommandComment.ParagraphComment == null)
                        break;

                    switch (blockCommandComment.CommandKind)
                    {
                        case CommentCommandKind.Brief:
                            sections.Add(new Section(CommentElement.Summary));
                            blockCommandComment.ParagraphComment.GetCommentSections(sections);
                            break;
                        case CommentCommandKind.Return:
                        case CommentCommandKind.Returns:
                            sections.Add(new Section(CommentElement.Returns));
                            blockCommandComment.ParagraphComment.GetCommentSections(sections);
                            break;
                        case CommentCommandKind.Since:
                            var lastBlockSection = sections.Last();
                            foreach (var inlineContentComment in blockCommandComment.ParagraphComment.Content)
                            {
                                inlineContentComment.GetCommentSections(sections);
                                if (inlineContentComment.HasTrailingNewline)
                                    lastBlockSection.NewLine();
                            }

                            break;
                        default:
                            sections.Add(new Section(CommentElement.Remarks));
                            blockCommandComment.ParagraphComment.GetCommentSections(sections);
                            break;
                    }

                    break;
                }
                case DocumentationCommentKind.ParamCommandComment:
                {
                    var paramCommandComment = (ParamCommandComment)comment;
                    var param = new Section(CommentElement.Param);
                    sections.Add(param);
                    if (paramCommandComment.Arguments.Count > 0)
                        param.Attributes.Add($"name=\"{paramCommandComment.Arguments[0].Text}\"");

                    if (paramCommandComment.ParagraphComment != null)
                    {
                        foreach (var inlineContentComment in paramCommandComment.ParagraphComment.Content)
                        {
                            inlineContentComment.GetCommentSections(sections);
                            if (inlineContentComment.HasTrailingNewline)
                                sections.Last().NewLine();
                        }
                    }

                    if (!string.IsNullOrEmpty(sections.Last().CurrentLine.ToString()))
                        sections.Add(new Section(CommentElement.Remarks));
                    break;
                }
                case DocumentationCommentKind.ParagraphComment:
                {
                    bool summaryParagraph = false;
                    if (sections.Count == 0)
                    {
                        sections.Add(new Section(CommentElement.Summary));
                        summaryParagraph = true;
                    }

                    var lastParagraphSection = sections.Last();
                    var paragraphComment = (ParagraphComment)comment;
                    foreach (var inlineContentComment in paragraphComment.Content)
                    {
                        inlineContentComment.GetCommentSections(sections);
                        if (inlineContentComment.HasTrailingNewline)
                            lastParagraphSection.NewLine();

                        lastParagraphSection = sections.Last();
                    }

                    if (!string.IsNullOrEmpty(lastParagraphSection.CurrentLine.ToString()))
                        lastParagraphSection.NewLine();

                    if (sections[0].GetLines().Count > 0 && summaryParagraph)
                    {
                        sections[0].GetLines().AddRange(sections.Skip(1).SelectMany(s => s.GetLines()));
                        sections.RemoveRange(1, sections.Count - 1);
                        sections.Add(new Section(CommentElement.Remarks));
                    }

                    break;
                }
                case DocumentationCommentKind.TextComment:
                {
                    var lastTextSection = sections.Last();
                    lastTextSection.CurrentLine
                        .Append(
                            GetText(comment, lastTextSection.Type is CommentElement.Returns or CommentElement.Param)
                                .TrimStart()
                        );
                    break;
                }
                case DocumentationCommentKind.InlineCommandComment:
                {
                    var lastInlineSection = sections.Last();
                    var inlineCommand = (InlineCommandComment)comment;

                    if (inlineCommand.CommandKind == CommentCommandKind.B)
                    {
                        var argText = $" <c>{inlineCommand.Arguments[0].Text}</c> ";
                        lastInlineSection.CurrentLine.Append(argText);
                    }

                    break;
                }
                case DocumentationCommentKind.HTMLStartTagComment:
                {
                    var startTag = (HTMLStartTagComment)comment;
                    var sectionType = CommentElementFromTag(startTag.TagName);

                    if (IsInlineCommentElement(sectionType))
                    {
                        var lastSection = sections.Last();
                        lastSection.CurrentLine.Append(startTag);
                        break;
                    }

                    sections.Add(new Section(sectionType)
                    {
                        Attributes = startTag.Attributes.Select(a => a.ToString()).ToList()
                    });
                    break;
                }
                case DocumentationCommentKind.HTMLEndTagComment:
                {
                    var endTag = (HTMLEndTagComment)comment;
                    var sectionType = CommentElementFromTag(endTag.TagName);

                    if (IsInlineCommentElement(sectionType))
                    {
                        var lastSection = sections.Last();
                        lastSection.CurrentLine.Append(endTag);
                    }

                    break;
                }
                case DocumentationCommentKind.HTMLTagComment:
                case DocumentationCommentKind.TParamCommandComment:
                case DocumentationCommentKind.VerbatimBlockComment:
                case DocumentationCommentKind.VerbatimLineComment:
                case DocumentationCommentKind.InlineContentComment:
                case DocumentationCommentKind.VerbatimBlockLineComment:
                case DocumentationCommentKind.BlockContentComment:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
        }

        private static string GetText(Comment comment, bool trim = false)
        {
            var textComment = ((TextComment)comment);
            var text = textComment.Text;
            if (trim)
                text = text.Trim();

            if (Helpers.RegexTag.IsMatch(text))
                return string.Empty;

            return HtmlEncoder.HtmlEncode(
                text.Length > 1 && text[0] == ' ' && text[1] != ' ' ? text[1..] : text);
        }

        private static void TrimSection(Section section)
        {
            var lines = section.GetLines();
            for (int i = 0; i < lines.Count - 1; i++)
            {
                if (string.IsNullOrWhiteSpace(lines[i]))
                    lines.RemoveAt(i--);
                else
                    break;
            }
            for (int i = lines.Count - 1; i >= 0; i--)
            {
                if (string.IsNullOrWhiteSpace(lines[i]))
                    lines.RemoveAt(i);
                else
                    break;
            }
        }

        private static void FormatComment(ITextGenerator textGenerator, List<Section> sections, CommentKind kind)
        {
            sections.Sort((x, y) => x.Type.CompareTo(y.Type));

            var remarks = sections.Where(s => s.Type == CommentElement.Remarks).ToList();
            if (remarks.Count != 0)
                remarks.First().GetLines().AddRange(remarks.Skip(1).SelectMany(s => s.GetLines()));

            if (remarks.Count > 1)
                sections.RemoveRange(sections.IndexOf(remarks.First()) + 1, remarks.Count - 1);

            var commentPrefix = Comment.GetMultiLineCommentPrologue(kind);
            foreach (var section in sections.Where(s => s.HasLines))
            {
                var lines = section.GetLines();
                var tag = section.Type.ToString().ToLowerInvariant();

                var attributes = string.Empty;
                if (section.Attributes.Count != 0)
                    attributes = ' ' + string.Join(" ", section.Attributes);

                textGenerator.Write($"{commentPrefix} <{tag}{attributes}>");
                if (lines.Count == 1)
                {
                    textGenerator.Write(lines[0]);
                }
                else
                {
                    textGenerator.NewLine();
                    foreach (var line in lines)
                        textGenerator.WriteLine($"{commentPrefix} <para>{line}</para>");
                    textGenerator.Write($"{commentPrefix} ");
                }
                textGenerator.WriteLine($"</{tag}>");
            }
        }

        private class Section
        {
            public Section(CommentElement type)
            {
                Type = type;
            }

            public StringBuilder CurrentLine { get; set; } = new();

            public CommentElement Type { get; set; }

            public List<string> Attributes { get; init; } = new();

            private List<string> lines { get; } = new();

            public bool HasLines => lines.Any();

            public void NewLine()
            {
                lines.Add(CurrentLine.ToString());
                CurrentLine.Clear();
            }

            public List<string> GetLines()
            {
                if (CurrentLine.Length > 0)
                    NewLine();
                return lines;
            }
        }

        private static CommentElement CommentElementFromTag(string tag)
        {
            return tag.ToLowerInvariant() switch
            {
                "c" => CommentElement.C,
                "code" => CommentElement.Code,
                "example" => CommentElement.Example,
                "exception" => CommentElement.Exception,
                "include" => CommentElement.Include,
                "list" => CommentElement.List,
                "para" => CommentElement.Para,
                "param" => CommentElement.Param,
                "paramref" => CommentElement.ParamRef,
                "permission" => CommentElement.Permission,
                "remarks" => CommentElement.Remarks,
                "return" or "returns" => CommentElement.Returns,
                "summary" => CommentElement.Summary,
                "typeparam" => CommentElement.TypeParam,
                "typeparamref" => CommentElement.TypeParamRef,
                "value" => CommentElement.Value,
                "seealso" => CommentElement.SeeAlso,
                "see" => CommentElement.See,
                "inheritdoc" => CommentElement.InheritDoc,
                _ => CommentElement.Unknown
            };
        }

        /// <summary>From https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/documentation-comments#d3-recommended-tags</summary>
        /// <remarks>Enum value is equal to sorting priority</remarks>
        private enum CommentElement
        {
            C = 1000,               // Set text in a code-like font
            Code = 1001,            // Set one or more lines of source code or program output
            Example = 11,           // Indicate an example
            Exception = 8,          // Identifies the exceptions a method can throw
            Include = 1,            // Includes XML from an external file
            List = 1002,            // Create a list or table
            Para = 1003,            // Permit structure to be added to text
            Param = 5,              // Describe a parameter for a method or constructor
            ParamRef = 1004,        // Identify that a word is a parameter name
            Permission = 7,         // Document the security accessibility of a member
            Remarks = 9,            // Describe additional information about a type
            Returns = 6,            // Describe the return value of a method
            See = 1005,             // Specify a link
            SeeAlso = 10,           // Generate a See Also entry
            Summary = 2,            // Describe a type or a member of a type
            TypeParam = 4,          // Describe a type parameter for a generic type or method
            TypeParamRef = 1006,    // Identify that a word is a type parameter name
            Value = 3,              // Describe a property
            InheritDoc = 0,         // Inherit documentation from a base class
            Unknown = 9999,         // Unknown tag
        }

        private static bool IsInlineCommentElement(CommentElement element) =>
            element switch
            {
                CommentElement.C => true,
                CommentElement.Code => true,
                CommentElement.List => true,
                CommentElement.Para => true,
                CommentElement.ParamRef => true,
                CommentElement.See => true,
                CommentElement.TypeParamRef => true,
                CommentElement.Unknown => true, // Print unknown tags as inline
                _ => ((int)element) >= 1000
            };
    }
}