/*
 * Copyright 2015 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.
 * 
 *      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.simulation.handler;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;

import org.eclipse.bpmn2.ExclusiveGateway;
import org.eclipse.bpmn2.FlowElement;
import org.eclipse.bpmn2.InclusiveGateway;
import org.eclipse.bpmn2.ParallelGateway;
import org.eclipse.bpmn2.SequenceFlow;
import org.jbpm.simulation.PathContext;
import org.jbpm.simulation.PathContext.Type;
import org.jbpm.simulation.PathContextManager;

public class GatewayElementHandler extends MainElementHandler {
    
    private PathContextManager manager;

    public boolean handle(FlowElement element, PathContextManager manager) {
        
        this.manager = manager;
        
        if (element instanceof ExclusiveGateway) {
            handleExclusiveGateway(getOutgoing(element));
            return true;
        } else if (element instanceof InclusiveGateway) {
            handleInclusiveGateway(getOutgoing(element));
            return true;
        } else if (element instanceof ParallelGateway) {
            handleParallelGateway(getOutgoing(element));
            return true;
        } else {
            throw new UnsupportedOperationException("Not supported element to handle " + element.eClass().getName());
        }

    }

    protected void handleExclusiveGateway(List<SequenceFlow> outgoing) {
        List<PathContext> locked = new ArrayList<PathContext>();
        Stack<PathContext> contextsAtThisNode = manager.getContextsFromStack();

        for (PathContext contextAtThisNode : contextsAtThisNode) {

            for (SequenceFlow seqFlow : outgoing) {

                FlowElement target = seqFlow.getTargetRef();
                if (!contextAtThisNode.getVisitedSplitPoint().contains(seqFlow)) {
                    PathContext separatePath = manager.cloneGiven(contextAtThisNode);
                    separatePath.addVisitedSplitPoint(seqFlow);
                    manager.addToPath(seqFlow, separatePath);
                    super.handle(target, manager);
                    separatePath.setLocked(true);
                    locked.add(separatePath);
                }
            }
        }
        // unlock
        for (PathContext ctx : locked) {
            ctx.setLocked(false);
        }
    }

    protected void handleInclusiveGateway(List<SequenceFlow> outgoing) {
        // firstly cover simple xor based - number of paths is equal to number
        // of outgoing
        handleExclusiveGateway(outgoing);
        Type currentType = manager.getContextFromStack().getType();
        manager.getContextFromStack().setType(Type.ROOT);

        // next cover all combinations of paths
        if (outgoing.size() > 2) {
            List<SequenceFlow> copy = new ArrayList<SequenceFlow>(outgoing);
            List<SequenceFlow> andCombination = null;
            for (SequenceFlow flow : outgoing) {

                // first remove one that we currently processing as that is not
                // a combination
                copy.remove(flow);
                PathContext contextAtThisNode = manager.cloneGivenWithoutPush(manager.getContextFromStack());
                for (SequenceFlow copyFlow : copy) {
                    manager.cloneGiven(contextAtThisNode);


                    andCombination = new ArrayList<SequenceFlow>();
                    andCombination.add(flow);
                    andCombination.add(copyFlow);

                    handleParallelGateway(andCombination);
                }
            }
        }
        manager.getContextFromStack().setType(Type.ROOT);
        // lastly cover and based - is single path that goes through all at the
        // same time
        handleParallelGateway(outgoing);
        manager.getContextFromStack().setType(currentType);

    }

    protected void handleParallelGateway(List<SequenceFlow> outgoing) {
        PathContext context = manager.getContextFromStack();
        boolean canBeFinished = context.isCanBeFinished();
        context.setCanBeFinished(false);
        manager.addAllToPath(outgoing, context);
        int counter = 0;
        for (SequenceFlow seqFlow : outgoing) {
            counter++;
            FlowElement target = seqFlow.getTargetRef();

            if (counter == outgoing.size()) {
                if (manager.getPaths().size() == 1) {
                    context.setCanBeFinished(canBeFinished);

                } else {
                    Iterator<PathContext> it = manager.getPaths().iterator();

                    while (it.hasNext()) {
                        PathContext pathContext = (PathContext) it.next();
                        if (pathContext.getType() == Type.ACTIVE) {
                            pathContext.setCanBeFinished(canBeFinished);
                        }
                    }
                }
            }
            
            super.handle(target, manager);
        }
        // finalize paths if there are any to cover scenario when there was not converging parallel gateway
        if (canBeFinished) {

            for (SequenceFlow seqFlow : outgoing) {
                manager.addToPath(seqFlow, context);
                manager.addToPath(seqFlow.getTargetRef(), context);
            }
            manager.finalizePathOnLeave();
            
        }
    }
}
