using PostSharp.Aspects;
using PostSharp.Extensibility;
using PostSharp.Serialization;
using System;
using System.Data;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace PostSharp.Samples.AutoRetry.Aspects
{
  /// <summary>
  ///   Aspect that, when applied to a method, causes invocations of this method to be retried if the method ends with
  ///   specified exceptions.
  /// </summary>
  [PSerializable]
  [LinesOfCodeAvoided(5)]
  [MulticastAttributeUsage(MulticastTargets.Method)]
  public sealed class AutoRetryAttribute : MethodInterceptionAspect
  {
    /// <summary>
    ///   Initializes a new <see cref="AutoRetryAttribute" /> with default values.
    /// </summary>
    public AutoRetryAttribute()
    {
      // Set the default values for properties.
      MaxRetries = 5;
      Delay = 3;
      HandledExceptions = new[] { typeof(WebException), typeof(DataException) };
    }

    /// <summary>
    ///   Gets or sets the maximum number of retries. The default value is 5.
    /// </summary>
    public int MaxRetries
    {
      get; set;
    }

    /// <summary>
    ///   Gets or sets the delay before retrying, in seconds. The default value is 3.
    /// </summary>
    public float Delay
    {
      get; set;
    }

    /// <summary>
    ///   Gets or sets the type of exceptions that cause the method invocation to be retried. The default value is
    ///   <see cref="WebException" /> and <see cref="DataException" />.
    /// </summary>
    public Type[] HandledExceptions
    {
      get; set;
    }

    public override async Task OnInvokeAsync(MethodInterceptionArgs args)
    {
      for (var i = 0; ; i++)
      {
        try
        {
          // Invoke the intercepted method.
          await args.ProceedAsync();

          // If we get here, it means the execution was successful.
          return;
        }
        catch (Exception e)
        {
          // The intercepted method threw an exception. Figure out if we can retry the method.

          if (CanRetry(i, e))
          {
            // Yes, we can retry. Write some message and wait a bit.

            Console.WriteLine(
              $"Method failed with exception {e.GetType().Name} '{e.Message}'. Sleeping {Delay} s and retrying. This was our attempt #{i + 1}.");
 
            if (Delay > 0)
            {
              await Task.Delay(TimeSpan.FromSeconds(Delay));
            }

            // Continue to the next iteration.
          }
          else
          {
            // No, we cannot retry. Rethrow the exception.
            throw;
          }
        }
      }
    }

    private bool CanRetry(int attempt, Exception e)
    {
      return attempt < MaxRetries &&
             (HandledExceptions == null || HandledExceptions.Any(type => type.IsInstanceOfType(e)));
    }
  }
}