/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License 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 org.apache.camel.processor.aggregate;

import java.util.concurrent.ThreadLocalRandom;

/**
 * Class to control how failed optimistic locks are tried. This policy supports random and exponential back-off delays.
 * <p/>
 * If {@code randomBackOff} is enabled and a value is supplied for {@code retryDelay} the value will be ignored.
 * <p/>
 * If {@code randomBackOff} is enabled and no value is set for {@code maximumRetryDelay}, a default value of 1000ms will
 * be used, the random delay will be between 0 and 1000 milliseconds.
 * <p/>
 * If both {@code randomBackOff} and {@code exponentialBackOff} are enabled, {@code exponentialBackOff} will take precedence.
 * <p/>
 * If {@code exponentialBackOff} is enabled and a value is set for {@code maximumRetryDelay}, the retry delay will keep
 * doubling in value until it reaches or exceeds {@code maximumRetryDelay}. After it has reached or exceeded {@code maximumRetryDelay}
 * the value of {@code maximumRetryDelay} will be used as the retry delay.
 * <p/>
 * If both {@code exponentialBackOff} and {@code randomBackOff} are disabled, the value of {@code retryDelay} will be used
 * as the retry delay and remain constant through all the retry attempts.
 * <p/>
 * If the value of {@code maximumRetries} is set above zero, retry attempts will stop at the value specified.
 * <p/>
 * The default behaviour of this policy is to retry forever and exponentially increase the back-off delay starting with 50ms.
 */
public class OptimisticLockRetryPolicy {
    private static final long DEFAULT_MAXIMUM_RETRY_DELAY = 1000L;

    private int maximumRetries;
    private long retryDelay = 50L;
    private long maximumRetryDelay;
    private boolean exponentialBackOff = true;
    private boolean randomBackOff;

    public OptimisticLockRetryPolicy() {
    }

    public boolean shouldRetry(final int retryCounter) {
        return maximumRetries <= 0 || retryCounter < maximumRetries;
    }

    public void doDelay(final int retryCounter) throws InterruptedException {
        long sleepFor = getDelay(retryCounter);
        if (sleepFor > 0) {
            Thread.sleep(sleepFor);
        }
    }

    public long getDelay(final int retryCounter) {
        long sleepFor = 0;
        if (retryDelay > 0 || randomBackOff) {
            sleepFor = exponentialBackOff ? (retryDelay << retryCounter)
                    : (randomBackOff ? ThreadLocalRandom.current().nextInt((int)(maximumRetryDelay > 0 ? maximumRetryDelay : DEFAULT_MAXIMUM_RETRY_DELAY)) : retryDelay);
            if (maximumRetryDelay > 0 && sleepFor > maximumRetryDelay) {
                sleepFor = maximumRetryDelay;
            }
        }
        return sleepFor;
    }

    public int getMaximumRetries() {
        return maximumRetries;
    }

    public void setMaximumRetries(int maximumRetries) {
        this.maximumRetries = maximumRetries;
    }

    public OptimisticLockRetryPolicy maximumRetries(int maximumRetries) {
        setMaximumRetries(maximumRetries);
        return this;
    }

    public long getRetryDelay() {
        return retryDelay;
    }

    public void setRetryDelay(long retryDelay) {
        this.retryDelay = retryDelay;
    }

    public OptimisticLockRetryPolicy retryDelay(long retryDelay) {
        setRetryDelay(retryDelay);
        return this;
    }

    public long getMaximumRetryDelay() {
        return maximumRetryDelay;
    }

    public void setMaximumRetryDelay(long maximumRetryDelay) {
        this.maximumRetryDelay = maximumRetryDelay;
    }

    public OptimisticLockRetryPolicy maximumRetryDelay(long maximumRetryDelay) {
        setMaximumRetryDelay(maximumRetryDelay);
        return this;
    }

    public boolean isExponentialBackOff() {
        return exponentialBackOff;
    }

    public void setExponentialBackOff(boolean exponentialBackOff) {
        this.exponentialBackOff = exponentialBackOff;
    }

    public OptimisticLockRetryPolicy exponentialBackOff() {
        setExponentialBackOff(true);
        return this;
    }

    public boolean isRandomBackOff() {
        return randomBackOff;
    }

    public void setRandomBackOff(boolean randomBackOff) {
        this.randomBackOff = randomBackOff;
    }

    public OptimisticLockRetryPolicy randomBackOff() {
        setRandomBackOff(true);
        return this;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("OptimisticLockRetryPolicy[");
        sb.append("maximumRetries=").append(maximumRetries);
        sb.append(", retryDelay=").append(retryDelay);
        sb.append(", maximumRetryDelay=").append(maximumRetryDelay);
        sb.append(", exponentialBackOff=").append(exponentialBackOff);
        sb.append(", randomBackOff=").append(randomBackOff);
        sb.append(']');
        return sb.toString();
    }
}