// 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 Microsoft.CodeAnalysis.Razor.Workspaces;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.Editor.Razor.Debugging;
using Microsoft.VisualStudio.LanguageServices.Razor.Extensions;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.TextManager.Interop;
using Microsoft.VisualStudio.Threading;
using Microsoft.VisualStudio.Utilities;
using TextSpan = Microsoft.VisualStudio.TextManager.Interop.TextSpan;

namespace Microsoft.VisualStudio.LanguageServices.Razor;

internal partial class RazorLanguageService : IVsLanguageDebugInfo
{
    private readonly RazorBreakpointResolver _breakpointResolver;
    private readonly RazorProximityExpressionResolver _proximityExpressionResolver;
    private readonly ILspServerActivationTracker _lspServerActivationTracker;
    private readonly IUIThreadOperationExecutor _uiThreadOperationExecutor;
    private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactory;
    private readonly JoinableTaskFactory _joinableTaskFactory;

    public RazorLanguageService(
        RazorBreakpointResolver breakpointResolver,
        RazorProximityExpressionResolver proximityExpressionResolver,
        ILspServerActivationTracker lspServerActivationTracker,
        IUIThreadOperationExecutor uiThreadOperationExecutor,
        IVsEditorAdaptersFactoryService editorAdaptersFactory,
        JoinableTaskFactory joinableTaskFactory)
    {
        if (breakpointResolver is null)
        {
            throw new ArgumentNullException(nameof(breakpointResolver));
        }

        if (proximityExpressionResolver is null)
        {
            throw new ArgumentNullException(nameof(proximityExpressionResolver));
        }

        if (uiThreadOperationExecutor is null)
        {
            throw new ArgumentNullException(nameof(uiThreadOperationExecutor));
        }

        if (editorAdaptersFactory is null)
        {
            throw new ArgumentNullException(nameof(editorAdaptersFactory));
        }

        if (lspServerActivationTracker is null)
        {
            throw new ArgumentNullException(nameof(lspServerActivationTracker));
        }

        if (joinableTaskFactory is null)
        {
            throw new ArgumentNullException(nameof(joinableTaskFactory));
        }

        _breakpointResolver = breakpointResolver;
        _proximityExpressionResolver = proximityExpressionResolver;
        _lspServerActivationTracker = lspServerActivationTracker;
        _uiThreadOperationExecutor = uiThreadOperationExecutor;
        _editorAdaptersFactory = editorAdaptersFactory;
        _joinableTaskFactory = joinableTaskFactory;
    }

    public int GetProximityExpressions(IVsTextBuffer pBuffer, int iLine, int iCol, int cLines, out IVsEnumBSTR? ppEnum)
    {
        if (!_lspServerActivationTracker.IsActive)
        {
            // We can't do anything if our LSP server isn't up and running, and can't initialize here due to UI thread dependency issues
            // This method should only be called during a debugging sessions, so we should never hit this case, and if we do, we have much
            // bigger problems.
            ppEnum = null;
            return VSConstants.E_NOTIMPL;
        }

        var textBuffer = _editorAdaptersFactory.GetDataBuffer(pBuffer);
        if (textBuffer is null)
        {
            // Can't resolve the text buffer, let someone else deal with this breakpoint.
            ppEnum = null;
            return VSConstants.E_NOTIMPL;
        }

        var snapshot = textBuffer.CurrentSnapshot;
        if (!ValidateLocation(snapshot, iLine, iCol))
        {
            // The point disappeared between sessions. Do not evaluate proximity expressions here.
            ppEnum = null;
            return VSConstants.E_FAIL;
        }

        var proximityExpressions = _uiThreadOperationExecutor.Execute(
            title: SR.ProximityExpression_Dialog_Title,
            description: SR.ProximityExpression_Dialog_Description,
            allowCancellation: true,
            showProgress: true,
            (cancellationToken) => _proximityExpressionResolver.TryResolveProximityExpressionsAsync(textBuffer, iLine, iCol, cancellationToken), _joinableTaskFactory);

        if (proximityExpressions is null)
        {
            ppEnum = null;
            return VSConstants.E_FAIL;
        }

        ppEnum = new VsEnumBSTR(proximityExpressions);
        return VSConstants.S_OK;
    }

    public int ValidateBreakpointLocation(IVsTextBuffer pBuffer, int iLine, int iCol, TextSpan[] pCodeSpan)
    {
        if (!_lspServerActivationTracker.IsActive)
        {
            // We can't do anything if our LSP server isn't up and running, and can't initialize here due to UI thread dependency issues
            // Returning like this means the debugger will place a breakpoint in the margin, but not highlight a span in the line.
            // We trust that it will later validate the breakpoint location and remove it if it's not valid, and that validation
            // happens via LSP anyway.
            return VSConstants.E_NOTIMPL;
        }

        var textBuffer = _editorAdaptersFactory.GetDataBuffer(pBuffer);
        if (textBuffer is null)
        {
            // Can't resolve the text buffer, let someone else deal with this breakpoint.
            return VSConstants.E_NOTIMPL;
        }

        var snapshot = textBuffer.CurrentSnapshot;
        if (!ValidateLocation(snapshot, iLine, iCol))
        {
            // The point disappeared between sessions. Do not allow a breakpoint here.
            return VSConstants.E_FAIL;
        }

        var breakpointRange = _uiThreadOperationExecutor.Execute(
            title: "Determining breakpoint location...",
            description: "Razor Debugger",
            allowCancellation: true,
            showProgress: true,
            (cancellationToken) => _breakpointResolver.TryResolveBreakpointRangeAsync(textBuffer, iLine, iCol, cancellationToken), _joinableTaskFactory);

        if (breakpointRange is null)
        {
            // Failed to create the dialog at all or no applicable breakpoint location.
            return VSConstants.E_FAIL;
        }

        pCodeSpan[0] = new TextSpan()
        {
            iStartIndex = breakpointRange.Start.Character,
            iStartLine = breakpointRange.Start.Line,
            iEndIndex = breakpointRange.End.Character,
            iEndLine = breakpointRange.End.Line,
        };

        return VSConstants.S_OK;
    }

    public int GetNameOfLocation(IVsTextBuffer pBuffer, int iLine, int iCol, out string? pbstrName, out int piLineOffset)
    {
        pbstrName = default;
        piLineOffset = default;
        return VSConstants.E_NOTIMPL;
    }

    public int GetLocationOfName(string pszName, out string? pbstrMkDoc, TextSpan[] pspanLocation)
    {
        pbstrMkDoc = default;
        return VSConstants.E_NOTIMPL;
    }

    public int ResolveName(string pszName, uint dwFlags, out IVsEnumDebugName? ppNames)
    {
        ppNames = default;
        return VSConstants.E_NOTIMPL;
    }

    public int GetLanguageID(IVsTextBuffer pBuffer, int iLine, int iCol, out Guid pguidLanguageID)
    {
        pguidLanguageID = default;
        return VSConstants.E_NOTIMPL;
    }

    public int IsMappedLocation(IVsTextBuffer pBuffer, int iLine, int iCol)
    {
        return VSConstants.E_NOTIMPL;
    }

    private static bool ValidateLocation(ITextSnapshot snapshot, int lineNumber, int columnIndex)
    {
        if (lineNumber < 0 || lineNumber >= snapshot.LineCount)
        {
            return false;
        }

        var line = snapshot.GetLineFromLineNumber(lineNumber);
        if (columnIndex < 0 || columnIndex > line.Length)
        {
            return false;
        }

        return true;
    }
}
