/*
 * Copyright 2013 Google Inc.
 *
 * Licensed 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.bitcoinj.utils;

import org.bitcoinj.core.Utils;
import com.google.common.primitives.Longs;

import static com.google.common.base.Preconditions.checkArgument;

/**
 * <p>Tracks successes and failures and calculates a time to retry the operation.</p>
 *
 * <p>The retries are exponentially backed off, up to a maximum interval.  On success the back off interval is reset.</p>
 */
public class ExponentialBackoff implements Comparable<ExponentialBackoff> {
    public static final int DEFAULT_INITIAL_MILLIS = 100;
    public static final float DEFAULT_MULTIPLIER = 1.1f;
    public static final int DEFAULT_MAXIMUM_MILLIS = 30 * 1000;

    private float backoff;
    private long retryTime;
    private final Params params;

    /**
     * Parameters to configure a particular kind of exponential backoff.
     */
    public static class Params {
        private final float initial;
        private final float multiplier;
        private final float maximum;

        /**
         * @param initialMillis the initial interval to wait, in milliseconds
         * @param multiplier the multiplier to apply on each failure
         * @param maximumMillis the maximum interval to wait, in milliseconds
         */
        public Params(long initialMillis, float multiplier, long maximumMillis) {
            checkArgument(multiplier > 1.0f, "multiplier must be greater than 1.0");
            checkArgument(maximumMillis >= initialMillis, "maximum must not be less than initial");

            this.initial = initialMillis;
            this.multiplier = multiplier;
            this.maximum = maximumMillis;
        }

        /**
         * Construct params with default values.
         */
        public Params() {
            initial = DEFAULT_INITIAL_MILLIS;
            multiplier = DEFAULT_MULTIPLIER;
            maximum = DEFAULT_MAXIMUM_MILLIS;
        }
    }

    public ExponentialBackoff(Params params) {
        this.params = params;
        trackSuccess();
    }

    /** Track a success - reset back off interval to the initial value */
    public final void trackSuccess() {
        backoff = params.initial;
        retryTime = Utils.currentTimeMillis();
    }

    /** Track a failure - multiply the back off interval by the multiplier */
    public void trackFailure() {
        retryTime = Utils.currentTimeMillis() + (long)backoff;
        backoff = Math.min(backoff * params.multiplier, params.maximum);
    }

    /** Get the next time to retry, in milliseconds since the epoch */
    public long getRetryTime() {
        return retryTime;
    }

    @Override
    public int compareTo(ExponentialBackoff other) {
        // note that in this implementation compareTo() is not consistent with equals()
        return Longs.compare(retryTime, other.retryTime);
    }

    @Override
    public String toString() {
        return "ExponentialBackoff retry=" + retryTime + " backoff=" + backoff;
    }
}