/*
 * Copyright 2010-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 * 
 *  http://aws.amazon.com/apache2.0
 * 
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */
using Amazon.Runtime.Internal.Util;
using System;
using System.Net;
namespace Amazon.Runtime.Internal
{
    /// 
    /// The retry handler has the generic logic for retrying requests.
    /// It uses a retry policy which specifies when 
    /// a retry should be performed.
    /// 
    public class RetryHandler : PipelineHandler
    {
        private ILogger _logger;
        /// 
        /// The logger used to log messages.
        /// 
        public override ILogger Logger
        {
            get { return _logger; }
            set
            {
                _logger = value;
                this.RetryPolicy.Logger = value;
            }
        }
        /// 
        /// The retry policy which specifies when 
        /// a retry should be performed.
        /// 
        public RetryPolicy RetryPolicy { get; private set; }
        
        /// 
        /// Constructor which takes in a retry policy.
        /// 
        /// Retry Policy
        public RetryHandler(RetryPolicy retryPolicy)
        {
            this.RetryPolicy = retryPolicy;
        }
        /// 
        /// Invokes the inner handler and performs a retry, if required as per the
        /// retry policy.
        /// 
        /// The execution context which contains both the
        /// requests and response context.
        public override void InvokeSync(IExecutionContext executionContext)
        {
            var requestContext = executionContext.RequestContext;
            var responseContext = executionContext.ResponseContext;
            bool shouldRetry = false;
            do
            {
                try
                {
                    base.InvokeSync(executionContext);
                    this.RetryPolicy.NotifySuccess(executionContext);
                    return;
                }
                catch (Exception exception)
                {
                    shouldRetry = this.RetryPolicy.Retry(executionContext, exception);
                    if (!shouldRetry)
                    {
                        LogForError(requestContext, exception);
                        throw;
                    }
                    else
                    {
                        requestContext.Retries++;
                        requestContext.Metrics.SetCounter(Metric.AttemptCount, requestContext.Retries);
                        LogForRetry(requestContext, exception);
                    }
                }
                PrepareForRetry(requestContext);
                using (requestContext.Metrics.StartEvent(Metric.RetryPauseTime))
                    this.RetryPolicy.WaitBeforeRetry(executionContext);
            } while (shouldRetry);
        }
#if AWS_ASYNC_API 
        /// 
        /// Invokes the inner handler and performs a retry, if required as per the
        /// retry policy.
        /// 
        /// The response type for the current request.
        /// The execution context, it contains the
        /// request and response context.
        /// A task that represents the asynchronous operation.
        public override async System.Threading.Tasks.Task InvokeAsync(IExecutionContext executionContext)
        {
            var requestContext = executionContext.RequestContext;
            var responseContext = executionContext.ResponseContext;
            bool shouldRetry = false;
            do
            {
                System.Runtime.ExceptionServices.ExceptionDispatchInfo capturedException = null;
                try
                {
                    T result = await base.InvokeAsync(executionContext).ConfigureAwait(false);
                    this.RetryPolicy.NotifySuccess(executionContext);
                    return result;
                }
                catch (Exception e)
                {
                    capturedException = System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e);
                }
                if (capturedException != null)
                {
                    shouldRetry = await this.RetryPolicy.RetryAsync(executionContext, capturedException.SourceException).ConfigureAwait(false);
                    if (!shouldRetry)
                    {
                        LogForError(requestContext, capturedException.SourceException);
                        capturedException.Throw();
                    }
                    else
                    {
                        requestContext.Retries++;
                        requestContext.Metrics.SetCounter(Metric.AttemptCount, requestContext.Retries);
                        LogForRetry(requestContext, capturedException.SourceException);
                    }
                }
                PrepareForRetry(requestContext);
                using (requestContext.Metrics.StartEvent(Metric.RetryPauseTime))
                    await RetryPolicy.WaitBeforeRetryAsync(executionContext).ConfigureAwait(false);
            } while (shouldRetry);
            throw new AmazonClientException("Neither a response was returned nor an exception was thrown in the Runtime RetryHandler.");
        }
#endif
#if AWS_APM_API 
        /// 
        /// Invokes the inner handler and performs a retry, if required as per the
        /// retry policy.
        /// 
        /// The execution context which contains both the
        /// requests and response context.
        protected override void InvokeAsyncCallback(IAsyncExecutionContext executionContext)
        {
            var requestContext = executionContext.RequestContext;
            var responseContext = executionContext.ResponseContext;
            var exception = responseContext.AsyncResult.Exception;
            var syncExecutionContext = ExecutionContext.CreateFromAsyncContext(executionContext);
            if (exception != null)
            {   
                var shouldRetry = this.RetryPolicy.Retry(syncExecutionContext, exception);
                if (shouldRetry)
                {
                    requestContext.Retries++;
                    requestContext.Metrics.SetCounter(Metric.AttemptCount, requestContext.Retries);
                    LogForRetry(requestContext, exception);
                    PrepareForRetry(requestContext);
                    // Clear out current exception
                    responseContext.AsyncResult.Exception = null;
                    using (requestContext.Metrics.StartEvent(Metric.RetryPauseTime))
                        this.RetryPolicy.WaitBeforeRetry(syncExecutionContext);
                    // Retry by calling InvokeAsync
                    this.InvokeAsync(executionContext);
                    return;
                }
                else
                {
                    LogForError(requestContext, exception);
                }
            }
            else
            {
                this.RetryPolicy.NotifySuccess(syncExecutionContext);
            }
            // Call outer handler
            base.InvokeAsyncCallback(executionContext);
        }
#endif
        /// 
        /// Prepares the request for retry.
        /// 
        /// Request context containing the state of the request.
        internal static void PrepareForRetry(IRequestContext requestContext)
        {
            if (requestContext.Request.ContentStream != null &&
                requestContext.Request.OriginalStreamPosition >= 0)
            {
                var stream = requestContext.Request.ContentStream;
                // If the stream is wrapped in a HashStream, reset the HashStream
                var hashStream = stream as HashStream;
                if (hashStream != null)
                {
                    hashStream.Reset();
                    stream = hashStream.GetSeekableBaseStream();
                }
                stream.Position = requestContext.Request.OriginalStreamPosition;
            }
        }
        
        private void LogForRetry(IRequestContext requestContext, Exception exception)
        {
#if !NETSTANDARD
            var webException = exception as WebException;
            if (webException != null)
            {
                Logger.InfoFormat("WebException ({1}) making request {2} to {3}. Attempting retry {4} of {5}.",
                          webException.Status,
                          requestContext.RequestName,
                          requestContext.Request.Endpoint.ToString(),
                          requestContext.Retries,
                          this.RetryPolicy.MaxRetries);
            }
            else
            {
#endif
            Logger.InfoFormat("{0} making request {1} to {2}. Attempting retry {3} of {4}.",
                          exception.GetType().Name,
                          requestContext.RequestName,
                          requestContext.Request.Endpoint.ToString(),
                          requestContext.Retries,
                          this.RetryPolicy.MaxRetries);
#if !NETSTANDARD
            }
#endif
        }
        private void LogForError(IRequestContext requestContext, Exception exception)
        {
            Logger.Error(exception, "{0} making request {1} to {2}. Attempt {3}.",
                          exception.GetType().Name,
                          requestContext.RequestName,
                          requestContext.Request.Endpoint.ToString(),
                          requestContext.Retries + 1);
        }        
    }
}