// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT license. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.AspNetCore.Razor.TextDifferencing;

internal partial class SourceTextDiffer
{
    private class CharDiffer : SourceTextDiffer
    {
        private readonly struct Buffer
        {
            public readonly char[] Array;
            public readonly int Start;
            public readonly int Length;

            public Buffer(char[] array, int start, int length)
                => (Array, Start, Length) = (array, start, length);

            public void Deconstruct(out char[] array, out int start, out int length)
                => (array, start, length) = (Array, Start, Length);

            public char this[int index]
                => Array[index - Start];

            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            public bool Contains(int index)
                => index >= Start && index < Start + Length;
        }

        private const int BufferSize = 1024 * 16;

        protected override int OldSourceLength { get; }
        protected override int NewSourceLength { get; }

        private char[] _appendBuffer;
        private Buffer _oldBuffer;
        private Buffer _newBuffer;

        public CharDiffer(SourceText oldText, SourceText newText)
            : base(oldText, newText)
        {
            _appendBuffer = RentArray(BufferSize);

            _oldBuffer = new(RentArray(BufferSize), 0, BufferSize);
            OldText.CopyTo(0, _oldBuffer.Array, 0, Math.Min(OldText.Length, BufferSize));

            _newBuffer = new(RentArray(BufferSize), 0, BufferSize);
            NewText.CopyTo(0, _newBuffer.Array, 0, Math.Min(NewText.Length, BufferSize));

            OldSourceLength = oldText.Length;
            NewSourceLength = newText.Length;
        }

        public override void Dispose()
        {
            ReturnArray(_appendBuffer);
            ReturnArray(_oldBuffer.Array);
            ReturnArray(_newBuffer.Array);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static void FillBuffer(ref Buffer buffer, SourceText text, int index)
        {
            // We slide our buffer so that index is in the middle. However, we have
            // have to be careful not extend past either the start or end of the SourceText.
            // Note that we always assume that we're filling the buffer with
            // BufferSize # of characters. If the SourceText is smaller than BufferSize,
            // this method shouldn't be called.

            Debug.Assert(text.Length >= BufferSize);

            var start = Math.Max(index - (BufferSize / 2), 0);

            if (start + BufferSize > text.Length)
            {
                start = text.Length - BufferSize;
            }

            text.CopyTo(start, buffer.Array, 0, BufferSize);
            buffer = new(buffer.Array, start, BufferSize);
        }

        protected override bool SourceEqual(int oldSourceIndex, int newSourceIndex)
        {
            ref var oldBuffer = ref _oldBuffer;
            ref var newBuffer = ref _newBuffer;

            if (!oldBuffer.Contains(oldSourceIndex))
            {
                FillBuffer(ref oldBuffer, OldText, oldSourceIndex);
            }

            if (!newBuffer.Contains(newSourceIndex))
            {
                FillBuffer(ref newBuffer, NewText, newSourceIndex);
            }

            return oldBuffer[oldSourceIndex] == newBuffer[newSourceIndex];
        }

        protected override int GetEditPosition(DiffEdit edit)
            => edit.Position;

        protected override int AppendEdit(DiffEdit edit, StringBuilder builder)
        {
            if (edit.Kind == DiffEditKind.Insert)
            {
                Assumes.NotNull(edit.NewTextPosition);
                var newTextPosition = edit.NewTextPosition.GetValueOrDefault();

                if (edit.Length > 1)
                {
                    var buffer = EnsureBuffer(ref _appendBuffer, edit.Length);
                    NewText.CopyTo(newTextPosition, buffer, 0, edit.Length);

                    builder.Append(buffer, 0, edit.Length);
                }
                else if (edit.Length == 1)
                {
                    builder.Append(NewText[newTextPosition]);
                }

                return edit.Position;
            }

            return edit.Position + edit.Length;
        }
    }
}
