/*
 * Written by Dawid Kurzyniec and released to the public domain, as explained
 * at http://creativecommons.org/licenses/publicdomain
 */

package edu.emory.mathcs.util.concurrent;

import edu.emory.mathcs.util.concurrent.*;
import edu.emory.mathcs.backport.java.util.concurrent.*;

/**
 * A  class maintaining a single reference variable serving as the result
 * of an operation. The result cannot be accessed until it has been set.
 * <p>
 * This class is intended primarily for subclassing. Typical usage scenario
 * is to create a new instance and invoke
 * {@link #createPerformer createPerformer}, thus
 * obtaining runnable that will execute specified task and set results in
 * that instance. Note that such obtained runnable should be executed only
 * once -- subsequent execution attempts will fail due to "task already
 * completed" condition.
 *
 * @see Executor
 **/
public class AsyncTask implements Future {

    /** completion status */
    volatile boolean completed = false;

    CancellationException cancellationException;

    /**
     * Wrapper for the thread in which async task is executed, or the task
     * itself if it implements {@link Cancellable}. Set by the thread that
     * is just about to start executing the task.
     */
    protected Cancellable cancellationHandler;

    /** Optional callback to invoke when task completes */
    final Callback callback;

    /** result of a task */
    Object result;

    /** exception object in case task completed abruptly */
    Throwable resultException;

    protected AsyncTask() { this(null); }

    protected AsyncTask(Callback cb) {
        this.callback = cb;
    }

    /**
     * Checks if the task has completed. Not synchronized to improve
     * concurrency, using "volatile" instead
     * @return <tt>true</tt> if task completed, <tt>false</tt> otherwise.
     */
    public boolean isDone() {
        return completed;
    }

    public boolean cancel(boolean mayInterruptIfRunning) {
        Throwable resultException = null;
        synchronized (this) {
            if (cancellationHandler != null) {
               // task is currently running
               if (mayInterruptIfRunning) {
                   return cancellationHandler.cancel(true);
               }
               else {
                   return false;
               }
            }
            else if (!completed) {
                // task not yet started. We could set this as completed
                // abruptly at this point, but then we would have to call
                // the callback, which would execute within the current thread,
                // possibly blocking. It seems safer to guarantee that
                // "interrupt()" always return promptly, and the interruption
                // status is propagated by the same thread which would execute
                // the task.
                this.cancellationException = new CancellationException("task cancelled");
                return true;
            }
            else {
                // task already completed
                return false;
            }
        }
    }

    public synchronized boolean isCancelled() {
        if (cancellationException != null) return true;
        if (cancellationHandler != null) return cancellationHandler.isCancelled();
        return false;
    }

    /**
     * Marks the task as completed.
     * @param result the result of a task.
     */
    protected void setCompleted(Object result) {
        synchronized (this) {
            if (completed) {
                throw new IllegalStateException("task already completed");
            }
            this.completed = true;
            this.result = result;
            this.cancellationHandler = null;
            notifyAll();
        }

        // invoking callbacks *after* setting future as completed and
        // outside the synchronization block makes it safe to call
        // interrupt() from within callback code (in which case it will be
        // ignored rather than cause deadlock / illegal state exception)
        invokeCallback(result, null);
    }

    /**
     * Marks the task as failed.
     * @param exception the cause of abrupt completion.
     * @throws IllegalStateException if task had been completed already.
     */
    protected void setFailed(Throwable exception) {
        synchronized (this) {
            if (completed) {
                throw new IllegalStateException("task already completed");
            }
            this.completed = true;
            this.resultException = exception;
            this.cancellationHandler = null;
            notifyAll();
        }

        // invoking callbacks *after* setting future as completed and
        // outside the synchronization block makes it safe to call
        // interrupt() from within callback code (in which case it will be
        // ignored rather than cause deadlock / illegal state exception)
        invokeCallback(null, exception);
    }

    public synchronized Object get()
        throws InterruptedException, ExecutionException
    {
        waitFor();
        return getResult();
    }

    public synchronized Object get(long timeout, TimeUnit tunit)
        throws InterruptedException, ExecutionException, TimeoutException
    {
        waitFor(tunit.convert(timeout, TimeUnit.MILLISECONDS));
        if (!completed) {
            throw new TimeoutException("Timeout when waiting for a result");
        }
        return getResult();
    }

    /**
     * Waits for the task to complete.
     */
    private void waitFor() throws InterruptedException {
        while (!completed) {
            wait();
        }
    }

    /**
     * Waits for the task to complete for timeout milliseconds.
     */
    private void waitFor(long timeout) throws InterruptedException {
        while (!completed && timeout > 0) {
            long timestart = System.currentTimeMillis();
            wait(timeout);
            if (completed) return;
            timeout -= (System.currentTimeMillis() - timestart);
        }
    }

    /**
     * Gets the result of the task.
     *
     * PRE: task completed
     * PRE: called from synchronized block
     */
    private Object getResult() throws ExecutionException {
        if (resultException != null) {
            throw new ExecutionException("task completed abruptly", resultException);
        }
        return result;
    }

    /**
     * Schedules specified task with given executor, and returns
     * completion handle that can be used to access the result or to cancel
     * the task. Later, if task completes successfully, the handle is marked
     * completed with the result that has been returned from the task.
     * If the task ends with an exception, the handle is marked
     * failed with cause being that exception. In such case, as a debugging
     * aid, current stack trace (that is, that of this method's invoker) is
     * appended to the original stack trace.
     *
     * @param executor the executor to use
     * @param call the task to schedule
     * @return completion handle that can be used to access the result or
     *         cancel the task.
     */
    public static AsyncTask start(final Executor executor, final Callable call)
    {
        return start(executor, call, null);
    }

    /**
     * Schedules specified task with given executor, and returns
     * completion handle that can be used to access the result or to cancel
     * the task. Later, if task completes successfully, the handle is marked
     * completed with the result that has been returned from the task.
     * If the task ends with an exception, the handle is marked
     * failed with cause being that exception. In such case, as a debugging
     * aid, current stack trace (that is, that of this method's invoker) is
     * appended to the original stack trace.
     *
     * @param executor the executor to use
     * @param call the task to schedule
     * @param cb callback to invoke upon completion
     * @return completion handle that can be used to access the result or
     *         cancel the task.
     */
    public static AsyncTask start(final Executor executor, final Callable call,
                                  final Callback cb)
    {
        return start(executor, call, cb, false);
    }

    /**
     * Schedules specified task with given executor, and returns
     * completion handle that can be used to access the result or to cancel
     * the task. Later, if task completes successfully, the handle is marked
     * completed with the result that has been returned from the task.
     * If the task ends with an exception, the handle is marked
     * failed with cause being that exception. In such case, as a debugging
     * aid, current stack trace (that is, that of this method's invoker) is
     * appended to the original stack trace unless
     * the disableStackTraces parameter is set to false
     *
     * @param executor the executor to use
     * @param call the task to schedule
     * @param cb callback to invoke upon completion
     * @param disableStackTraces if true, does not append invoker stack trace
     *        to traces of exceptions thrown during task execution
     * @return completion handle that can be used to access the result or
     *         cancel the task.
     */
    public static AsyncTask start(final Executor executor, final Callable call,
                                  final Callback cb, boolean disableStackTraces)
    {
        final AsyncTask task = new AsyncTask(cb);
        executor.execute(task.createPerformer(call, disableStackTraces));
        return task;
    }

    /**
     * Creates a runnable that will execute specified call and then mark this
     * AsyncTask with the result of that call. If the call completes
     * successfully, this AsyncTask is marked
     * completed with the result returned by the call. If the call
     * throws an exception, the AsyncTask is marked as failed with
     * cause being that exception. The stack trace of the thread in which
     * the performer is created is appended to the failure cause stack
     * trace unless the disableStackTraces parameter is set to false.
     * <p>
     * This method is intended to be used by subclasses. Runnable
     * returned from this method should be executed only once -- subsequent
     * execution attempts will fail due to "task already completed" condition.
     *
     * @param call the call to execute
     * @param disableStackTraces if true, does not append invoker stack trace
     *        to traces of exceptions thrown during execution of the runnable
     * @return runnable that will execute specified call and
     *         set the result of this AsyncTask upon completion
     */
    protected Runnable createPerformer(final Callable call,
                                       boolean disableStackTraces)
    {
        final Throwable stackCxt;
        if (disableStackTraces) {
            stackCxt = null;
        }
        else {
            stackCxt = new Throwable();
        }

        return new Runnable() {
            public void run() {
                try {
                    synchronized (this) {
                        if (cancellationException != null) {
                            // async interruption before task started; propagate
                            throw cancellationException;
                        }
                        else {
                            cancellationHandler = createCancellationHandler(call);
                        }
                    }

                    Object result = call.call();
                    synchronized (this) {
                        cancellationHandler = null;
                        if (Thread.interrupted()) {
                            throw new InterruptedException("Task interrupted");
                        }
                    }
                    setCompleted(result);
                }
                catch (Throwable ex) {
                    if (stackCxt != null) {
                        appendContextStackTrace(ex, stackCxt);
                    }
                    setFailed(ex);
                }
            }
        };
    }

    /**
     * Overridable cancellation policy that governs what should be done upon
     * cancellation of tasks that have already started running.
     * This method is invoked in the worker thread by the runnable created
     * by {@link #createPerformer createPerformer}, before invoking the actual
     * call. The
     * cancellationHandler returned from this method is then stored in this
     * AsyncTask. Later, if the user attempts cancellation while the call is
     * already executing, the request is delegated to the handler which must
     * then supply the appropriate action and indication of success or failure.
     * <p>
     * The default implementation behaves as follows. If the callable for
     * which the cancellation handler is requested implements
     * {@link Cancellable} itself, that callable itself is returned as its own
     * cancellation handler; in other words, the cancellation policy will be
     * supplied directly by the callable implementation. Otherwise, the
     * default behavior is to interrupt the worker thread if the
     * mayInterruptIfRunning parameter is set to true, and fail in the other
     * case.
     *
     * @param call the call for which the cancellation handler is requested
     * @return cancellation handler that handles cancellation attempts
     *         while the task is already executing
     */
    protected Cancellable createCancellationHandler(Callable call) {
        if (call instanceof Cancellable) {
            return (Cancellable) call;
        }
        else {
            final Thread workThread = Thread.currentThread();
            return new Cancellable() {
                public boolean cancel(boolean mayInterruptIfRunning) {
                    if (mayInterruptIfRunning) {
                        workThread.interrupt();
                        return true;
                    }
                    return false;
                }
                public boolean isDone() {
                    return false;
                }
                public boolean isCancelled() {
                    return workThread.isInterrupted();
                }
            };
        }
    }

    private static void appendContextStackTrace(Throwable ex, Throwable cxt) {
        StackTraceElement[] exTrace = ex.getStackTrace();
        StackTraceElement[] cxtTrace = cxt.getStackTrace();
        StackTraceElement[] combinedTrace =
            new StackTraceElement[exTrace.length + cxtTrace.length];
        System.arraycopy(exTrace, 0, combinedTrace, 0,
                         exTrace.length);
        System.arraycopy(cxtTrace, 0, combinedTrace, exTrace.length, cxtTrace.length);
        ex.setStackTrace(combinedTrace);
    }

    private void invokeCallback(Object result, Throwable resultException) {
        if (callback != null) {
            try {
                if (resultException != null) {
                    callback.failed(resultException);
                } else {
                    callback.completed(result);
                }
            }
            catch (Throwable error) {
                // cannot propagate; caller of e.g. setCompleted() is not
                // interested in the exception from callback
                java.io.PrintStream s = System.err;
                synchronized (s) {
                    s.print("Exception occured within callback: ");
                    error.printStackTrace(s);
                }
            }
        }
    }

    public static interface Cancellable {
        boolean cancel(boolean mayInterruptIfRunning);
        boolean isCancelled();
    }
}
