/**
 * Copyright 2012-2018 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.
 */
package com.amazonaws.services.simpleworkflow.flow.aspectj;

import java.util.Arrays;
import java.util.TreeMap;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;

import com.amazonaws.services.simpleworkflow.flow.DecisionContext;
import com.amazonaws.services.simpleworkflow.flow.DecisionContextProvider;
import com.amazonaws.services.simpleworkflow.flow.DecisionContextProviderImpl;
import com.amazonaws.services.simpleworkflow.flow.WorkflowClock;
import com.amazonaws.services.simpleworkflow.flow.WorkflowContext;
import com.amazonaws.services.simpleworkflow.flow.annotations.ExponentialRetry;
import com.amazonaws.services.simpleworkflow.flow.annotations.ExponentialRetryUpdate;
import com.amazonaws.services.simpleworkflow.flow.annotations.ExponentialRetryVersion;
import com.amazonaws.services.simpleworkflow.flow.core.Promise;
import com.amazonaws.services.simpleworkflow.flow.core.Settable;
import com.amazonaws.services.simpleworkflow.flow.interceptors.AsyncExecutor;
import com.amazonaws.services.simpleworkflow.flow.interceptors.AsyncRetryingExecutor;
import com.amazonaws.services.simpleworkflow.flow.interceptors.AsyncRunnable;
import com.amazonaws.services.simpleworkflow.flow.interceptors.ExponentialRetryPolicy;
import com.amazonaws.services.simpleworkflow.flow.worker.IncompatibleWorkflowDefinition;

/**
 * This class is for internal use only and may be changed or removed without
 * prior notice.
 */
@Aspect
public class ExponentialRetryAspect {

    @SuppressWarnings({ "rawtypes", "unchecked" })
    private final class DecoratorInvocationHandler implements AsyncRunnable {

        private final ProceedingJoinPoint pjp;

        private final Settable result;

        public DecoratorInvocationHandler(ProceedingJoinPoint pjp, Settable result) {
            this.pjp = pjp;
            this.result = result;
        }

        @Override
        public void run() throws Throwable {
            if (result != null) {
                result.unchain();
                result.chain((Promise) pjp.proceed());
            }
            else {
                pjp.proceed();
            }
        }
    }

    private final DecisionContextProvider decisionContextProviderImpl = new DecisionContextProviderImpl();

    @Around("execution(@com.amazonaws.services.simpleworkflow.flow.annotations.ExponentialRetryUpdate * *(..)) && @annotation(retryUpdateAnnotation)")
    public Object retry(final ProceedingJoinPoint pjp, ExponentialRetryUpdate retryUpdateAnnotation) throws Throwable {
        ExponentialRetryVersion[] versions = retryUpdateAnnotation.versions();
        TreeMap<Integer, ExponentialRetry> retriesByVersion = new TreeMap<Integer, ExponentialRetry>();
        for (ExponentialRetryVersion retryVersion : versions) {
            int internalVersion = retryVersion.implementationVersion();
            ExponentialRetry previous = retriesByVersion.put(internalVersion, retryVersion.retry());
            if (previous != null) {
                throw new IncompatibleWorkflowDefinition("More then one @ExponentialRetry annotation found for version "
                        + internalVersion);
            }
        }
        DecisionContext decisionContext = decisionContextProviderImpl.getDecisionContext();
        WorkflowContext workflowContext = decisionContext.getWorkflowContext();
        String component = retryUpdateAnnotation.component();
        // Checking isImplementationVersion from the largest version to the lowest one
        for (int internalVersion : retriesByVersion.descendingKeySet()) {
            ExponentialRetry retryAnnotation = retriesByVersion.get(internalVersion);
            if (workflowContext.isImplementationVersion(component, internalVersion)) {
                return retry(pjp, retryAnnotation);
            }
        }
        throw new IncompatibleWorkflowDefinition(
                "@ExponentialRetryUpdate doesn't include implementationVersion compatible with the workflow execution");
    }

    @Around("execution(@com.amazonaws.services.simpleworkflow.flow.annotations.ExponentialRetry * *(..)) && @annotation(retryAnnotation)")
    public Object retry(final ProceedingJoinPoint pjp, ExponentialRetry retryAnnotation) throws Throwable {
        ExponentialRetryPolicy retryPolicy = createExponentialRetryPolicy(retryAnnotation);

        WorkflowClock clock = decisionContextProviderImpl.getDecisionContext().getWorkflowClock();
        AsyncExecutor executor = new AsyncRetryingExecutor(retryPolicy, clock);

        Settable<?> result;
        if (isVoidReturnType(pjp)) {
            result = null;
        }
        else {
            result = new Settable<Object>();
        }
        DecoratorInvocationHandler handler = new DecoratorInvocationHandler(pjp, result);
        executor.execute(handler);
        return result;
    }

    private boolean isVoidReturnType(final ProceedingJoinPoint pjp) {
        boolean isVoidReturnType = false;
        final Signature signature = pjp.getStaticPart().getSignature();
        if (signature instanceof MethodSignature) {
            final MethodSignature methodSignature = (MethodSignature) signature;
            isVoidReturnType = (methodSignature != null) ? Void.TYPE.equals(methodSignature.getReturnType()) : false;
        }
        return isVoidReturnType;
    }

    private ExponentialRetryPolicy createExponentialRetryPolicy(ExponentialRetry retryAnnotation) {

        ExponentialRetryPolicy retryPolicy = new ExponentialRetryPolicy(retryAnnotation.initialRetryIntervalSeconds()).withExceptionsToRetry(
                Arrays.asList(retryAnnotation.exceptionsToRetry())).withExceptionsToExclude(
                Arrays.asList(retryAnnotation.excludeExceptions())).withBackoffCoefficient(retryAnnotation.backoffCoefficient()).withMaximumRetryIntervalSeconds(
                retryAnnotation.maximumRetryIntervalSeconds()).withRetryExpirationIntervalSeconds(
                retryAnnotation.retryExpirationSeconds()).withMaximumAttempts(retryAnnotation.maximumAttempts());

        retryPolicy.validate();
        return retryPolicy;
    }
}