using Semmle.Extraction.CommentProcessing;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Semmle.Extraction.Entities;
using System;

namespace Semmle.Extraction.CSharp.Entities
{
    class CommentLine : CachedEntity<(Microsoft.CodeAnalysis.Location, string)>, ICommentLine
    {
        CommentLine(Context cx, Microsoft.CodeAnalysis.Location loc, CommentType type, string text, string raw)
            : base(cx, (loc, text))
        {
            Type = type;
            RawText = raw;
        }

        public Microsoft.CodeAnalysis.Location Location => symbol.Item1;
        public CommentType Type { get; private set; }

        public string Text { get { return symbol.Item2; } }
        public string RawText { get; private set; }

        public static void Extract(Context cx, SyntaxTrivia trivia)
        {
            switch (trivia.Kind())
            {
                case SyntaxKind.SingleLineDocumentationCommentTrivia:
                    /*
                        This is actually a multi-line comment consisting of /// lines.
                        So split it up.
                    */

                    var text = trivia.ToFullString();

                    var split = text.Split('\n');
                    var currentLocation = trivia.GetLocation().SourceSpan.Start - 3;

                    for (int line = 0; line < split.Length - 1; ++line)
                    {
                        string fullLine = split[line];
                        var nextLineLocation = currentLocation + fullLine.Length + 1;
                        fullLine = fullLine.TrimEnd('\r');
                        string trimmedLine = fullLine;

                        int leadingSpaces = trimmedLine.IndexOf('/');
                        if (leadingSpaces != -1)
                        {
                            fullLine = fullLine.Substring(leadingSpaces);
                            currentLocation += leadingSpaces;
                            trimmedLine = trimmedLine.Substring(leadingSpaces + 3); // Remove leading spaces and the "///"
                            trimmedLine = trimmedLine.Trim();

                            var span = Microsoft.CodeAnalysis.Text.TextSpan.FromBounds(currentLocation, currentLocation + fullLine.Length);
                            var location = Microsoft.CodeAnalysis.Location.Create(trivia.SyntaxTree, span);
                            var commentType = CommentType.XmlDoc;
                            cx.CommentGenerator.AddComment(Create(cx, location, commentType, trimmedLine, fullLine));
                        }
                        else
                        {
                            cx.ModelError("Unexpected comment format");
                        }
                        currentLocation = nextLineLocation;
                    }
                    break;

                case SyntaxKind.SingleLineCommentTrivia:
                    {
                        string contents = trivia.ToString().Substring(2);
                        var commentType = CommentType.Singleline;
                        if (contents.Length > 0 && contents[0] == '/')
                        {
                            commentType = CommentType.XmlDoc;
                            contents = contents.Substring(1);       // An XML comment.
                        }
                        cx.CommentGenerator.AddComment(Create(cx, trivia.GetLocation(), commentType, contents.Trim(), trivia.ToFullString()));
                    }
                    break;
                case SyntaxKind.MultiLineDocumentationCommentTrivia:
                case SyntaxKind.MultiLineCommentTrivia:
                    /*  We receive a single SyntaxTrivia for a multiline block spanning several lines.
                        So we split it into separate lines
                    */
                    text = trivia.ToFullString();

                    split = text.Split('\n');
                    currentLocation = trivia.GetLocation().SourceSpan.Start;

                    for (int line = 0; line < split.Length; ++line)
                    {
                        string fullLine = split[line];
                        var nextLineLocation = currentLocation + fullLine.Length + 1;
                        fullLine = fullLine.TrimEnd('\r');
                        string trimmedLine = fullLine;
                        if (line == 0) trimmedLine = trimmedLine.Substring(2);
                        if (line == split.Length - 1) trimmedLine = trimmedLine.Substring(0, trimmedLine.Length - 2);
                        trimmedLine = trimmedLine.Trim();

                        var span = Microsoft.CodeAnalysis.Text.TextSpan.FromBounds(currentLocation, currentLocation + fullLine.Length);
                        var location = Microsoft.CodeAnalysis.Location.Create(trivia.SyntaxTree, span);
                        var commentType = line == 0 ? CommentType.Multiline : CommentType.MultilineContinuation;
                        cx.CommentGenerator.AddComment(Create(cx, location, commentType, trimmedLine, fullLine));
                        currentLocation = nextLineLocation;
                    }
                    break;
                // Strangely, these are reported as SingleLineCommentTrivia.
                case SyntaxKind.DocumentationCommentExteriorTrivia:
                    cx.ModelError($"Unhandled comment type {trivia.Kind()} for {trivia}");
                    break;
            }
        }

        Extraction.Entities.Location location;

        public override void Populate()
        {
            location = Context.Create(Location);
            Context.Emit(Tuples.commentline(this, Type == CommentType.MultilineContinuation ? CommentType.Multiline : Type, Text, RawText));
            Context.Emit(Tuples.commentline_location(this, location));
        }

        public override Microsoft.CodeAnalysis.Location ReportingLocation => location.symbol;

        public override bool NeedsPopulation => true;

        public override IId Id
        {
            get
            {
                var loc = Context.Create(Location);
                return new Key(loc, ";commentline");
            }
        }

        static CommentLine Create(Context cx, Microsoft.CodeAnalysis.Location loc, CommentType type, string text, string raw) => CommentLineFactory.Instance.CreateEntity(cx, loc, type, text, raw);

        class CommentLineFactory : ICachedEntityFactory<(Microsoft.CodeAnalysis.Location, CommentType, string, string), CommentLine>
        {
            public static readonly CommentLineFactory Instance = new CommentLineFactory();

            public CommentLine Create(Context cx, (Microsoft.CodeAnalysis.Location, CommentType, string, string) init) =>
                new CommentLine(cx, init.Item1, init.Item2, init.Item3, init.Item4);
        }

        public override TrapStackBehaviour TrapStackBehaviour => TrapStackBehaviour.OptionalLabel;
    }
}
