/* Copyright 2010-2012 Alfresco Software, Ltd.
 * Copyright 2012 Thorben Lindhauer
 * 
 * Licensed under the Apache License, ersion 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.activiti.engine.impl.pvm.runtime;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.activiti.engine.impl.pvm.PvmActivity;
import org.activiti.engine.impl.pvm.PvmException;
import org.activiti.engine.impl.pvm.PvmExecution;
import org.activiti.engine.impl.pvm.PvmProcessDefinition;
import org.activiti.engine.impl.pvm.PvmProcessElement;
import org.activiti.engine.impl.pvm.PvmProcessInstance;
import org.activiti.engine.impl.pvm.PvmTransition;
import org.activiti.engine.impl.pvm.delegate.ActivityExecution;
import org.activiti.engine.impl.pvm.delegate.ExecutionListenerExecution;
import org.activiti.engine.impl.pvm.delegate.SignallableActivityBehavior;
import org.activiti.engine.impl.pvm.process.ActivityImpl;
import org.activiti.engine.impl.pvm.process.ProcessDefinitionImpl;
import org.activiti.engine.impl.pvm.process.TransitionImpl;

/**
 * @author Tom Baeyens
 * @author Joram Barrez
 */
public class ExecutionImpl implements Serializable, ActivityExecution,
		ExecutionListenerExecution, PvmExecution, InterpretableExecution {

	private static final long serialVersionUID = 1L;

	private static Logger log = Logger.getLogger(ExecutionImpl.class.getName());

	// current position
	// /////////////////////////////////////////////////////////

	protected ProcessDefinitionImpl processDefinition;

	/** current activity */
	protected ActivityImpl activity;

	/** current transition. is null when there is no transition being taken. */
	protected TransitionImpl transition = null;

	/**
	 * the process instance. this is the root of the execution tree. the
	 * processInstance of a process instance is a self reference.
	 */
	protected ExecutionImpl processInstance;

	/** the parent execution */
	protected ExecutionImpl parent;

	/** nested executions representing scopes or concurrent paths */
	protected List<ExecutionImpl> executions;

	/** super execution, not-null if this execution is part of a subprocess */
	protected ExecutionImpl superExecution;

	/**
	 * reference to a subprocessinstance, not-null if currently subprocess is
	 * started from this execution
	 */
	protected ExecutionImpl subProcessInstance;

	/** only available until the process instance is started */
	protected StartingExecution startingExecution;

	// state/type of execution
	// //////////////////////////////////////////////////

	/**
	 * indicates if this execution represents an active path of execution.
	 * Executions are made inactive in the following situations:
	 * <ul>
	 * <li>an execution enters a nested scope</li>
	 * <li>an execution is split up into multiple concurrent executions, then
	 * the parent is made inactive.</li>
	 * <li>an execution has arrived in a parallel gateway or join and that join
	 * has not yet activated/fired.</li>
	 * <li>an execution is ended.</li>
	 * </ul>
	 */
	protected boolean isActive = true;
	protected boolean isScope = true;
	protected boolean isConcurrent = false;
	protected boolean isEnded = false;
	protected boolean isEventScope = false;

	protected Map<String, Object> variables = null;

	// events
	// ///////////////////////////////////////////////////////////////////

	protected String eventName;
	protected PvmProcessElement eventSource;
	protected int executionListenerIndex = 0;

	// cascade deletion ////////////////////////////////////////////////////////

	protected boolean deleteRoot;
	protected String deleteReason;

	// replaced by
	// //////////////////////////////////////////////////////////////

	/**
	 * when execution structure is pruned during a takeAll, then the original
	 * execution has to be resolved to the replaced execution.
	 * 
	 * @see {@link #takeAll(List, List)} {@link OutgoingExecution}
	 */
	protected ExecutionImpl replacedBy;

	// atomic operations
	// ////////////////////////////////////////////////////////

	/**
	 * next operation. process execution is in fact runtime interpretation of
	 * the process model. each operation is a logical unit of interpretation of
	 * the process. so sequentially processing the operations drives the
	 * interpretation or execution of a process.
	 * 
	 * @see AtomicOperation
	 * @see #performOperation(AtomicOperation)
	 */
	protected AtomicOperation nextOperation;
	protected boolean isOperating = false;

	/* Default constructor for ibatis/jpa/etc. */
	public ExecutionImpl() {
	}

	public ExecutionImpl(ActivityImpl initial) {
		startingExecution = new StartingExecution(initial);
	}

	// lifecycle methods
	// ////////////////////////////////////////////////////////

	/**
	 * creates a new execution. properties processDefinition, processInstance
	 * and activity will be initialized.
	 */
	public ExecutionImpl createExecution() {
		// create the new child execution
		ExecutionImpl createdExecution = newExecution();

		// manage the bidirectional parent-child relation
		ensureExecutionsInitialized();
		executions.add(createdExecution);
		createdExecution.setParent(this);

		// initialize the new execution
		createdExecution.setProcessDefinition(getProcessDefinition());
		createdExecution.setProcessInstance(getProcessInstance());
		createdExecution.setActivity(getActivity());

		return createdExecution;
	}

	/** instantiates a new execution. can be overridden by subclasses */
	protected ExecutionImpl newExecution() {
		return new ExecutionImpl();
	}

	public PvmProcessInstance createSubProcessInstance(
			PvmProcessDefinition processDefinition) {
		ExecutionImpl subProcessInstance = newExecution();

		// manage bidirectional super-subprocess relation
		subProcessInstance.setSuperExecution(this);
		this.setSubProcessInstance(subProcessInstance);

		// Initialize the new execution
		subProcessInstance
				.setProcessDefinition((ProcessDefinitionImpl) processDefinition);
		subProcessInstance.setProcessInstance(subProcessInstance);

		return subProcessInstance;
	}

	public void initialize() {
	}

	public void destroy() {
		setScope(false);
	}

	public void remove() {
		ensureParentInitialized();
		if (parent != null) {
			parent.ensureExecutionsInitialized();
			parent.executions.remove(this);
		}

		// remove event scopes:
		List<InterpretableExecution> childExecutions = new ArrayList<InterpretableExecution>(
				getExecutions());
		for (InterpretableExecution childExecution : childExecutions) {
			if (childExecution.isEventScope()) {
				log.fine("removing eventScope " + childExecution);
				childExecution.destroy();
				childExecution.remove();
			}
		}
	}

	// parent
	// ///////////////////////////////////////////////////////////////////

	/** ensures initialization and returns the parent */
	public ExecutionImpl getParent() {
		ensureParentInitialized();
		return parent;
	}

	/**
	 * all updates need to go through this setter as subclasses can override
	 * this method
	 */
	public void setParent(InterpretableExecution parent) {
		this.parent = (ExecutionImpl) parent;
	}

	/**
	 * must be called before memberfield parent is used. can be used by
	 * subclasses to provide parent member field initialization.
	 */
	protected void ensureParentInitialized() {
	}

	// executions
	// ///////////////////////////////////////////////////////////////

	/** ensures initialization and returns the non-null executions list */
	public List<ExecutionImpl> getExecutions() {
		ensureExecutionsInitialized();
		return executions;
	}

	public ExecutionImpl getSuperExecution() {
		ensureSuperExecutionInitialized();
		return superExecution;
	}

	public void setSuperExecution(ExecutionImpl superExecution) {
		this.superExecution = superExecution;
		if (superExecution != null) {
			superExecution.setSubProcessInstance(null);
		}
	}

	// Meant to be overridden by persistent subclasseses
	protected void ensureSuperExecutionInitialized() {
	}

	public ExecutionImpl getSubProcessInstance() {
		ensureSubProcessInstanceInitialized();
		return subProcessInstance;
	}

	public void setSubProcessInstance(InterpretableExecution subProcessInstance) {
		this.subProcessInstance = (ExecutionImpl) subProcessInstance;
	}

	// Meant to be overridden by persistent subclasses
	protected void ensureSubProcessInstanceInitialized() {
	}

	public void deleteCascade(String deleteReason) {
		this.deleteReason = deleteReason;
		this.deleteRoot = true;
		performOperation(AtomicOperation.DELETE_CASCADE);
	}

	/**
	 * removes an execution. if there are nested executions, those will be ended
	 * recursively. if there is a parent, this method removes the bidirectional
	 * relation between parent and this execution.
	 */
	public void end() {
		isActive = false;
		isEnded = true;
		performOperation(AtomicOperation.ACTIVITY_END);
	}

	/** searches for an execution positioned in the given activity */
	public ExecutionImpl findExecution(String activityId) {
		if ((getActivity() != null)
				&& (getActivity().getId().equals(activityId))) {
			return this;
		}
		for (ExecutionImpl nestedExecution : getExecutions()) {
			ExecutionImpl result = nestedExecution.findExecution(activityId);
			if (result != null) {
				return result;
			}
		}
		return null;
	}

	public List<String> findActiveActivityIds() {
		List<String> activeActivityIds = new ArrayList<String>();
		collectActiveActivityIds(activeActivityIds);
		return activeActivityIds;
	}

	protected void collectActiveActivityIds(List<String> activeActivityIds) {
		ensureActivityInitialized();
		if (isActive && activity != null) {
			activeActivityIds.add(activity.getId());
		}
		ensureExecutionsInitialized();
		for (ExecutionImpl execution : executions) {
			execution.collectActiveActivityIds(activeActivityIds);
		}
	}

	/**
	 * must be called before memberfield executions is used. can be used by
	 * subclasses to provide executions member field initialization.
	 */
	protected void ensureExecutionsInitialized() {
		if (executions == null) {
			executions = new ArrayList<ExecutionImpl>();
		}
	}

	// process definition
	// ///////////////////////////////////////////////////////

	/** ensures initialization and returns the process definition. */
	public ProcessDefinitionImpl getProcessDefinition() {
		ensureProcessDefinitionInitialized();
		return processDefinition;
	}

	public String getProcessDefinitionId() {
		return getProcessDefinition().getId();
	}

	/**
	 * for setting the process definition, this setter must be used as
	 * subclasses can override
	 */

	/**
	 * must be called before memberfield processDefinition is used. can be used
	 * by subclasses to provide processDefinition member field initialization.
	 */
	protected void ensureProcessDefinitionInitialized() {
	}

	// process instance
	// /////////////////////////////////////////////////////////

	/** ensures initialization and returns the process instance. */
	public ExecutionImpl getProcessInstance() {
		ensureProcessInstanceInitialized();
		return processInstance;
	}

	public String getProcessInstanceId() {
		return getProcessInstance().getId();
	}

	public String getBusinessKey() {
		return getProcessInstance().getBusinessKey();
	}

	public String getProcessBusinessKey() {
		return getProcessInstance().getBusinessKey();
	}

	/**
	 * for setting the process instance, this setter must be used as subclasses
	 * can override
	 */
	public void setProcessInstance(InterpretableExecution processInstance) {
		this.processInstance = (ExecutionImpl) processInstance;
	}

	/**
	 * must be called before memberfield processInstance is used. can be used by
	 * subclasses to provide processInstance member field initialization.
	 */
	protected void ensureProcessInstanceInitialized() {
	}

	// activity
	// /////////////////////////////////////////////////////////////////

	/** ensures initialization and returns the activity */
	public ActivityImpl getActivity() {
		ensureActivityInitialized();
		return activity;
	}

	/**
	 * sets the current activity. can be overridden by subclasses. doesn't
	 * require initialization.
	 */
	public void setActivity(ActivityImpl activity) {
		this.activity = activity;
	}

	/**
	 * must be called before the activity member field or getActivity() is
	 * called
	 */
	protected void ensureActivityInitialized() {
	}

	// scopes
	// ///////////////////////////////////////////////////////////////////

	protected void ensureScopeInitialized() {
	}

	public boolean isScope() {
		return isScope;
	}

	public void setScope(boolean isScope) {
		this.isScope = isScope;
	}

	// process instance start implementation
	// ////////////////////////////////////

	public void start() {
		if (startingExecution == null && isProcessInstance()) {
			startingExecution = new StartingExecution(
					processDefinition.getInitial());
		}
		performOperation(AtomicOperation.PROCESS_START);
	}

	// methods that translate to operations
	// /////////////////////////////////////

	public void signal(String signalName, Object signalData) {
		ensureActivityInitialized();
		SignallableActivityBehavior activityBehavior = (SignallableActivityBehavior) activity
				.getActivityBehavior();
		try {
			activityBehavior.signal(this, signalName, signalData);
		} catch (RuntimeException e) {
			throw e;
		} catch (Exception e) {
			throw new PvmException("couldn't process signal '" + signalName
					+ "' on activity '" + activity.getId() + "': "
					+ e.getMessage(), e);
		}
	}

	public void take(PvmTransition transition) {
		if (this.transition != null) {
			throw new PvmException("already taking a transition");
		}
		if (transition == null) {
			throw new PvmException("transition is null");
		}
		setTransition((TransitionImpl) transition);
		performOperation(AtomicOperation.TRANSITION_NOTIFY_LISTENER_END);
	}

	public void executeActivity(PvmActivity activity) {
		setActivity((ActivityImpl) activity);
		performOperation(AtomicOperation.ACTIVITY_START);
	}

	public List<ActivityExecution> findInactiveConcurrentExecutions(
			PvmActivity activity) {
		List<ActivityExecution> inactiveConcurrentExecutionsInActivity = new ArrayList<ActivityExecution>();
		List<ActivityExecution> otherConcurrentExecutions = new ArrayList<ActivityExecution>();
		if (isConcurrent()) {
			List<? extends ActivityExecution> concurrentExecutions = getParent()
					.getExecutions();
			for (ActivityExecution concurrentExecution : concurrentExecutions) {
				if (concurrentExecution.getActivity() == activity) {
					if (concurrentExecution.isActive()) {
						throw new PvmException(
								"didn't expect active execution in " + activity
										+ ". bug?");
					}
					inactiveConcurrentExecutionsInActivity
							.add(concurrentExecution);
				} else {
					otherConcurrentExecutions.add(concurrentExecution);
				}
			}
		} else {
			if (!isActive()) {
				inactiveConcurrentExecutionsInActivity.add(this);
			} else {
				otherConcurrentExecutions.add(this);
			}
		}
		if (log.isLoggable(Level.FINE)) {
			log.fine("inactive concurrent executions in '" + activity + "': "
					+ inactiveConcurrentExecutionsInActivity);
			log.fine("other concurrent executions: "
					+ otherConcurrentExecutions);
		}
		return inactiveConcurrentExecutionsInActivity;
	}

	@SuppressWarnings("unchecked")
	public void takeAll(List<PvmTransition> transitions,
			List<ActivityExecution> recyclableExecutions) {
		transitions = new ArrayList<PvmTransition>(transitions);
		recyclableExecutions = (recyclableExecutions != null ? new ArrayList<ActivityExecution>(
				recyclableExecutions) : new ArrayList<ActivityExecution>());

		if (recyclableExecutions.size() > 1) {
			for (ActivityExecution recyclableExecution : recyclableExecutions) {
				if (((ExecutionImpl) recyclableExecution).isScope()) {
					throw new PvmException(
							"joining scope executions is not allowed");
				}
			}
		}

		ExecutionImpl concurrentRoot = ((isConcurrent && !isScope) ? getParent()
				: this);
		List<ExecutionImpl> concurrentActiveExecutions = new ArrayList<ExecutionImpl>();
		for (ExecutionImpl execution : concurrentRoot.getExecutions()) {
			if (execution.isActive()) {
				concurrentActiveExecutions.add(execution);
			}
		}

		if (log.isLoggable(Level.FINE)) {
			log.fine("transitions to take concurrent: " + transitions);
			log.fine("active concurrent executions: "
					+ concurrentActiveExecutions);
		}

		if ((transitions.size() == 1) && (concurrentActiveExecutions.isEmpty())) {

			List<ExecutionImpl> recyclableExecutionImpls = (List) recyclableExecutions;
			for (ExecutionImpl prunedExecution : recyclableExecutionImpls) {
				// End the pruned executions if necessary.
				// Some recyclable executions are inactivated (joined
				// executions)
				// Others are already ended (end activities)
				if (!prunedExecution.isEnded()) {
					log.fine("pruning execution " + prunedExecution);
					prunedExecution.remove();
				}
			}

			log.fine("activating the concurrent root " + concurrentRoot
					+ " as the single path of execution going forward");
			concurrentRoot.setActive(true);
			concurrentRoot.setActivity(activity);
			concurrentRoot.setConcurrent(false);
			concurrentRoot.take(transitions.get(0));

		} else {

			List<OutgoingExecution> outgoingExecutions = new ArrayList<OutgoingExecution>();

			recyclableExecutions.remove(concurrentRoot);

			log.fine("recyclable executions for reused: "
					+ recyclableExecutions);

			// first create the concurrent executions
			while (!transitions.isEmpty()) {
				PvmTransition outgoingTransition = transitions.remove(0);

				ExecutionImpl outgoingExecution = null;
				if (recyclableExecutions.isEmpty()) {
					outgoingExecution = concurrentRoot.createExecution();
					log.fine("new " + outgoingExecution
							+ " created to take transition "
							+ outgoingTransition);
				} else {
					outgoingExecution = (ExecutionImpl) recyclableExecutions
							.remove(0);
					log.fine("recycled " + outgoingExecution
							+ " to take transition " + outgoingTransition);
				}

				outgoingExecution.setActive(true);
				outgoingExecution.setScope(false);
				outgoingExecution.setConcurrent(true);
				outgoingExecutions.add(new OutgoingExecution(outgoingExecution,
						outgoingTransition, true));
			}

			// prune the executions that are not recycled
			for (ActivityExecution prunedExecution : recyclableExecutions) {
				log.fine("pruning execution " + prunedExecution);
				prunedExecution.end();
			}

			// then launch all the concurrent executions
			for (OutgoingExecution outgoingExecution : outgoingExecutions) {
				outgoingExecution.take();
			}
		}
	}

	public void performOperation(AtomicOperation executionOperation) {
		this.nextOperation = executionOperation;
		if (!isOperating) {
			isOperating = true;
			while (nextOperation != null) {
				AtomicOperation currentOperation = this.nextOperation;
				this.nextOperation = null;
				if (log.isLoggable(Level.FINEST)) {
					log.finest("AtomicOperation: " + currentOperation + " on "
							+ this);
				}
				currentOperation.execute(this);
			}
			isOperating = false;
		}
	}

	public boolean isActive(String activityId) {
		return findExecution(activityId) != null;
	}

	// variables
	// ////////////////////////////////////////////////////////////////

	public Object getVariable(String variableName) {
		ensureVariablesInitialized();

		// If value is found in this scope, return it
		if (variables.containsKey(variableName)) {
			return variables.get(variableName);
		}

		// If value not found in this scope, check the parent scope
		ensureParentInitialized();
		if (parent != null) {
			return parent.getVariable(variableName);
		}

		// Variable is nowhere to be found
		return null;
	}

	public Map<String, Object> getVariables() {
		Map<String, Object> collectedVariables = new HashMap<String, Object>();
		collectVariables(collectedVariables);
		return collectedVariables;
	}

	protected void collectVariables(Map<String, Object> collectedVariables) {
		ensureParentInitialized();
		if (parent != null) {
			parent.collectVariables(collectedVariables);
		}
		ensureVariablesInitialized();
		for (String variableName : variables.keySet()) {
			collectedVariables.put(variableName, variables.get(variableName));
		}
	}

	public void setVariables(Map<String, ? extends Object> variables) {
		ensureVariablesInitialized();
		if (variables != null) {
			for (String variableName : variables.keySet()) {
				setVariable(variableName, variables.get(variableName));
			}
		}
	}

	public void setVariable(String variableName, Object value) {
		ensureVariablesInitialized();
		if (variables.containsKey(variableName)) {
			setVariableLocally(variableName, value);
		} else {
			ensureParentInitialized();
			if (parent != null) {
				parent.setVariable(variableName, value);
			} else {
				setVariableLocally(variableName, value);
			}
		}
	}

	public void setVariableLocally(String variableName, Object value) {
		log.fine("setting variable '" + variableName + "' to value '" + value
				+ "' on " + this);
		variables.put(variableName, value);
	}

	public boolean hasVariable(String variableName) {
		ensureVariablesInitialized();
		if (variables.containsKey(variableName)) {
			return true;
		}
		ensureParentInitialized();
		if (parent != null) {
			return parent.hasVariable(variableName);
		}
		return false;
	}

	protected void ensureVariablesInitialized() {
		if (variables == null) {
			variables = new HashMap<String, Object>();
		}
	}

	// toString
	// /////////////////////////////////////////////////////////////////

	public String toString() {
		if (isProcessInstance()) {
			return "ProcessInstance[" + getToStringIdentity() + "]";
		} else {
			return (isEventScope ? "EventScope" : "")
					+ (isConcurrent ? "Concurrent" : "")
					+ (isScope() ? "Scope" : "") + "Execution["
					+ getToStringIdentity() + "]";
		}
	}

	protected String getToStringIdentity() {
		return Integer.toString(System.identityHashCode(this));
	}

	// customized getters and setters
	// ///////////////////////////////////////////

	public boolean isProcessInstance() {
		ensureParentInitialized();
		return parent == null;
	}

	public void inactivate() {
		this.isActive = false;
	}

	// allow for subclasses to expose a real id
	// /////////////////////////////////

	public String getId() {
		return null;
	}

	// getters and setters
	// //////////////////////////////////////////////////////

	public TransitionImpl getTransition() {
		return transition;
	}

	public void setTransition(TransitionImpl transition) {
		this.transition = transition;
	}

	public Integer getExecutionListenerIndex() {
		return executionListenerIndex;
	}

	public void setExecutionListenerIndex(Integer executionListenerIndex) {
		this.executionListenerIndex = executionListenerIndex;
	}

	public boolean isConcurrent() {
		return isConcurrent;
	}

	public void setConcurrent(boolean isConcurrent) {
		this.isConcurrent = isConcurrent;
	}

	public boolean isActive() {
		return isActive;
	}

	public void setActive(boolean isActive) {
		this.isActive = isActive;
	}

	public boolean isEnded() {
		return isEnded;
	}

	public void setProcessDefinition(ProcessDefinitionImpl processDefinition) {
		this.processDefinition = processDefinition;
	}

	public String getEventName() {
		return eventName;
	}

	public void setEventName(String eventName) {
		this.eventName = eventName;
	}

	public PvmProcessElement getEventSource() {
		return eventSource;
	}

	public void setEventSource(PvmProcessElement eventSource) {
		this.eventSource = eventSource;
	}

	public String getDeleteReason() {
		return deleteReason;
	}

	public void setDeleteReason(String deleteReason) {
		this.deleteReason = deleteReason;
	}

	public ExecutionImpl getReplacedBy() {
		return replacedBy;
	}

	public void setReplacedBy(InterpretableExecution replacedBy) {
		this.replacedBy = (ExecutionImpl) replacedBy;
	}

	public void setExecutions(List<ExecutionImpl> executions) {
		this.executions = executions;
	}

	public boolean isDeleteRoot() {
		return deleteRoot;
	}

	public void createVariableLocal(String variableName, Object value) {
	}

	public void createVariablesLocal(Map<String, ? extends Object> variables) {
	}

	public Object getVariableLocal(Object variableName) {
		return null;
	}

	public Set<String> getVariableNames() {
		return null;
	}

	public Set<String> getVariableNamesLocal() {
		return null;
	}

	public Map<String, Object> getVariablesLocal() {
		return null;
	}

	public boolean hasVariableLocal(String variableName) {
		return false;
	}

	public boolean hasVariables() {
		return false;
	}

	public boolean hasVariablesLocal() {
		return false;
	}

	public void removeVariable(String variableName) {
	}

	public void removeVariableLocal(String variableName) {
	}

	public void removeVariables() {
	}

	public void removeVariablesLocal() {
	}

	public Object setVariableLocal(String variableName, Object value) {
		return null;
	}

	public void setVariablesLocal(Map<String, ? extends Object> variables) {
	}

	public boolean isEventScope() {
		return isEventScope;
	}

	public void setEventScope(boolean isEventScope) {
		this.isEventScope = isEventScope;
	}

	public StartingExecution getStartingExecution() {
		return startingExecution;
	}

	public void disposeStartingExecution() {
		startingExecution = null;
	}
}
