/* Licensed 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.activiti.engine.impl.bpmn.behavior;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.activiti.engine.ActivitiException;
import org.activiti.engine.delegate.event.impl.ActivitiEventBuilder;
import org.activiti.engine.impl.Condition;
import org.activiti.engine.impl.bpmn.helper.SkipExpressionUtil;
import org.activiti.engine.impl.bpmn.parser.BpmnParse;
import org.activiti.engine.impl.context.Context;
import org.activiti.engine.impl.persistence.entity.ExecutionEntity;
import org.activiti.engine.impl.persistence.entity.JobEntity;
import org.activiti.engine.impl.persistence.entity.TimerJobEntity;
import org.activiti.engine.impl.pvm.PvmTransition;
import org.activiti.engine.impl.pvm.delegate.ActivityExecution;
import org.activiti.engine.impl.pvm.process.ActivityImpl;
import org.activiti.engine.impl.pvm.runtime.InterpretableExecution;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
import org.flowable.common.engine.impl.interceptor.EngineConfigurationConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Helper class for implementing BPMN 2.0 activities, offering convenience methods specific to BPMN 2.0.
 * <p>
 * This class can be used by inheritance or aggregation.
 *
 * @author Joram Barrez
 */
public class BpmnActivityBehavior implements Serializable {

    private static final long serialVersionUID = 1L;

    private static final Logger LOGGER = LoggerFactory.getLogger(BpmnActivityBehavior.class);

    /**
     * Performs the default outgoing BPMN 2.0 behavior, which is having parallel paths of executions for the outgoing sequence flow.
     * <p>
     * More precisely: every sequence flow that has a condition which evaluates to true (or which doesn't have a condition), is selected for continuation of the process instance. If multiple sequencer
     * flow are selected, multiple, parallel paths of executions are created.
     */
    public void performDefaultOutgoingBehavior(ActivityExecution activityExecution) {
        ActivityImpl activity = (ActivityImpl) activityExecution.getActivity();
        if (!(activity.getActivityBehavior() instanceof IntermediateCatchEventActivityBehavior)) {
            dispatchJobCanceledEvents(activityExecution);
        }
        performOutgoingBehavior(activityExecution, true, false, null);
    }

    /**
     * Performs the default outgoing BPMN 2.0 behavior (@see {@link #performDefaultOutgoingBehavior(ActivityExecution)}), but without checking the conditions on the outgoing sequence flow.
     * <p>
     * This means that every outgoing sequence flow is selected for continuing the process instance, regardless of having a condition or not. In case of multiple outgoing sequence flow, multiple
     * parallel paths of executions will be created.
     */
    public void performIgnoreConditionsOutgoingBehavior(ActivityExecution activityExecution) {
        performOutgoingBehavior(activityExecution, false, false, null);
    }

    /**
     * dispatch job canceled event for job associated with given execution entity
     *
     * @param activityExecution
     */
    protected void dispatchJobCanceledEvents(ActivityExecution activityExecution) {
        if (activityExecution instanceof ExecutionEntity) {
            List<JobEntity> jobs = ((ExecutionEntity) activityExecution).getJobs();
            for (JobEntity job : jobs) {
                if (Context.getProcessEngineConfiguration().getEventDispatcher().isEnabled()) {
                    Context.getProcessEngineConfiguration().getEventDispatcher().dispatchEvent(
                            ActivitiEventBuilder.createEntityEvent(FlowableEngineEventType.JOB_CANCELED, job),
                            EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
                }
            }

            List<TimerJobEntity> timerJobs = ((ExecutionEntity) activityExecution).getTimerJobs();
            for (TimerJobEntity job : timerJobs) {
                if (Context.getProcessEngineConfiguration().getEventDispatcher().isEnabled()) {
                    Context.getProcessEngineConfiguration().getEventDispatcher().dispatchEvent(
                            ActivitiEventBuilder.createEntityEvent(FlowableEngineEventType.JOB_CANCELED, job),
                            EngineConfigurationConstants.KEY_PROCESS_ENGINE_CONFIG);
                }
            }
        }
    }

    /**
     * Actual implementation of leaving an activity.
     *
     * @param execution                      The current execution context
     * @param checkConditions                Whether or not to check conditions before determining whether or not to take a transition.
     * @param throwExceptionIfExecutionStuck If true, an {@link ActivitiException} will be thrown in case no transition could be found to leave the activity.
     */
    protected void performOutgoingBehavior(ActivityExecution execution,
                                           boolean checkConditions, boolean throwExceptionIfExecutionStuck, List<ActivityExecution> reusableExecutions) {

        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Leaving activity '{}'", execution.getActivity().getId());
        }

        String defaultSequenceFlow = (String) execution.getActivity().getProperty("default");
        List<PvmTransition> transitionsToTake = new ArrayList<>();

        List<PvmTransition> outgoingTransitions = execution.getActivity().getOutgoingTransitions();
        for (PvmTransition outgoingTransition : outgoingTransitions) {
            Expression skipExpression = outgoingTransition.getSkipExpression();

            if (!SkipExpressionUtil.isSkipExpressionEnabled(execution, skipExpression)) {
                if (defaultSequenceFlow == null || !outgoingTransition.getId().equals(defaultSequenceFlow)) {
                    Condition condition = (Condition) outgoingTransition.getProperty(BpmnParse.PROPERTYNAME_CONDITION);
                    if (condition == null || !checkConditions || condition.evaluate(outgoingTransition.getId(), execution)) {
                        transitionsToTake.add(outgoingTransition);
                    }
                }

            } else if (SkipExpressionUtil.shouldSkipFlowElement(execution, skipExpression)) {
                transitionsToTake.add(outgoingTransition);
            }
        }

        if (transitionsToTake.size() == 1) {

            execution.take(transitionsToTake.get(0));

        } else if (transitionsToTake.size() >= 1) {

            execution.inactivate();
            if (reusableExecutions == null || reusableExecutions.isEmpty()) {
                execution.takeAll(transitionsToTake, Collections.singletonList(execution));
            } else {
                execution.takeAll(transitionsToTake, reusableExecutions);
            }

        } else {

            if (defaultSequenceFlow != null) {
                PvmTransition defaultTransition = execution.getActivity().findOutgoingTransition(defaultSequenceFlow);
                if (defaultTransition != null) {
                    execution.take(defaultTransition);
                } else {
                    throw new ActivitiException("Default sequence flow '" + defaultSequenceFlow + "' could not be not found");
                }
            } else {

                Object isForCompensation = execution.getActivity().getProperty(BpmnParse.PROPERTYNAME_IS_FOR_COMPENSATION);
                if (isForCompensation != null && (Boolean) isForCompensation) {
                    if (execution instanceof ExecutionEntity) {
                        Context.getCommandContext().getHistoryManager().recordActivityEnd((ExecutionEntity) execution);
                    }
                    InterpretableExecution parentExecution = (InterpretableExecution) execution.getParent();
                    ((InterpretableExecution) execution).remove();
                    parentExecution.signal("compensationDone", null);

                } else {

                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("No outgoing sequence flow found for {}. Ending execution.", execution.getActivity().getId());
                    }
                    execution.end();

                    if (throwExceptionIfExecutionStuck) {
                        throw new ActivitiException("No outgoing sequence flow of the inclusive gateway '" + execution.getActivity().getId()
                                + "' could be selected for continuing the process");
                    }
                }

            }
        }
    }

}
