/*
 * Copyright (c) 2002-2010 JSCAPE
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 *  the terms of the license agreement you entered into with JSCAPE.
 */

package com.jscape.util;

import java.util.concurrent.Callable;

/**
 * @author Alex Usun
 */
public class Attemptable<T>
        implements Callable<T> {

    private final Callable<T> base;
    private final Time attemptInterval;
    private final int maxAttempts;
    private boolean success;
    private int attempt;
    private T result;
    private volatile boolean cancelled;
    private Exception error;

    public static <T> T run(Callable<T> base, long attemptIntervalMillis, int maxAttempts)
            throws Exception {
        return new Attemptable<T>(base, attemptIntervalMillis, maxAttempts).call();
    }

    public Attemptable(Callable<T> base, long attemptIntervalMillis, int maxAttempts) {
        Assert.notNull(base);
        this.base = base;

        Assert.greaterOrEqual(attemptIntervalMillis, 0, "Bad attempt interval value.");
        this.attemptInterval = Time.millis(attemptIntervalMillis);

        Assert.greater(maxAttempts, 0, "Bad max attempts value.");
        this.maxAttempts = maxAttempts;
    }

    public T call()
            throws Exception {
        init();

        while (shouldAttempt()) {
            if (this.attempt > 0) {
                this.attemptInterval.sleepSafe();
            }
            nextAttempt();
        }

        assertNotCancelled();
        assertNoErrors();

        return this.result;
    }

    public void cancel() {
        this.cancelled = true;
    }

    @Override
    public String toString() {
        return "Attemptable {"
                + "base=" + this.base
                + ", attemptInterval=" + this.attemptInterval
                + ", maxAttempts=" + this.maxAttempts
                + ", success=" + this.success
                + ", attempt=" + this.attempt
                + ", result=" + this.result
                + ", cancelled=" + this.cancelled
                + ", error=" + this.error
                + '}';
    }

    private void init() {
        this.success = false;
        this.attempt = 0;
        this.result = null;
        this.error = null;
    }

    private boolean shouldAttempt() {
        return !this.success && this.attempt < this.maxAttempts && !this.cancelled;
    }

    private void nextAttempt()
            throws Exception {
        try {
            this.result = this.base.call();
            this.success = true;
        } catch (Exception e) {
            this.error = e;
            ++this.attempt;
        }
    }

    private void assertNotCancelled() {
        if (!this.success && this.cancelled) {
            throw new java.util.concurrent.CancellationException();
        }
    }

    private void assertNoErrors()
            throws Exception {
        if (!this.success && this.error != null) {
            throw this.error;
        }
    }
}
