﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Globalization;
using System.Runtime.InteropServices;

namespace Microsoft.VisualStudio.IntegrationTest.Utilities.InProcess
{
    internal class Debugger_InProc : InProcComponent
    {
        /// <summary>
        /// HResult for "Operation Not Supported" when raising commands. 
        /// </summary>
        private const uint OperationNotSupportedHResult = 0x8971003c;

        /// <summary>
        /// Time to wait between retries if "Operation Not Supported" is thrown when raising a debugger stepping command.
        /// </summary>
        private static readonly TimeSpan DebuggerCommandRetryTimeout = TimeSpan.FromSeconds(10);

        /// <summary>
        /// Time to wait before re-polling a delegate.
        /// </summary>
        private static readonly TimeSpan DefaultPollingInterCallSleep = TimeSpan.FromMilliseconds(250);

        private readonly EnvDTE.Debugger _debugger;

        private Debugger_InProc()
        {
            _debugger = GetDTE().Debugger;
        }

        public static Debugger_InProc Create()
            => new Debugger_InProc();

        public void SetBreakPoint(string fileName, int lineNumber, int columnIndex)
        {
            // Need to increment the line number because editor line numbers starts from 0 but the debugger ones starts from 1.
            _debugger.Breakpoints.Add(File: fileName, Line: lineNumber + 1, Column: columnIndex);
        }

        public void Go(bool waitForBreakMode) => _debugger.Go(waitForBreakMode);

        public void StepOver(bool waitForBreakOrEnd) => WaitForRaiseDebuggerDteCommand(() => _debugger.StepOver(waitForBreakOrEnd));

        public void Stop(bool waitForDesignMode) => _debugger.Stop(WaitForDesignMode: waitForDesignMode);

        public void SetNextStatement() => _debugger.SetNextStatement();

        public void ExecuteStatement(string statement) => _debugger.ExecuteStatement(statement);

        public Common.Expression GetExpression(string expressionText) => new Common.Expression(_debugger.GetExpression(expressionText));

        /// <summary>
        /// Executes the specified action delegate and retries if Operation Not Supported is thrown.
        /// </summary>
        /// <param name="action">Action delegate to exectute.</param>
        private static void WaitForRaiseDebuggerDteCommand(Action action)
        {
            var actionSucceeded = false;

            Func<bool> predicate = delegate
            {
                try
                {
                    action();
                    actionSucceeded = true;
                }
                catch (COMException ex)
                {
                    if ((uint)ex.ErrorCode != OperationNotSupportedHResult)
                    {
                        var message = string.Format(
                            CultureInfo.InvariantCulture,
                            "Failed to raise debugger command, an unexpected '{0}' was thrown with the HResult of '{1}'.",
                            typeof(COMException),
                            ex.ErrorCode);

                        throw new Exception(message, ex);
                    }

                    actionSucceeded = false;
                }

                return actionSucceeded;
            };

            // Repeat the command if "Operation Not Supported" is thrown.
            if (!TryWaitFor(DebuggerCommandRetryTimeout, predicate))
            {
                var message = string.Format(
                    CultureInfo.InvariantCulture,
                    "Failed to raise debugger command within '{0}' seconds.",
                    DebuggerCommandRetryTimeout.TotalSeconds);

                throw new Exception(message);
            }
        }

        /// <summary>
        /// Polls for the specified delegate to return true for the given timeout.
        /// </summary>
        /// <param name="timeout">Timeout to keep polling.</param>
        /// <param name="predicate">Delegate to invoke.</param>
        /// <returns>True if the delegate returned true when polled, otherwise false.</returns>
        public static bool TryWaitFor(TimeSpan timeout, Func<bool> predicate)
        {
            return TryWaitFor(timeout, DefaultPollingInterCallSleep, predicate);
        }

        /// <summary>
        /// Polls for the specified delegate to return true for the given timeout.
        /// </summary>
        /// <param name="timeout">Timeout to keep polling.</param>
        /// <param name="interval">Time to wait between polling.</param>
        /// <param name="predicate">Delegate to invoke.</param>
        /// <returns>
        /// True if the delegate returned true when polled, otherwise false.
        /// </returns>
        private static bool TryWaitFor(TimeSpan timeout, TimeSpan interval, Func<bool> predicate)
        {
            var endTime = DateTime.UtcNow + timeout;
            var validationDelegateSuccess = false;

            while (DateTime.UtcNow < endTime)
            {
                // Note: we don't do this inline in the while() condition and return the result of 
                // (DateTime.Now < startTime + timeout) as this could lead to cases where the validation
                // delegate returned true, at the boundary of the valid time, and would return false
                // when hitting the return statement. 
                if (predicate())
                {
                    validationDelegateSuccess = true;
                    break;
                }

                System.Threading.Thread.Sleep(interval);
            }

            return validationDelegateSuccess;
        }
    }
}
