/* 
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you 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.apache.wiki.workflow;

import org.apache.wiki.api.core.Context;
import org.apache.wiki.api.exceptions.WikiException;
import org.apache.wiki.event.WikiEventEmitter;
import org.apache.wiki.event.WorkflowEvent;

import java.io.Serializable;
import java.security.Principal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * <p>
 * Sequence of {@link Step} objects linked together. Workflows are always initialized with a message key that denotes the name of the
 * Workflow, and a Principal that represents its owner.
 * </p>
 * <h2>Workflow lifecycle</h2>
 * A Workflow's state (obtained by {@link #getCurrentState()}) will be one of the following:
 * </p>
 * <ul>
 * <li><strong>{@link #CREATED}</strong>: after the Workflow has been instantiated, but before it has been started using the {@link #start(Context)}
 * method.</li>
 * <li><strong>{@link #RUNNING}</strong>: after the Workflow has been started using the {@link #start(Context)} method, but before it has
 * finished processing all Steps. Note that a Workflow can only be started once; attempting to start it again results in an
 * IllegalStateException. Callers can place the Workflow into the WAITING state by calling {@link #waitstate()}.</li>
 * <li><strong>{@link #WAITING}</strong>: when the Workflow has temporarily paused, for example because of a pending Decision. Once the
 * responsible actor decides what to do, the caller can change the Workflow back to the RUNNING state by calling the {@link #restart(Context)}
 * method (this is done automatically by the Decision class, for instance, when the {@link Decision#decide(Outcome, Context)} method is invoked)</li>
 * <li><strong>{@link #COMPLETED}</strong>: after the Workflow has finished processing all Steps, without errors.</li>
 * <li><strong>{@link #ABORTED}</strong>: if a Step has elected to abort the Workflow.</li>
 * </ul>
 * <h2>Steps and processing algorithm</h2>
 * <p>
 * Workflow Step objects can be of type {@link Decision}, {@link Task} or other Step subclasses. Decisions require user input, while Tasks
 * do not. See the {@link Step} class for more details.
 * </p>
 * <p>
 * After instantiating a new Workflow (but before telling it to {@link #start(Context)}), calling classes should specify the first Step by
 * executing the {@link #setFirstStep(Step)} method. Additional Steps can be chained by invoking the first step's
 * {@link Step#addSuccessor(Outcome, Step)} method.
 * </p>
 * <p>
 * When a Workflow's <code>start</code> method is invoked, the Workflow retrieves the first Step and processes it. This Step, and subsequent
 * ones, are processed as follows:
 * </p>
 * <ul>
 * <li>The Step's {@link Step#start()} method executes, which sets the start time.</li>
 * <li>The Step's {@link Step#execute(Context)} method is called to begin processing, which will return an Outcome to indicate completion,
 * continuation or errors:</li>
 * <ul>
 * <li>{@link Outcome#STEP_COMPLETE} indicates that the execution method ran without errors, and that the Step should be considered
 * "completed."</li>
 * <li>{@link Outcome#STEP_CONTINUE} indicates that the execution method ran without errors, but that the Step is not "complete" and should
 * be put into the WAITING state.</li>
 * <li>{@link Outcome#STEP_ABORT} indicates that the execution method encountered errors, and should abort the Step <em>and</em> the
 * Workflow as a whole. When this happens, the Workflow will set the current Step's Outcome to {@link Outcome#STEP_ABORT} and invoke the
 * Workflow's {@link #abort(Context)} method. The Step's processing errors, if any, can be retrieved by {@link Step#getErrors()}.</li>
 * </ul>
 * <li>The Outcome of the <code>execute</code> method also affects what happens next. Depending on the result (and assuming the Step did
 * not abort), the Workflow will either move on to the next Step or put the Workflow into the {@link Workflow#WAITING} state:</li>
 * <ul>
 * <li>If the Outcome denoted "completion" (<em>i.e.</em>, its {@link Step#isCompleted()} method returns <code>true</code>) then the Step
 * is considered complete; the Workflow looks up the next Step by calling the current Step's {@link Step#getSuccessor(Outcome)} method. If
 * <code>successor()</code> returns a non-<code>null</code> Step, the return value is marked as the current Step and added to the Workflow's
 * Step history. If <code>successor()</code> returns <code>null</code>, then the Workflow has no more Steps and it enters the
 * {@link #COMPLETED} state.</li>
 * <li>If the Outcome did not denote "completion" (<em>i.e.</em>, its {@link Step#isCompleted()} method returns <code>false</code>), then
 * the Step still has further work to do. The Workflow enters the {@link #WAITING} state and stops further processing until a caller
 * restarts it.</li>
 * </ul>
 * </ul>
 * </p>
 * <p>
 * The currently executing Step can be obtained by {@link #getCurrentStep()}. The actor for the current Step is returned by
 * {@link #getCurrentActor()}.
 * </p>
 * <p>
 * To provide flexibility for specific implementations, the Workflow class provides two additional features that enable Workflow
 * participants (<em>i.e.</em>, Workflow subclasses and Step/Task/Decision subclasses) to share context and state information. These two
 * features are <em>named attributes</em> and <em>message arguments</em>:
 * </p>
 * <ul>
 * <li><strong>Named attributes</strong> are simple key-value pairs that Workflow participants can get or set. Keys are Strings; values
 * can be any Object. Named attributes are set with {@link #setAttribute(String, Serializable)} and retrieved with {@link #getAttribute(String)}.</li>
 * <li><strong>Message arguments</strong> are used in combination with JSPWiki's {@link org.apache.wiki.i18n.InternationalizationManager} to
 * create language-independent user interface messages. The message argument array is retrieved via {@link #getMessageArguments()}; the
 * first two array elements will always be these: a String representing work flow owner's name, and a String representing the current
 * actor's name. Workflow participants can add to this array by invoking {@link #addMessageArgument(Serializable)}.</li>
 * </ul>
 * <h2>Example</h2>
 * <p>
 * Workflow Steps can be very powerful when linked together. JSPWiki provides two abstract subclasses classes that you can use to build
 * your own Workflows: Tasks and Decisions. As noted, Tasks are Steps that execute without user intervention, while Decisions require
 * actors (<em>aka</em> Principals) to take action. Decisions and Tasks can be mixed freely to produce some highly elaborate branching
 * structures.
 * </p>
 * <p>
 * Here is a simple case. For example, suppose you would like to create a Workflow that (a) executes a initialization Task, (b) pauses to
 * obtain an approval Decision from a user in the Admin group, and if approved, (c) executes a "finish" Task. Here's sample code that
 * illustrates how to do it:
 * </p>
 *
 * <pre>
 *    // Create workflow; owner is current user
 * 1  Workflow workflow = new Workflow( &quot; workflow.myworkflow &quot;, context.getCurrentUser() );
 *
 *    // Create custom initialization task
 * 2  Step initTask = new InitTask( this );
 *
 *    // Create finish task
 * 3  Step finishTask = new FinishTask( this );
 *
 *    // Create an intermediate decision step
 * 4  Principal actor = new GroupPrincipal( &quot;Admin&quot; );
 * 5  Step decision = new SimpleDecision( this, &quot;decision.AdminDecision&quot;, actor );
 *
 *    // Hook the steps together
 * 6  initTask.addSuccessor( Outcome.STEP_COMPLETE, decision );
 * 7  decision.addSuccessor( Outcome.DECISION_APPROVE, finishTask );
 *
 *    // Set workflow's first step
 * 8  workflow.setFirstStep( initTask );
 * </pre>
 *
 * <p>
 * Some comments on the source code:
 * </p>
 * <ul>
 * <li>Line 1 instantiates the workflow with a sample message key and designated owner Principal, in this case the current wiki user</li>
 * <li>Lines 2 and 3 instantiate the custom Task subclasses, which contain the business logic</li>
 * <li>Line 4 creates the relevant GroupPrincipal for the <code>Admin</code> group, who will be the actor in the Decision step</li>
 * <li>Line 5 creates the Decision step, passing the Workflow, sample message key, and actor in the constructor</li>
 * <li>Line 6 specifies that if the InitTask's Outcome signifies "normal completion" (STEP_COMPLETE), the SimpleDecision step should be
 * invoked next</li>
 * <li>Line 7 specifies that if the actor (anyone possessing the <code>Admin</code> GroupPrincipal) selects DECISION_APPROVE, the FinishTask
 * step should be invoked</li>
 * <li>Line 8 adds the InitTask (and all of its successor Steps, nicely wired together) to the workflow</li>
 * </ul>
 */
public class Workflow implements Serializable {

    private static final long serialVersionUID = 5228149040690660032L;

    private static final AtomicInteger idsCounter = new AtomicInteger( 1 );

    /** ID value: the workflow ID has not been set. */
    public static final int ID_NOT_SET = 0;

    /** State value: Workflow completed all Steps without errors. */
    public static final int COMPLETED = 50;

    /** State value: Workflow aborted before completion. */
    public static final int ABORTED = 40;

    /** State value: Workflow paused, typically because a Step returned an Outcome that doesn't signify "completion." */
    public static final int WAITING = 30;

    /** State value: Workflow started, and is running. */
    public static final int RUNNING = -1;

    /** State value: Workflow instantiated, but not started. */
    public static final int CREATED = -2;

    /** attribute map. */
    private Map< String, Serializable > m_attributes;

    /** The initial Step for this Workflow. */
    private Step m_firstStep;

    /** Flag indicating whether the Workflow has started yet. */
    private boolean m_started;

    private final LinkedList< Step > m_history;

    private int m_id;

    private final String m_key;

    private final Principal m_owner;

    private final List<Serializable> m_messageArgs;

    private int m_state;

    private Step m_currentStep;

    /**
     * Constructs a new Workflow object with a supplied message key, owner Principal, and undefined unique identifier {@link #ID_NOT_SET}.
     * Once instantiated the Workflow is considered to be in the {@link #CREATED} state; a caller must explicitly invoke the
     * {@link #start(Context)} method to begin processing.
     *
     * @param messageKey the message key used to construct a localized workflow name, such as <code>workflow.saveWikiPage</code>
     * @param owner the Principal who owns the Workflow. Typically, this is the user who created and submitted it
     */
    public Workflow( final String messageKey, final Principal owner ) {
        m_attributes = new ConcurrentHashMap<>();
        m_currentStep = null;
        m_history = new LinkedList<>();
        m_id = idsCounter.getAndIncrement();
        m_key = messageKey;
        m_messageArgs = new ArrayList<>();
        m_owner = owner;
        m_started = false;
        m_state = CREATED;
        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.CREATED );
    }

    /**
     * Aborts the Workflow by setting the current Step's Outcome to {@link Outcome#STEP_ABORT}, and the Workflow's overall state to
     * {@link #ABORTED}. It also appends the aborted Step into the workflow history, and sets the current step to <code>null</code>.
     * If the Step is a Decision, it is removed from the DecisionQueue. This method can be called at any point in the lifecycle prior
     * to completion, but it cannot be called twice. It finishes by calling the {@link #cleanup()} method to flush retained objects.
     * If the Workflow had been previously aborted, this method throws an IllegalStateException.
     */
    public final synchronized void abort( final Context context ) {
        // Check corner cases: previous abort or completion
        if( m_state == ABORTED ) {
            throw new IllegalStateException( "The workflow has already been aborted." );
        }
        if( m_state == COMPLETED ) {
            throw new IllegalStateException( "The workflow has already completed." );
        }

        if( m_currentStep != null ) {
            if( m_currentStep instanceof Decision ) {
                WikiEventEmitter.fireWorkflowEvent( m_currentStep, WorkflowEvent.DQ_REMOVAL, context );
            }
            m_currentStep.setOutcome( Outcome.STEP_ABORT );
            m_history.addLast( m_currentStep );
        }
        m_state = ABORTED;
        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.ABORTED );
        cleanup();
    }

    /**
     * Appends a message argument object to the array returned by {@link #getMessageArguments()}. The object <em>must</em> be an type
     * used by the {@link java.text.MessageFormat}: String, Date, or Number (BigDecimal, BigInteger, Byte, Double, Float, Integer, Long,
     * Short). If the object is not of type String, Number or Date, this method throws an IllegalArgumentException.
     *
     * @param obj the object to add
     */
    public final void addMessageArgument( final Serializable obj ) {
        if( obj instanceof String || obj instanceof Date || obj instanceof Number ) {
            m_messageArgs.add( obj );
            return;
        }
        throw new IllegalArgumentException( "Message arguments must be of type String, Date or Number." );
    }

    /**
     * Returns the actor Principal responsible for the current Step. If there is
     * no current Step, this method returns <code>null</code>.
     *
     * @return the current actor
     */
    public final synchronized Principal getCurrentActor() {
        if( m_currentStep == null ) {
            return null;
        }
        return m_currentStep.getActor();
    }

    /**
     * Returns the workflow state: {@link #CREATED}, {@link #RUNNING}, {@link #WAITING}, {@link #COMPLETED} or {@link #ABORTED}.
     *
     * @return the workflow state
     */
    public final int getCurrentState()
    {
        return m_state;
    }

    /**
     * Returns the current Step, or <code>null</code> if the workflow has not started or already completed.
     *
     * @return the current step
     */
    public final Step getCurrentStep()
    {
        return m_currentStep;
    }

    /**
     * Retrieves a named Object associated with this Workflow. If the Workflow has completed or aborted, this method always returns
     * <code>null</code>.
     *
     * @param attr the name of the attribute
     * @return the value
     */
    public final Object getAttribute( final String attr ) {
        return m_attributes.get( attr );
    }

    /**
     * Retrieves workflow's attributes.
     *
     * @return workflow's attributes.
     */
    public final Map< String, Serializable > getAttributes() {
        return m_attributes;
    }

    /**
     * The end time for this Workflow, expressed as a system time number. This value is equal to the end-time value returned by the final
     * Step's {@link Step#getEndTime()} method, if the workflow has completed. Otherwise, this method returns {@link Step#TIME_NOT_SET}.
     *
     * @return the end time
     */
    public final Date getEndTime() {
        if( isCompleted() ) {
            final Step last = m_history.getLast();
            if( last != null ) {
                return last.getEndTime();
            }
        }
        return Step.TIME_NOT_SET;
    }

    /**
     * Returns the unique identifier for this Workflow. If not set, this method returns ID_NOT_SET ({@value #ID_NOT_SET}).
     *
     * @return the unique identifier
     */
    public final synchronized int getId()
    {
        return m_id;
    }

    /**
     * <p>
     * Returns an array of message arguments, used by {@link java.text.MessageFormat} to create localized messages. The first
     * two array elements will always be these:
     * </p>
     * <ul>
     * <li>String representing the name of the workflow owner (<em>i.e.</em>,{@link #getOwner()})</li>
     * <li>String representing the name of the current actor (<em>i.e.</em>,{@link #getCurrentActor()}).
     * If the current step is <code>null</code> because the workflow hasn't started or has already
     * finished, the value of this argument will be a dash character (<code>-</code>)</li>
     * </ul>
     * <p>
     * Workflow and Step subclasses are free to append items to this collection with {@link #addMessageArgument(Serializable)}.
     * </p>
     *
     * @return the array of message arguments
     */
    public final Serializable[] getMessageArguments() {
        final List< Serializable > args = new ArrayList<>();
        args.add( m_owner.getName() );
        final Principal actor = getCurrentActor();
        args.add( actor == null ? "-" : actor.getName() );
        args.addAll( m_messageArgs );
        return args.toArray( new Serializable[0] );
    }

    /**
     * Returns an i18n message key for the name of this workflow; for example,
     * <code>workflow.saveWikiPage</code>.
     *
     * @return the name
     */
    public final String getMessageKey()
    {
        return m_key;
    }

    /**
     * The owner Principal on whose behalf this Workflow is being executed; that is, the user who created the workflow.
     *
     * @return the name of the Principal who owns this workflow
     */
    public final Principal getOwner()
    {
        return m_owner;
    }

    /**
     * The start time for this Workflow, expressed as a system time number. This value is equal to the start-time value returned by the
     * first Step's {@link Step#getStartTime()} method, if the workflow has started already. Otherwise, this method returns
     * {@link Step#TIME_NOT_SET}.
     *
     * @return the start time
     */
    public final Date getStartTime()
    {
        return isStarted() ? m_firstStep.getStartTime() : Step.TIME_NOT_SET;
    }

    /**
     * Returns a Step history for this Workflow as a List, chronologically, from the first Step to the currently executing one. The first
     * step is the first item in the array. If the Workflow has not started, this method returns a zero-length array.
     *
     * @return an array of Steps representing those that have executed, or are currently executing
     */
    public final List< Step > getHistory()
    {
        return Collections.unmodifiableList( m_history );
    }

    /**
     * Returns <code>true</code> if the workflow had been previously aborted.
     *
     * @return the result
     */
    public final boolean isAborted()
    {
        return m_state == ABORTED;
    }

    /**
     * Determines whether this Workflow is completed; that is, if it has no additional Steps to perform. If the last Step in the workflow is
     * finished, this method will return <code>true</code>.
     *
     * @return <code>true</code> if the workflow has been started but has no more steps to perform; <code>false</code> if not.
     */
    public final synchronized boolean isCompleted() {
        // If current step is null, then we're done
        return m_started && m_state == COMPLETED;
    }

    /**
     * Determines whether this Workflow has started; that is, its {@link #start(Context)} method has been executed.
     *
     * @return <code>true</code> if the workflow has been started; <code>false</code> if not.
     */
    public final boolean isStarted()
    {
        return m_started;
    }

    /**
     * Convenience method that returns the predecessor of the current Step. This method simply examines the Workflow history and returns the
     * second-to-last Step.
     *
     * @return the predecessor, or <code>null</code> if the first Step is currently executing
     */
    public final Step getPreviousStep()
    {
        return previousStep( m_currentStep );
    }

    /**
     * Restarts the Workflow from the {@link #WAITING} state and puts it into the {@link #RUNNING} state again. If the Workflow had not
     * previously been paused, this method throws an IllegalStateException. If any of the Steps in this Workflow throw a WikiException,
     * the Workflow will abort and propagate the exception to callers.
     *
     * @param context current wiki context
     * @throws WikiException if the current task's {@link Task#execute( Context )} method throws an exception
     */
    public final synchronized void restart( final Context context ) throws WikiException {
        if( m_state != WAITING ) {
            throw new IllegalStateException( "Workflow is not paused; cannot restart." );
        }
        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.STARTED );
        m_state = RUNNING;
        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.RUNNING );

        // Process current step
        try {
            processCurrentStep( context );
        } catch( final WikiException e ) {
            abort( context );
            throw e;
        }
    }

    /**
     * Temporarily associates an object with this Workflow, as a named attribute, for the duration of workflow execution. The passed
     * object can be anything required by an executing Step, although it <em>should</em> be serializable. Note that when the workflow
     * completes or aborts, all attributes will be cleared.
     *
     * @param attr the attribute name
     * @param obj  the value
     */
    public final void setAttribute( final String attr, final Serializable obj ) {
        m_attributes.put( attr, obj );
    }

    /**
     * Sets the first Step for this Workflow, which will be executed immediately
     * after the {@link #start( Context )} method executes. Note than the Step is not
     * marked as the "current" step or added to the Workflow history until the
     * {@link #start( Context )} method is called.
     *
     * @param step the first step for the workflow
     */
    public final synchronized void setFirstStep( final Step step )
    {
        m_firstStep = step;
    }

    /**
     * Sets the unique identifier for this Workflow.
     *
     * @param id the unique identifier
     */
    public final synchronized void setId( final int id )
    {
        this.m_id = id;
    }

    /**
     * Starts the Workflow and sets the state to {@link #RUNNING}. If the Workflow has already been started (or previously aborted), this
     * method returns an {@linkplain IllegalStateException}. If any of the Steps in this Workflow throw a WikiException, the Workflow will
     * abort and propagate the exception to callers.
     *
     * @param context current wiki context.
     * @throws WikiException if the current Step's {@link Step#start()} method throws an exception of any kind
     */
    public final synchronized void start( final Context context ) throws WikiException {
        if( m_state == ABORTED ) {
            throw new IllegalStateException( "Workflow cannot be started; it has already been aborted." );
        }
        if( m_started ) {
            throw new IllegalStateException( "Workflow has already started." );
        }
        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.STARTED );
        m_started = true;
        m_state = RUNNING;

        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.RUNNING );
        // Mark the first step as the current one & add to history
        m_currentStep = m_firstStep;
        m_history.add( m_currentStep );

        // Process current step
        try {
            processCurrentStep( context );
        } catch( final WikiException e ) {
            abort( context );
            throw e;
        }
    }

    /**
     * Sets the Workflow in the {@link #WAITING} state. If the Workflow is not running or has already been paused, this method throws an
     * IllegalStateException. Once paused, the Workflow can be un-paused by executing the {@link #restart(Context)} method.
     */
    public final synchronized void waitstate() {
        if ( m_state != RUNNING ) {
            throw new IllegalStateException( "Workflow is not running; cannot pause." );
        }
        m_state = WAITING;
        WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.WAITING );
    }

    /**
     * Clears the attribute map and sets the current step field to <code>null</code>.
     */
    protected void cleanup() {
        m_currentStep = null;
        m_attributes = null;
    }

    /**
     * Protected helper method that changes the Workflow's state to {@link #COMPLETED} and sets the current Step to <code>null</code>. It
     * calls the {@link #cleanup()} method to flush retained objects. This method will no-op if it has previously been called.
     */
    protected final synchronized void complete() {
        if( !isCompleted() ) {
            m_state = COMPLETED;
            WikiEventEmitter.fireWorkflowEvent( this, WorkflowEvent.COMPLETED );
            cleanup();
        }
    }

    /**
     * Protected method that returns the predecessor for a supplied Step.
     *
     * @param step the Step for which the predecessor is requested
     * @return its predecessor, or <code>null</code> if the first Step was supplied.
     */
    protected final Step previousStep( final Step step ) {
        final int index = m_history.indexOf( step );
        return index < 1 ? null : m_history.get( index - 1 );
    }

    /**
     * Protected method that processes the current Step by calling {@link Step#execute( Context )}. If the <code>execute</code> throws an
     * exception, this method will propagate the exception immediately to callers without aborting.
     *
     * @throws WikiException if the current Step's {@link Step#start()} method throws an exception of any kind
     */
    protected final void processCurrentStep( final Context context ) throws WikiException {
        while ( m_currentStep != null ) {
            // Start and execute the current step
            if( !m_currentStep.isStarted() ) {
                m_currentStep.start();
            }
            final Outcome result = m_currentStep.execute( context );
            if( Outcome.STEP_ABORT.equals( result ) ) {
                abort( context );
                break;
            }

            if( !m_currentStep.isCompleted() ) {
                m_currentStep.setOutcome( result );
            }

            // Get the execution Outcome; if not complete, pause workflow and exit
            final Outcome outcome = m_currentStep.getOutcome();
            if ( !outcome.isCompletion() ) {
                waitstate();
                break;
            }

            // Get the next Step; if null, we're done
            final Step nextStep = m_currentStep.getSuccessor( outcome );
            if ( nextStep == null ) {
                complete();
                break;
            }

            // Add the next step to Workflow history, and mark as current
            m_history.add( nextStep );
            m_currentStep = nextStep;
        }

    }

}
