// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Syntax;

namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
    internal class SpanEditHandler
    {
        private static readonly int TypeHashCode = typeof(SpanEditHandler).GetHashCode();

        public SpanEditHandler(Func<string, IEnumerable<Syntax.InternalSyntax.SyntaxToken>> tokenizer)
            : this(tokenizer, AcceptedCharactersInternal.Any)
        {
        }

        public SpanEditHandler(Func<string, IEnumerable<Syntax.InternalSyntax.SyntaxToken>> tokenizer, AcceptedCharactersInternal accepted)
        {
            AcceptedCharacters = accepted;
            Tokenizer = tokenizer;
        }

        public AcceptedCharactersInternal AcceptedCharacters { get; set; }

        public Func<string, IEnumerable<Syntax.InternalSyntax.SyntaxToken>> Tokenizer { get; set; }

        public static SpanEditHandler CreateDefault()
        {
            return CreateDefault(c => Enumerable.Empty<Syntax.InternalSyntax.SyntaxToken>());
        }

        public static SpanEditHandler CreateDefault(Func<string, IEnumerable<Syntax.InternalSyntax.SyntaxToken>> tokenizer)
        {
            return new SpanEditHandler(tokenizer);
        }

        public virtual EditResult ApplyChange(SyntaxNode target, SourceChange change)
        {
            return ApplyChange(target, change, force: false);
        }

        public virtual EditResult ApplyChange(SyntaxNode target, SourceChange change, bool force)
        {
            var result = PartialParseResultInternal.Accepted;
            if (!force)
            {
                result = CanAcceptChange(target, change);
            }

            // If the change is accepted then apply the change
            if ((result & PartialParseResultInternal.Accepted) == PartialParseResultInternal.Accepted)
            {
                return new EditResult(result, UpdateSpan(target, change));
            }
            return new EditResult(result, target);
        }

        public virtual bool OwnsChange(SyntaxNode target, SourceChange change)
        {
            var end = target.EndPosition;
            var changeOldEnd = change.Span.AbsoluteIndex + change.Span.Length;
            return change.Span.AbsoluteIndex >= target.Position &&
                   (changeOldEnd < end || (changeOldEnd == end && AcceptedCharacters != AcceptedCharactersInternal.None));
        }

        protected virtual PartialParseResultInternal CanAcceptChange(SyntaxNode target, SourceChange change)
        {
            return PartialParseResultInternal.Rejected;
        }

        protected virtual SyntaxNode UpdateSpan(SyntaxNode target, SourceChange change)
        {
            var newContent = change.GetEditedContent(target);
            var builder = Syntax.InternalSyntax.SyntaxListBuilder<Syntax.InternalSyntax.SyntaxToken>.Create();
            foreach (var token in Tokenizer(newContent))
            {
                builder.Add(token);
            }

            SyntaxNode newTarget = null;
            if (target is RazorMetaCodeSyntax)
            {
                newTarget = Syntax.InternalSyntax.SyntaxFactory.RazorMetaCode(builder.ToList()).CreateRed(target.Parent, target.Position);
            }
            else if (target is MarkupTextLiteralSyntax)
            {
                newTarget = Syntax.InternalSyntax.SyntaxFactory.MarkupTextLiteral(builder.ToList()).CreateRed(target.Parent, target.Position);
            }
            else if (target is MarkupEphemeralTextLiteralSyntax)
            {
                newTarget = Syntax.InternalSyntax.SyntaxFactory.MarkupEphemeralTextLiteral(builder.ToList()).CreateRed(target.Parent, target.Position);
            }
            else if (target is CSharpStatementLiteralSyntax)
            {
                newTarget = Syntax.InternalSyntax.SyntaxFactory.CSharpStatementLiteral(builder.ToList()).CreateRed(target.Parent, target.Position);
            }
            else if (target is CSharpExpressionLiteralSyntax)
            {
                newTarget = Syntax.InternalSyntax.SyntaxFactory.CSharpExpressionLiteral(builder.ToList()).CreateRed(target.Parent, target.Position);
            }
            else if (target is CSharpEphemeralTextLiteralSyntax)
            {
                newTarget = Syntax.InternalSyntax.SyntaxFactory.CSharpEphemeralTextLiteral(builder.ToList()).CreateRed(target.Parent, target.Position);
            }
            else if (target is UnclassifiedTextLiteralSyntax)
            {
                newTarget = Syntax.InternalSyntax.SyntaxFactory.UnclassifiedTextLiteral(builder.ToList()).CreateRed(target.Parent, target.Position);
            }
            else
            {
                Debug.Fail($"The type {target?.GetType().Name} is not a supported span node.");
            }

            var context = target.GetSpanContext();
            newTarget = context != null ? newTarget?.WithSpanContext(context) : newTarget;

            return newTarget;
        }

        protected internal static bool IsAtEndOfFirstLine(SyntaxNode target, SourceChange change)
        {
            var endOfFirstLine = target.GetContent().IndexOfAny(new char[] { (char)0x000d, (char)0x000a, (char)0x2028, (char)0x2029 });
            return (endOfFirstLine == -1 || (change.Span.AbsoluteIndex - target.Position) <= endOfFirstLine);
        }

        /// <summary>
        /// Returns true if the specified change is an insertion of text at the end of this span.
        /// </summary>
        protected internal static bool IsEndDeletion(SyntaxNode target, SourceChange change)
        {
            return change.IsDelete && IsAtEndOfSpan(target, change);
        }

        /// <summary>
        /// Returns true if the specified change is a replacement of text at the end of this span.
        /// </summary>
        protected internal static bool IsEndReplace(SyntaxNode target, SourceChange change)
        {
            return change.IsReplace && IsAtEndOfSpan(target, change);
        }

        protected internal static bool IsAtEndOfSpan(SyntaxNode target, SourceChange change)
        {
            return (change.Span.AbsoluteIndex + change.Span.Length) == target.EndPosition;
        }

        public override string ToString()
        {
            return GetType().Name + ";Accepts:" + AcceptedCharacters;
        }

        public override bool Equals(object obj)
        {
            return obj is SpanEditHandler other &&
                GetType() == other.GetType() &&
                AcceptedCharacters == other.AcceptedCharacters;
        }

        public override int GetHashCode()
        {
            // Hash code should include only immutable properties but Equals also checks the type.
            return TypeHashCode;
        }
    }
}
