/*
 * Copyright 2013 Red Hat, Inc. and/or its affiliates.
 *
 * 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.jbpm.workflow.instance.impl;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

import org.jbpm.process.core.ContextContainer;
import org.jbpm.process.core.context.exception.CompensationScope;
import org.jbpm.process.instance.ContextInstanceContainer;
import org.jbpm.process.instance.ProcessInstance;
import org.jbpm.process.instance.context.exception.CompensationScopeInstance;
import org.jbpm.ruleflow.core.RuleFlowProcess;
import org.jbpm.workflow.core.Node;
import org.jbpm.workflow.core.impl.NodeImpl;
import org.jbpm.workflow.instance.NodeInstance;
import org.jbpm.workflow.instance.NodeInstanceContainer;
import org.jbpm.workflow.instance.WorkflowRuntimeException;
import org.jbpm.workflow.instance.node.CompositeNodeInstance;
import org.kie.api.definition.process.NodeContainer;
import org.kie.kogito.internal.process.event.KogitoEventListener;

import static org.jbpm.process.core.context.exception.CompensationScope.COMPENSATION_SCOPE;
import static org.jbpm.process.core.context.exception.CompensationScope.IMPLICIT_COMPENSATION_PREFIX;

class CompensationEventListener implements KogitoEventListener {

    private WorkflowProcessInstanceImpl instance;

    public CompensationEventListener(WorkflowProcessInstanceImpl instance) {
        this.instance = instance;
    }

    private ProcessInstance getProcessInstance() {
        return instance;
    }

    /**
     * When signaling compensation, you can do that in 1 of 2 ways:
     * 1. signalEvent("Compensation", <node-with-compensation-handler-id>)
     * This is specific compensation, that only possibly triggers the compensation handler
     * attached to the node referred to by the <node-with-compensation-handler-id>.
     * 2. signalEvent("Compensation", "implicit:" + <node-container-containing-compensation-scope-id> )
     * This is implicit or general compensation, in which you trigger all visible compensation handlers
     * (in the proper order, etc.) in the (sub-)process referred to by
     * the <node-container-containing-compensation-scope-id>.
     */
    public void signalEvent(String compensationType, Object activityRefStr) {
        if (!(activityRefStr instanceof String)) {
            throw new WorkflowRuntimeException(null, getProcessInstance(),
                    "Compensation can only be triggered with String events, not an event of type "
                            + (activityRefStr == null ? "null" : activityRefStr.getClass().getSimpleName()));
        }

        // 1. parse the activity ref (is it general or specific compensation?)
        String activityRef = (String) activityRefStr;
        String toCompensateNodeId = activityRef;
        boolean generalCompensation = false;
        if (activityRef.startsWith(IMPLICIT_COMPENSATION_PREFIX)) {
            toCompensateNodeId = activityRef.substring(IMPLICIT_COMPENSATION_PREFIX.length());
            generalCompensation = true;
        }

        org.jbpm.process.core.Process process = (org.jbpm.process.core.Process) instance.getProcess();

        // 2. for specific compensation: find the node that will be compensated
        //    for general compensation: find the compensation scope container that contains all the visible compensation handlers
        org.kie.api.definition.process.Node toCompensateNode = null;
        ContextContainer compensationScopeContainer = null;
        if (generalCompensation) {
            if (toCompensateNodeId.equals(instance.getProcessId())) {
                compensationScopeContainer = process;
            } else {
                compensationScopeContainer = (ContextContainer) findNode(toCompensateNodeId);
            }
        } else {
            toCompensateNode = findNode(toCompensateNodeId);
        }

        // 3. If the node exists, 
        //   a. find the node container for which the compensation handler is visible
        //   b. create the compensation scope instance
        //   c. handle the exception (which also cleans up the generated node instances)
        if (toCompensateNode != null || compensationScopeContainer != null) {
            CompensationScope compensationScope = null;
            if (compensationScopeContainer != null) {
                compensationScope = (CompensationScope) compensationScopeContainer.getDefaultContext(COMPENSATION_SCOPE);
            } else {
                compensationScope = (CompensationScope) ((NodeImpl) toCompensateNode).resolveContext(COMPENSATION_SCOPE, toCompensateNodeId);
            }
            assert compensationScope != null : "Compensation scope for node [" + toCompensateNodeId + "] could not be found!";

            CompensationScopeInstance scopeInstance;
            if (compensationScope.getContextContainerId().equals(process.getId())) {
                // process level compensation
                scopeInstance = (CompensationScopeInstance) instance.getContextInstance(compensationScope);
            } else {
                // nested compensation
                Stack<NodeInstance> generatedInstances;
                if (toCompensateNode == null) {
                    // logic is the same if it's specific or general
                    generatedInstances = createNodeInstanceContainers((org.kie.api.definition.process.Node) compensationScopeContainer, true);
                } else {
                    generatedInstances = createNodeInstanceContainers(toCompensateNode, false);
                }
                NodeInstance nodeInstanceContainer = generatedInstances.peek();
                scopeInstance = ((CompensationScopeInstance) ((ContextInstanceContainer) nodeInstanceContainer).getContextInstance(compensationScope));
                scopeInstance.addCompensationInstances(generatedInstances);
            }

            scopeInstance.handleException(activityRef, null);
        }
    }

    private org.kie.api.definition.process.Node findNode(String nodeId) {
        org.kie.api.definition.process.Node found = null;
        Queue<org.kie.api.definition.process.Node> allProcessNodes = new LinkedList<>();
        allProcessNodes.addAll(Arrays.asList(instance.getNodeContainer().getNodes()));
        while (!allProcessNodes.isEmpty()) {
            org.kie.api.definition.process.Node node = allProcessNodes.poll();
            if (nodeId.equals(node.getMetaData().get("UniqueId"))) {
                found = node;
                break;
            }
            if (node instanceof NodeContainer) {
                allProcessNodes.addAll(Arrays.asList(((NodeContainer) node).getNodes()));
            }
        }
        return found;
    }

    private Stack<NodeInstance> createNodeInstanceContainers(org.kie.api.definition.process.Node toCompensateNode, boolean generalCompensation) {
        Stack<NodeContainer> nestedNodes = new Stack<NodeContainer>();
        Stack<NodeInstance> generatedInstances = new Stack<NodeInstance>();

        NodeContainer parentContainer = ((Node) toCompensateNode).getParentContainer();
        while (!(parentContainer instanceof RuleFlowProcess)) {
            nestedNodes.add(parentContainer);
            parentContainer = ((Node) parentContainer).getParentContainer();
        }

        NodeInstanceContainer parentInstance;

        if (nestedNodes.isEmpty()) {
            // nestedNodes is empty
            parentInstance = (NodeInstanceContainer) getProcessInstance();
        } else {
            parentInstance = (NodeInstanceContainer) ((WorkflowProcessInstanceImpl) getProcessInstance()).getNodeInstance((org.kie.api.definition.process.Node) nestedNodes.pop());
            generatedInstances.add((NodeInstance) parentInstance);
        }

        NodeInstanceContainer childInstance;
        while (!nestedNodes.isEmpty()) {
            // generate
            childInstance = (NodeInstanceContainer) parentInstance.getNodeInstance((org.kie.api.definition.process.Node) nestedNodes.pop());
            assert childInstance instanceof CompositeNodeInstance
                    : "A node with child nodes should end up creating a CompositeNodeInstance type.";

            // track and modify
            generatedInstances.add((NodeInstance) childInstance);

            // loop
            parentInstance = childInstance;
        }
        if (generalCompensation) {
            childInstance = (NodeInstanceContainer) parentInstance.getNodeInstance(toCompensateNode);
            generatedInstances.add((NodeInstance) childInstance);
        }

        return generatedInstances;
    }

    private final String[] eventTypes = { "Compensation" };

    public String[] getEventTypes() {
        return eventTypes;
    }

}