/*******************************************************************************
 * Copyright (c) 2006, 2008 Wind River Systems and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 * 
 * Contributors:
 *     Wind River Systems - initial API and implementation
 *     Nokia			  - added StepWithProgress. Oct, 2008
 *******************************************************************************/
package org.eclipse.cdt.dsf.concurrent;

import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;

import org.eclipse.cdt.dsf.concurrent.RequestMonitor.ICanceledListener;
import org.eclipse.cdt.dsf.internal.DsfPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;

/**
 * Convenience class for implementing a series of commands that need to be 
 * executed asynchronously.  
 * <p>
 * Certain complex tasks require multiple commands to be executed in a chain, 
 * because for example result of one command is used as input into another 
 * command.  The typical DSF pattern of solving this problem is the following:
 * <li> 
 * <br> 1. original caller passes a RequestMonitor callback to a method and invokes it
 * <br> 2. the method is executed by a subsystem
 * <br> 3. when the method is finished it calls another method and passes 
 * the original callback to it
 * <br> 4. steps 2-3 are repeated number of times
 * <br> 5. when the last method in a chain is executed, it submits the original
 * RequestMonitor callback
 * </li>
 * <p>
 * This pattern is very useful in itself, but it proves very difficult to follow
 * because the methods can be scattered accross many classes and systems.  Also
 * if progress reporting, cancellability, and roll-back ability is required, it
 * has to be re-implemented every time.  The Sequence class tries to address
 * this problem by containing this pattern in a single class. 
 * 
 * @since 1.0
 */
@ThreadSafe
abstract public class Sequence extends DsfRunnable implements Future<Object> {

    /**
     * The abstract class that each step has to implement.  
     */
    abstract public static class Step {
        private Sequence fSequence;
        
        /**
         * Sets the sequence that this step belongs to.  It is only accessible 
         * by the sequence itself, and is not meant to be called by sequence
         * sub-classes. 
         */
        void setSequence(Sequence sequence) { fSequence = sequence; }
        
        /** Returns the sequence that this step is running in. */
        public Sequence getSequence() { return fSequence; }
        
        /** 
         * Executes the step.  Overriding classes should perform the 
         * work in this method.
         * @param rm Result token to submit to executor when step is finished.
         */
        public void execute(RequestMonitor rm) {
            rm.done();
        }

        /** 
         * Roll back gives the step implementation a chance to undo the 
         * operation that was performed by execute().
         * <br>
         * Note if the {@link #execute(RequestMonitor)} call completes with a 
         * non-OK status, then rollBack will not be called for that step.  
         * Instead it will be called for the previous step.
         * @param rm Result token to submit to executor when rolling back the step is finished.
         */
        public void rollBack(RequestMonitor rm) { 
            rm.done();
        }
        
        /** 
         * Returns the number of progress monitor ticks corresponding to this 
         * step.
         */
        public int getTicks() { return 1; }

        /**
         * Task name for this step. This will be displayed in the label of the
         * progress monitor of the owner sequence.
         * 
         * @return name of the task carried out by the step, can be 
         * <code>null</code>, in which case the overall task name will be used.
         * 
         * @since 1.1
         */
        public String getTaskName() {
            return ""; //$NON-NLS-1$
        }
    }

    /**
     * A step that will report execution progress by itself on the progress
     * monitor of the owner sequence.<br>
     * <br>
     * Note we don't offer a rollBack(RequestMonitor, IProgressMonitor) as we 
     * don't want end user to be able to cancel the rollback.
     * 
     * @since 1.1
     */
    abstract public static class StepWithProgress extends Step {

        @Override
        // don't allow subclass to implement this by "final" it.
        final public void execute(RequestMonitor rm) {
            assert false : "execute(RequestMonitor rm, IProgressMonitor pm) should be called instead"; //$NON-NLS-1$
        }

        /**
         * Execute the step with a progress monitor. Note the given progress
         * monitor is a sub progress monitor of the owner sequence which is
         * supposed to be fully controlled by the step. Namely the step should
         * call beginTask() and done() of the monitor.
         * 
         * @param rm
         * @param pm
         */
        public void execute(RequestMonitor rm, IProgressMonitor pm) {
            rm.done();
            pm.done();
        }
    }

    /** The synchronization object for this future */
    final Sync fSync = new Sync();

    /** 
     * Executor that this sequence is running in.  It is used by the sequence
     * to submit the runnables for steps, and for submitting the result.
     */  
    final private DsfExecutor fExecutor;
    
    /** 
     * Result callback to invoke when the sequence is finished.  Intended to 
     * be used when the sequence is created and invoked from the executor 
     * thread.  Otherwise, the {@link Future#get()} method is the appropriate
     * method of retrieving the result. 
     */
    final private RequestMonitor fRequestMonitor;
    
    /** Status indicating the success/failure of the test.  Used internally only. */
    @ConfinedToDsfExecutor("getExecutor") 
    private IStatus fStatus = Status.OK_STATUS;    
    
    @ConfinedToDsfExecutor("getExecutor") 
    private int fCurrentStepIdx = 0;
    
    /** Task name for this sequence used with the progress monitor */ 
    final private String fTaskName;
    
    /** Task name used when the sequence is being rolled back. */
    final private String fRollbackTaskName;
    
    final private IProgressMonitor fProgressMonitor;
    
    /** Convenience constructor with limited arguments. */
    public Sequence(DsfExecutor executor) {
        this(executor, new NullProgressMonitor(), "", "", null); //$NON-NLS-1$ //$NON-NLS-2$
    }

    /** 
     * Creates a sequence with a request monitor.  If the client cancels the 
     * request monitor, then the request monitors in the 
     * {@link Step#execute(RequestMonitor)}
     * implementations will immediately call the cancel listeners to notify.
     * 
     * @param executor The DSF executor which will be used to invoke all 
     * steps. 
     * @param rm The request monitor which will be invoked when the sequence
     * is completed.
     */
    public Sequence(DsfExecutor executor, RequestMonitor rm) {
        this(executor, new NullProgressMonitor(), "", "", rm); //$NON-NLS-1$ //$NON-NLS-2$
    }

    /**
     * Creates a sequence with a progress monitor.  If the progress monitor is
     * canceled, then request monitors in the 
     * {@link Step#execute(RequestMonitor)} implementations will need to call 
     * rm.isCanceled() to discover the cancellation.
     * @param executor The DSF executor which will be used to invoke all 
     * steps. 
     * @param pm Progress monitor for monitoring this sequence.  
     * @param taskName Name that will be used in call to 
     * {@link IProgressMonitor#beginTask(String, int)},when the task is 
     * started.
     * @param rollbackTaskName Name that will be used in call to 
     * {@link IProgressMonitor#subTask(String)} if the task is canceled or 
     * aborted. 
     * 
     * @since 1.1
     */
    public Sequence(DsfExecutor executor, IProgressMonitor pm, String taskName, String rollbackTaskName) {
        this(executor, pm, taskName, rollbackTaskName, new RequestMonitorWithProgress(ImmediateExecutor.getInstance(), pm));
    }
    
    /**
     * Creates a sequence with a request monitor that includes a progress 
     * monitor.  
     * @param executor The DSF executor which will be used to invoke all 
     * steps. 
     * @param rm The request monitor containing the progress monitor
     * @param taskName Name that will be used in call to 
     * {@link IProgressMonitor#beginTask(String, int)},when the task is 
     * started.
     * @param rollbackTaskName Name that will be used in call to 
     * {@link IProgressMonitor#subTask(String)} if the task is canceled or 
     * aborted. 
     * 
     * @since 1.1
     */
    public Sequence(DsfExecutor executor, RequestMonitorWithProgress rm, String taskName, String rollbackTaskName) {
        this(executor, rm.getProgressMonitor(), taskName, rollbackTaskName, rm); 
    }    
    
    /**
     * Constructor that initialized the steps and the result callback.
     * <p>Note: This constructor should not be used because it creates a 
     * potential ambiguity when one of the two monitors is canceled.</p>
     * 
     * @param executor The DSF executor which will be used to invoke all 
     * steps. 
     * @param pm Progress monitor for monitoring this sequence.  This 
     * parameter cannot be null.
     * @param taskName Name that will be used in call to 
     * {@link IProgressMonitor#beginTask(String, int)},when the task is 
     * started.
     * @param rollbackTaskName Name that will be used in call to 
     * {@link IProgressMonitor#subTask(String)} if the task is canceled or 
     * aborted. 
     * @param Result that will be submitted to executor when sequence is 
     * finished.  Can be null if calling from non-executor thread and using 
     * {@link Future#get()} method to wait for the sequence result.
     */
    private Sequence(DsfExecutor executor, IProgressMonitor pm, String taskName, String rollbackTaskName, RequestMonitor rm) {
        fExecutor = executor;
        fProgressMonitor = pm;
        fTaskName = taskName;
        fRollbackTaskName = rollbackTaskName;
        fRequestMonitor = rm;
        
        if (fRequestMonitor != null) {
            fRequestMonitor.addCancelListener(new ICanceledListener() {
                public void requestCanceled(RequestMonitor rm) {
                    fSync.doCancel();
                }
            });
        }
    }

    /** 
     * Returns the steps to be executed.  It is up to the deriving class to
     * supply the steps and to ensure that the list of steps will not be
     * modified after the sequence is constructed.
     * <p>
     * Steps are purposely not accepted as part of the DsfConstructor, in 
     * order to allow deriving classes to create the steps as a field.  And a 
     * setSteps() method is not provided, to guarantee that the steps will not
     * be modified once set (perhaps this is a bit paranoid, but oh well).  
     */
    abstract public Step[] getSteps();

    
    /** Returns the DSF executor for this sequence */
    public DsfExecutor getExecutor() { return fExecutor; }
    
    /**
     * Returns the RequestMonitor callback that is registered with the Sequence
     */
    public RequestMonitor getRequestMonitor() { return fRequestMonitor; }
    
    /**
     * The get method blocks until sequence is complete, but always returns null.
     * @see java.concurrent.Future#get
     */
    public Object get() throws InterruptedException, ExecutionException { 
        fSync.doGet();
        return null;
    }

    /**
     * The get method blocks until sequence is complete or until timeout is 
     * reached, but always returns null.
     * @see java.concurrent.Future#get
     */
    public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        fSync.doGet();
        return null;
    }

    /**
     * Don't try to interrupt the DSF executor thread, just ignore the request 
     * if set.
     * <p>If a request monitor was specified when creating a sequence, that 
     * request monitor will be canceled by this method as well.  The client 
     * can also use the request monitor's cancel method to cancel the sequence.
     * 
     * @see RequestMonitor#cancel()
     */
    public boolean cancel(boolean mayInterruptIfRunning) {
        // Cancel the request monitor first, to avoid a situation where
        // the request monitor is not canceled but the status is set
        // to canceled.
        if (fRequestMonitor != null) {
            fRequestMonitor.cancel();
        }
        return fSync.doCancel();
    }

    public boolean isCancelled() { return fSync.doIsCancelled(); }

    public boolean isDone() { return fSync.doIsDone(); }

    
    public void run() {
        // Change the state to running.
        if (fSync.doRun()) {
            // Set the reference to this sequence in each step.
            int totalTicks = 0;
            for (Step step : getSteps()) { 
                step.setSequence(this); 
                totalTicks += step.getTicks();
            }
            
            // Set the task name
            if (fTaskName != null) {
                fProgressMonitor.beginTask(fTaskName, totalTicks);
            }
            
            // Call the first step
            executeStep(0);
        } else {
            fSync.doFinish();
        }
    }

    /**
     * To be called only by the step implementation, Tells the sequence to 
     * submit the next step. 
     */
    private void executeStep(int nextStepIndex) {
        /*
         * At end of each step check progress monitor to see if it's cancelled.
         * If progress monitor is cancelled, mark the whole sequence as 
         * cancelled.
         */
        if (fProgressMonitor.isCanceled()) {
            cancel(false);
        }

        /*
         * If sequence was cencelled during last step (or before the sequence 
         * was ever executed), start rolling back the execution. 
         */
        if (isCancelled()) {
            cancelExecution();
            return;
        } 
         
        /*
         *  Check if we've reached the last step.  Note that if execution was
         *  cancelled during the last step (and thus the sequence is 
         *  technically finished, since it was cancelled it will be rolled 
         *  back. 
         */
        if (nextStepIndex >= getSteps().length) {
            finish();
            return;
        }

        // Proceed with executing next step.
        fCurrentStepIdx = nextStepIndex;
        try {
            Step currentStep = getSteps()[fCurrentStepIdx];
            final boolean stepControlsProgress = (currentStep instanceof StepWithProgress);

            RequestMonitor rm = new RequestMonitor(fExecutor, fRequestMonitor) {
                final private int fStepIdx = fCurrentStepIdx;

                @Override
                public void handleSuccess() {
                    // Check if we're still the correct step.
                    assert fStepIdx == fCurrentStepIdx;
                    if (!stepControlsProgress) {
                        // then sequence handles the progress report.
                        fProgressMonitor.worked(getSteps()[fStepIdx].getTicks());
                    }
                    executeStep(fStepIdx + 1);
                }
                
                @Override
                protected void handleCancel() {
                    Sequence.this.cancel(false);
                    cancelExecution();
                };
                
                @Override
                protected void handleErrorOrWarning() {
                    abortExecution(getStatus());                    
                };

                @Override
                public String toString() {
                    return "Sequence \"" + fTaskName + "\", result for executing step #" + fStepIdx + " = " + getStatus(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                }
            };

            fProgressMonitor.subTask(currentStep.getTaskName());
            
            if (stepControlsProgress) {

                // Create a sub-monitor that will be controlled by the step.
                SubProgressMonitor subMon = new SubProgressMonitor(fProgressMonitor, currentStep.getTicks(),
                    SubProgressMonitor.PREPEND_MAIN_LABEL_TO_SUBTASK);

                ((StepWithProgress) currentStep).execute(rm, subMon);
            } else { // regular Step
                currentStep.execute(rm);
            }

        } catch (Throwable t) {
            /*
             * Catching the exception here will only work if the exception 
             * happens within the execute method.  It will not work in cases 
             * when the execute submits other runnables, and the other runnables
             * encounter the exception.
             */ 
            abortExecution(new Status(
                IStatus.ERROR, DsfPlugin.PLUGIN_ID, 0, 
                "Unhandled exception when executing Sequence " + this + ", step #" + fCurrentStepIdx,  //$NON-NLS-1$ //$NON-NLS-2$
                t));
            
            /*
             * Since we caught the exception, it will not be logged by 
             * DefaultDsfExecutable.afterExecution().  So log it here.
             */
            DefaultDsfExecutor.logException(t);
        }
    }
     
    /**
     * To be called only by the step implementation. Tells the sequence to 
     * roll back next step. 
     */
    private void rollBackStep(int stepIdx) {
        // If we reach before step 0, finish roll back. 
        if (stepIdx < 0) {
            finish();
            return;
        }
        
        // Proceed with rolling back given step.
        fCurrentStepIdx = stepIdx;
        try {
            getSteps()[fCurrentStepIdx].rollBack(new RequestMonitor(fExecutor, null) {
                final private int fStepIdx = fCurrentStepIdx;
                @Override
                public void handleCompleted() {
                    // Check if we're still the correct step.
                    assert fStepIdx == fCurrentStepIdx;
             
                    // Proceed to the next step.
                    if (isSuccess()) {
                        // NOTE: The getTicks() is ticks for executing the step,
                        // not for rollBack,
                        // though it does not really hurt to use it here.
                        fProgressMonitor.worked(getSteps()[fStepIdx].getTicks());

                        rollBackStep(fStepIdx - 1);
                    } else {
                        abortRollBack(getStatus());
                    }
                }
                @Override
                public String toString() {
                    return "Sequence \"" + fTaskName + "\", result for rolling back step #" + fStepIdx + " = " + getStatus(); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                }                
            });
        } catch(Throwable t) {
            /*
             * Catching the exception here will only work if the exception 
             * happens within the execute method.  It will not work in cases 
             * when the execute submits other runnables, and the other runnables
             * encounter the exception.
             */ 
            abortRollBack(new Status(
                IStatus.ERROR, DsfPlugin.PLUGIN_ID, 0, 
                "Unhandled exception when rolling back Sequence " + this + ", step #" + fCurrentStepIdx,  //$NON-NLS-1$ //$NON-NLS-2$
                t));
            
            /*
             * Since we caught the exception, it will not be logged by 
             * DefaultDsfExecutable.afterExecution().  So log it here.
             */
            DefaultDsfExecutor.logException(t);
        }
    }

    /**
     * Tells the sequence that its execution is to be aborted and it 
     * should start rolling back the sequence as if it was cancelled by user.  
     */
    private void cancelExecution() {
        if (fRollbackTaskName != null) {
            fProgressMonitor.subTask(fRollbackTaskName);
        }
        fStatus = new Status(IStatus.CANCEL, DsfPlugin.PLUGIN_ID, -1, "Sequence \"" + fTaskName + "\" cancelled.", null); //$NON-NLS-1$ //$NON-NLS-2$
        if (fRequestMonitor != null) {
            fRequestMonitor.setStatus(fStatus);
        }

        /* 
         * No need to call fSync, it should have been taken care of by 
         * Future#cancel method. 
         * 
         * Note that we're rolling back starting with the current step, 
         * because the current step was fully executed.  This is unlike
         * abortExecution() where the current step caused the roll-back.
         */ 
        rollBackStep(fCurrentStepIdx);
    }

    /**
     * Tells the sequence that its execution is to be aborted and it 
     * should start rolling back the sequence as if it was cancelled by user.  
     */
    private void abortExecution(final IStatus error) {
        if (fRollbackTaskName != null) {
            fProgressMonitor.subTask(fRollbackTaskName);
        }
        fStatus = error;
        if (fRequestMonitor != null) {
            fRequestMonitor.setStatus(error);
        }
        fSync.doAbort(new CoreException(error));
        
        // Roll back starting with previous step, since current step failed.
        rollBackStep(fCurrentStepIdx - 1);
    }

    /**
     * Tells the sequence that that is rolling back, to abort roll back, and
     * notify the clients.  
     */
    private void abortRollBack(final IStatus error) {
        if (fRollbackTaskName != null) {
            fProgressMonitor.subTask(fRollbackTaskName);
        }
        
        /*
         * Compose new status based on previous status information and new
         * error information.
         */
        MultiStatus newStatus = 
            new MultiStatus(DsfPlugin.PLUGIN_ID, error.getCode(), 
                            "Sequence \"" + fTaskName + "\" failed while rolling back.", null); //$NON-NLS-1$ //$NON-NLS-2$
        newStatus.merge(error);
        newStatus.merge(fStatus);
        fStatus = newStatus;

        if (fRequestMonitor != null) {
            fRequestMonitor.setStatus(newStatus);
        }
        
        finish();
    }
    
    private void finish() {
        if (fRequestMonitor != null) fRequestMonitor.done();
        fSync.doFinish();
    }

    @SuppressWarnings("serial")
    final class Sync extends AbstractQueuedSynchronizer {
        private static final int STATE_RUNNING = 1;
        private static final int STATE_FINISHED = 2;
        private static final int STATE_ABORTING = 4;
        private static final int STATE_ABORTED = 8;
        private static final int STATE_CANCELLING = 16;
        private static final int STATE_CANCELLED = 32;

        private Throwable fException;

        private boolean isFinished(int state) {
            return (state & (STATE_FINISHED | STATE_CANCELLED | STATE_ABORTED)) != 0;
        }

        @Override
        protected int tryAcquireShared(int ignore) {
            return doIsDone()? 1 : -1;
        }

        @Override
        protected boolean tryReleaseShared(int ignore) {
            return true; 
        }

        boolean doIsCancelled() {
            int state = getState();
            return (state & (STATE_CANCELLING | STATE_CANCELLED)) != 0;
        }
        
        boolean doIsDone() {
            return isFinished(getState());
        }

        void doGet() throws InterruptedException, ExecutionException {
            acquireSharedInterruptibly(0);
            if (getState() == STATE_CANCELLED) throw new CancellationException();
            if (fException != null) throw new ExecutionException(fException);
        }

        void doGet(long nanosTimeout) throws InterruptedException, ExecutionException, TimeoutException {
            if (!tryAcquireSharedNanos(0, nanosTimeout)) throw new TimeoutException();                
            if (getState() == STATE_CANCELLED) throw new CancellationException();
            if (fException != null) throw new ExecutionException(fException);
        }

        void doAbort(Throwable t) {
            while(true) {
                int s = getState();
                if (isFinished(s)) return;
                if (compareAndSetState(s, STATE_ABORTING)) break;
            }
            fException = t;
        }

        boolean doCancel() {
            while(true) {
                int s = getState();
                if (isFinished(s)) return false;
                if (s == STATE_ABORTING) return false;
                if (compareAndSetState(s, STATE_CANCELLING)) break;
            }
            return true;
        }

        void doFinish() {
            while(true) {
                int s = getState();
                if (isFinished(s)) return;
                if (s == STATE_ABORTING) {
                    if (compareAndSetState(s, STATE_ABORTED)) break;
                } else if (s == STATE_CANCELLING) {
                    if (compareAndSetState(s, STATE_CANCELLED)) break;
                } else {
                    if (compareAndSetState(s, STATE_FINISHED)) break;
                }
            }
            releaseShared(0);
        }

        boolean doRun() {
            return compareAndSetState(0, STATE_RUNNING); 
        }
    }

}
