/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.communications.command.client;

import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.rhq.enterprise.communications.command.CommandResponse;
import org.rhq.enterprise.communications.command.impl.remotepojo.RemotePojoInvocationCommandResponse;

/**
 * This is kind of "future" that can be used to wait for a remote POJO invocation. You typically use this to wait for an
 * asynchronous remote pojo invocation. Because this implements {@link CommandResponseCallback}, it can be used as the
 * callback to {@link ClientRemotePojoFactory#setAsynch(boolean, CommandResponseCallback)}.
 *
 * <p>This does not implement Future because its semantics are slightly different, but it does work in a similar manner.
 * Unlike Future, this object is not cancelable, but like Future, you can {@link #get()} the results blocking
 * indefinitely or you can supply a timeout via {@link #get(long, TimeUnit)}. This object can support multiple
 * "calculation results" - once you retreive the results, you should {@link #reset()} it in order to prepare this object
 * to receive another result. If you do not reset this object, the get methods will never block again, they will always
 * immediately return the last results it received. For this reason, it is preferable that you retreive the results via
 * {@link #get(long, TimeUnit)} or {@link #getAndReset(long, TimeUnit)} to force the reset to occur.</p>
 *
 * <p>This class is multi-thread safe, however, you must ensure that you call {@link #get()} to retrieve the results
 * before the callback method {@link #commandSent(CommandResponse)} is called again. If you do not, you will lose the
 * results for that previous invocation.</p>
 *
 * <p>Example usage:</p>
 *
 * <pre>
 * ClientRemotePojoFactory factory = ...
 * RemotePojoInvocationFuture future = new RemotePojoInvocationFuture();
 * factory.setAsync(true, future);
 * MyRemoteAPI pojo = factory.getRemotePojo(MyRemoteAPI.class);
 *
 * pojo.aMethodCall(); // will be sent asynchronously
 * MyObject o = (MyObject) future.getAndReset(); // blocks until aMethodCall really finishes
 *
 * pojo.anotherCall(); // another asynchronous request
 * AnotherObject o = (AnotherObject) future.getAndReset(); // blocks until anotherCall really finishes
 * </pre>
 *
 * @author John Mazzitelli
 */
public class RemotePojoInvocationFuture implements CommandResponseCallback {
    /**
     * the UID to identify the serializable version of this class
     */
    private static final long serialVersionUID = 1L;

    /**
     * When non-<code>null</code>, is the results of the last pojo invocation.
     */
    private RemotePojoInvocationCommandResponse m_results = null;

    /**
     * This will reset this object such that it can prepare to accept another remote pojo invocation result. Calling
     * this method will result in this object clearing out any current calculation result, thus making {@link #get()}
     * block until a new response is received via {@link #commandSent(CommandResponse)}. Note that this method will
     * block if another thread is currently waiting in {@link #get()} - it will unblock once that thread is done
     * waiting.
     */
    public void reset() {
        synchronized (this) {
            m_results = null;
        }

        return;
    }

    /**
     * This stores the new response in this object as this future's calculation result (any previous result is lost).
     * This new response's results object will be returned by {@link #get()}.
     *
     * @see CommandResponseCallback#commandSent(CommandResponse)
     */
    public void commandSent(CommandResponse response) {
        synchronized (this) {
            if (response instanceof RemotePojoInvocationCommandResponse) {
                m_results = (RemotePojoInvocationCommandResponse) response;
            } else {
                m_results = new RemotePojoInvocationCommandResponse(response);
            }

            this.notifyAll();
        }

        return;
    }

    /**
     * Same as {@link #get()}, but before this method returns, this object is {@link #reset()}.
     *
     * @return the invocation results
     *
     * @throws InterruptedException
     * @throws ExecutionException
     *
     * @see    java.util.concurrent.Future#get()
     */
    public Object getAndReset() throws InterruptedException, ExecutionException {
        Object results = get();
        reset(); // note that if a concurrent thread calls get again, this will block; this rarely, if ever, should occur
        return results;
    }

    /**
     * Same as {@link #get(long, TimeUnit)}, but before this method returns, this object is {@link #reset()}.
     *
     * @param  timeout the maximum amount of time to wait
     * @param  unit    the unit of time that <code>timeout</code> is specified in
     *
     * @return the invocation results
     *
     * @throws InterruptedException
     * @throws ExecutionException
     * @throws TimeoutException
     *
     * @see    #get(long, TimeUnit)
     */
    public Object getAndReset(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException,
        TimeoutException {
        Object results = get(timeout, unit);
        reset(); // note that if a concurrent thread calls get again, this will block; this rarely, if ever, should occur
        return results;
    }

    /**
     * Blocks indefinitely until the remote invocation results are available, at which time those results are returned.
     * If an exception occurred during the invocation, that exception is stored as the cause in the thrown
     * {@link ExecutionException}.
     *
     * @return the remote invocation results
     *
     * @throws InterruptedException if the current thread waiting for the results is interrupted
     * @throws ExecutionException   if the remote invocation threw an exception.
     */
    public Object get() throws InterruptedException, ExecutionException {
        try {
            return get(Long.MAX_VALUE, TimeUnit.MILLISECONDS);
        } catch (TimeoutException e) {
            // this should never happen - it would mean we waited for Long.MAX_VALUE milliseconds!
            throw new ExecutionException(e);
        }
    }

    /**
     * Blocks for, at most, the specified amount of time or until the remote invocation results are available, at which
     * time those results are returned. If an exception occurred during the invocation, that exception is stored as the
     * cause in the thrown {@link ExecutionException}.
     *
     * @param  timeout the maximum amount of time to wait
     * @param  unit    the unit of time that <code>timeout</code> is specified in
     *
     * @return the remote invocation results
     *
     * @throws InterruptedException if the current thread waiting for the results is interrupted
     * @throws ExecutionException   if the remote invocation threw an exception.
     * @throws TimeoutException     if the given amount of time has expired before the results have been received
     */
    public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        RemotePojoInvocationCommandResponse result;
        long wait_ms = unit.toMillis(timeout);

        synchronized (this) {
            if (m_results == null) {
                this.wait(wait_ms);
            }

            result = m_results;
        }

        if (result == null) {
            throw new TimeoutException();
        } else if (!result.isSuccessful()) {
            ExecutionException exc_to_throw;
            Throwable result_exc = result.getException();

            if ((result_exc instanceof InvocationTargetException) && (result_exc.getCause() != null)) {
                exc_to_throw = new ExecutionException(result_exc.getCause());
            } else {
                exc_to_throw = new ExecutionException(result_exc);
            }

            throw exc_to_throw;
        }

        // the command response results object is the object returned by the remote method
        return result.getResults();
    }

    /**
     * Returns <code>true</code> if results are available and can be retrieved via the get methods without blocking.
     * Once the first remote invocation is done, this method will always return <code>true</code>, until this object is
     * {@link #reset()}.
     *
     * @return <code>true</code> if results are available; <code>false</code> if the invocation has not completed yet
     */
    public boolean isDone() {
        Object current_results = m_results; // assignment does not require synchronization
        return current_results != null;
    }
}