﻿// 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.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using UIAutomationClient;

namespace Microsoft.VisualStudio.IntegrationTest.Utilities
{
    public static class Helper
    {
        /// <summary>
        /// A long timeout used to avoid hangs in tests, where a test failure manifests as an operation never occurring.
        /// </summary>
        public static readonly TimeSpan HangMitigatingTimeout = TimeSpan.FromMinutes(1);

        private static IUIAutomation2? _automation;

        public static IUIAutomation2 Automation
        {
            get
            {
                if (_automation == null)
                {
                    Interlocked.CompareExchange(ref _automation, new CUIAutomation8(), null);
                }

                return _automation;
            }
        }

        /// <summary>
        /// This method will retry the action represented by the 'action' argument,
        /// milliseconds, waiting 'delay' milliseconds after each retry. If a given retry returns a value 
        /// other than default(T), this value is returned.
        /// </summary>
        /// <param name="action">the action to retry</param>
        /// <param name="delay">the amount of time to wait between retries in milliseconds</param>
        /// <typeparam name="T">type of return value</typeparam>
        /// <returns>the return value of 'action'</returns>
        public static T? Retry<T>(Func<T> action, int delay)
            => Retry(action, TimeSpan.FromMilliseconds(delay));

        /// <summary>
        /// This method will retry the action represented by the 'action' argument,
        /// milliseconds, waiting 'delay' milliseconds after each retry and will swallow all exceptions. 
        /// If a given retry returns a value other than default(T), this value is returned.
        /// </summary>
        /// <param name="action">the action to retry</param>
        /// <param name="delay">the amount of time to wait between retries in milliseconds</param>
        /// <typeparam name="T">type of return value</typeparam>
        /// <returns>the return value of 'action'</returns>
        public static T? RetryIgnoringExceptions<T>(Func<T> action, int delay)
            => RetryIgnoringExceptions(action, TimeSpan.FromMilliseconds(delay));

        /// <summary>
        /// This method will retry the action represented by the 'action' argument,
        /// waiting for 'delay' time after each retry. If a given retry returns a value 
        /// other than default(T), this value is returned.
        /// </summary>
        /// <param name="action">the action to retry</param>
        /// <param name="delay">the amount of time to wait between retries</param>
        /// <typeparam name="T">type of return value</typeparam>
        /// <returns>the return value of 'action'</returns>
        public static T? Retry<T>(Func<T> action, TimeSpan delay, int retryCount = -1)
        {
            return RetryHelper(() =>
                {
                    try
                    {
                        return action();
                    }
                    catch (COMException)
                    {
                        // Devenv can throw COMExceptions if it's busy when we make DTE calls.
                        return default;
                    }
                },
                delay,
                retryCount);
        }

        /// <summary>
        /// This method will retry the asynchronous action represented by <paramref name="action"/>,
        /// waiting for <paramref name="delay"/> time after each retry. If a given retry returns a value 
        /// other than the default value of <typeparamref name="T"/>, this value is returned.
        /// </summary>
        /// <param name="action">the asynchronous action to retry</param>
        /// <param name="delay">the amount of time to wait between retries</param>
        /// <typeparam name="T">type of return value</typeparam>
        /// <returns>the return value of <paramref name="action"/></returns>
        public static Task<T?> RetryAsync<T>(Func<Task<T>> action, TimeSpan delay)
        {
            return RetryAsyncHelper(async () =>
            {
                try
                {
                    return await action();
                }
                catch (COMException)
                {
                    // Devenv can throw COMExceptions if it's busy when we make DTE calls.
                    return default;
                }
            },
                delay);
        }

        /// <summary>
        /// This method will retry the action represented by the 'action' argument,
        /// milliseconds, waiting 'delay' milliseconds after each retry and will swallow all exceptions. 
        /// If a given retry returns a value other than default(T), this value is returned.
        /// </summary>
        /// <param name="action">the action to retry</param>
        /// <param name="delay">the amount of time to wait between retries in milliseconds</param>
        /// <typeparam name="T">type of return value</typeparam>
        /// <returns>the return value of 'action'</returns>
        public static T? RetryIgnoringExceptions<T>(Func<T> action, TimeSpan delay, int retryCount = -1)
        {
            return RetryHelper(() =>
                {
                    try
                    {
                        return action();
                    }
                    catch (Exception)
                    {
                        return default;
                    }
                },
                delay,
                retryCount);
        }

        private static T RetryHelper<T>(Func<T> action, TimeSpan delay, int retryCount)
        {
            for (var i = 0; true; i++)
            {
                var retval = action();
                if (i == retryCount)
                {
                    return retval;
                }

                if (!Equals(default(T), retval))
                {
                    return retval;
                }

                Thread.Sleep(delay);
            }
        }

        private static async Task<T> RetryAsyncHelper<T>(Func<Task<T>> action, TimeSpan delay)
        {
            while (true)
            {
                var retval = await action().ConfigureAwait(true);
                if (!Equals(default(T), retval))
                {
                    return retval;
                }

                await Task.Delay(delay).ConfigureAwait(true);
            }
        }
    }
}
