package de.ugoe.cs.autoquest.tasktrees.temporalrelation;

import java.util.ArrayList;
import java.util.List;

import de.ugoe.cs.autoquest.eventcore.guimodel.IGUIElement;
import de.ugoe.cs.autoquest.tasktrees.treeifc.IEventTask;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ISequence;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskTreeBuilder;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskTreeNode;
import de.ugoe.cs.autoquest.tasktrees.treeifc.ITaskTreeNodeFactory;

/**
 * TODO comment
 * 
 * @version $Revision: $ $Date: 18.03.2012$
 * @author 2012, last modified by $Author: patrick$
 */
public class DefaultGuiElementSequenceDetectionRule implements TemporalRelationshipRule {

    /*
     * (non-Javadoc)
     * 
     * @see de.ugoe.cs.tasktree.temporalrelation.TemporalRelationshipRule#apply(TaskTreeNode,
     * TaskTreeBuilder, TaskTreeNodeFactory)
     */
    @Override
    public RuleApplicationResult apply(ITaskTreeNode        parent,
                                       ITaskTreeBuilder     builder,
                                       ITaskTreeNodeFactory nodeFactory,
                                       boolean              finalize)
    {
        if (!(parent instanceof ISequence)) {
            return null;
        }

        RuleApplicationResult result = new RuleApplicationResult();
        
        IGUIElement lastGuiElement = null;
        int index = 0;
        while (index < parent.getChildren().size()) {
            ITaskTreeNode child = parent.getChildren().get(index);
            IGUIElement currentGuiElement = getGUIElement(child);
            if ((index > 0) && (!lastGuiElement.equals(currentGuiElement))) {
                ReducableCommonDenominator commonDenominator =
                    getNextReducableCommonDenominator(parent, index - 1);
                    
                if (commonDenominator != null) {
                    // condense only if not all children would be condensed or if we can be sure,
                    // that there will be no further child that should be included in the condensed
                    // sequence
                    if ((commonDenominator.noOfTasks < parent.getChildren().size()) &&
                        (!isOnGuiElementPath(commonDenominator.commonGuiElement, currentGuiElement)))
                    {
                        condenseTasksToSequence(parent, index, commonDenominator.noOfTasks,
                                                builder, nodeFactory, result);

                        result.setRuleApplicationStatus
                            (RuleApplicationStatus.RULE_APPLICATION_FINISHED);
                        return result;
                    }
                    else {
                        // the common denominator is on the parent path of the next GUI element.
                        // Therefore, the current sequences is not finished yet. So break up.
                        result.setRuleApplicationStatus
                            (RuleApplicationStatus.RULE_APPLICATION_FEASIBLE);
                    }
                }
            }

            lastGuiElement = currentGuiElement;
            index++;
        }

        ReducableCommonDenominator commonDenominator =
            getNextReducableCommonDenominator(parent, parent.getChildren().size() - 1);
        
        if ((commonDenominator != null) &&
            (commonDenominator.noOfTasks < parent.getChildren().size()))
        {
            if (finalize) {
                condenseTasksToSequence
                    (parent, index, commonDenominator.noOfTasks, builder, nodeFactory, result);
                
                result.setRuleApplicationStatus(RuleApplicationStatus.RULE_APPLICATION_FINISHED);
                
                return result;
            }
            else {
                result.setRuleApplicationStatus(RuleApplicationStatus.RULE_APPLICATION_FEASIBLE);
            }
        }

        return result;
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param guiElement
     * @param detectedTasks
     * @param parent
     * @param index
     * @param builder
     * @param nodeFactory
     * @return
     */
    private void condenseTasksToSequence(ITaskTreeNode         parent,
                                         int                   parentIndex,
                                         int                   noOfTasks,
                                         ITaskTreeBuilder      builder,
                                         ITaskTreeNodeFactory  nodeFactory,
                                         RuleApplicationResult result)
    {
        ISequence newSequence = nodeFactory.createNewSequence();
        for (int i = 0; i < noOfTasks; i++) {
            builder.addChild(newSequence, parent.getChildren().get(parentIndex - noOfTasks));
            // remove exactly the same number of children from the parent.
            builder.removeChild((ISequence) parent, parentIndex - noOfTasks);
        }
                
        builder.addChild((ISequence) parent, parentIndex - noOfTasks, newSequence);
        result.addNewlyCreatedParentNode(newSequence);
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param detectedTasks
     * @return
     */
    private ReducableCommonDenominator getNextReducableCommonDenominator(ITaskTreeNode parent,
                                                                         int           childIndex)
    {
        ReducableCommonDenominator commonDenominator = null;
        
        // a common denominator can only exist for at least two task tree nodes
        if (childIndex > 0) {
            // start with the last one
            int pos = childIndex;

            commonDenominator = new ReducableCommonDenominator();
            
            // check for further predecessors, if they match the same common denominator
            IGUIElement currentCommonDenominator = null;
            do {
                if (--pos < 0) {
                    currentCommonDenominator = null;
                }
                else {
                    currentCommonDenominator = getCommonDenominator
                        (getGUIElement(parent.getChildren().get(pos)),
                         getGUIElement(parent.getChildren().get(pos + 1)));
                }
                
                if (commonDenominator.commonGuiElement == null) {
                    commonDenominator.commonGuiElement = currentCommonDenominator;
                }
            }
            while ((commonDenominator.commonGuiElement != null) &&
                   (commonDenominator.commonGuiElement.equals(currentCommonDenominator)));
            
            if (commonDenominator.commonGuiElement != null) {
                // pos points to the last element, that has not the same common denominator.
                // This one must be subtracted from the task number as well
                commonDenominator.noOfTasks = childIndex - pos;
            }
            else {
                commonDenominator = null;
            }
        }
        
        return commonDenominator;
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param child
     * @return
     */
    private IGUIElement getGUIElement(ITaskTreeNode node) {
        List<IGUIElement> terminalGUIElements = new ArrayList<IGUIElement>();
        getTerminalGUIElements(node, terminalGUIElements);
        return getCommonDenominator(terminalGUIElements);
    }
        
    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param detectedTaskGroups
     * @return
     */
    /*private IGUIElement getCommonDenominator(Stack<Task> detectedTasks, int start) {
        List<IGUIElement> allGUIElements = new ArrayList<IGUIElement>();
        
        for (int i = start; i < detectedTasks.size(); i++) {
            allGUIElements.add(detectedTasks.get(i).commonGuiElement);
        }
        
        return getCommonDenominator(allGUIElements);
    }*/

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param child
     * @return
     */
    private IGUIElement getCommonDenominator(IGUIElement guiElement1, IGUIElement guiElement2) {
        List<IGUIElement> allGUIElements = new ArrayList<IGUIElement>();
        allGUIElements.add(guiElement1);
        allGUIElements.add(guiElement2);
        return getCommonDenominator(allGUIElements);
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param child
     * @return
     */
    private IGUIElement getCommonDenominator(List<IGUIElement> guiElements) {
        IGUIElement commonDenominator = null;
        
        if (guiElements.size() > 0) {
            List<IGUIElement> commonDenominatorPath = new ArrayList<IGUIElement>();
            
            // create a reference list using the first GUI element
            IGUIElement guiElement = guiElements.get(0);
            while (guiElement != null) {
                commonDenominatorPath.add(0, guiElement);
                guiElement = guiElement.getParent();
            }
            
            // for each other GUI element, check the reference list for the first element in the
            // path, that is not common to the current one, and delete it as well as it subsequent
            // siblings
            List<IGUIElement> currentPath = new ArrayList<IGUIElement>();
            for (int i = 1; i < guiElements.size(); i++) {
                currentPath.clear();
                guiElement = guiElements.get(i);
                while (guiElement != null) {
                    currentPath.add(0, guiElement);
                    guiElement = guiElement.getParent();
                }
                
                // determine the index of the first unequal path element
                int index = 0;
                while ((index < commonDenominatorPath.size()) && (index < currentPath.size()) &&
                        commonDenominatorPath.get(index).equals(currentPath.get(index)))
                {
                    index++;
                }
                
                // remove all elements from the common denonimator path, that do not match
                while (index < commonDenominatorPath.size()) {
                    commonDenominatorPath.remove(index);
                }
            }
            
            if (commonDenominatorPath.size() > 0) {
                commonDenominator = commonDenominatorPath.get(commonDenominatorPath.size() - 1);
            }
        }
        
        return commonDenominator;
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param child
     * @return
     */
    private void getTerminalGUIElements(ITaskTreeNode node, List<IGUIElement> terminalGUIElements) {
        if (node instanceof IEventTask) {
            if (((IEventTask) node).getEventTarget() instanceof IGUIElement) {
                terminalGUIElements.add((IGUIElement) ((IEventTask) node).getEventTarget());
            }
        }
        else {
            for (ITaskTreeNode child : node.getChildren()) {
                getTerminalGUIElements(child, terminalGUIElements);
            }
        }
    }

    /**
     * <p>
     * TODO: comment
     * </p>
     *
     * @param currentCommonDenominator
     * @param guiElement
     * @return
     */
    private boolean isOnGuiElementPath(IGUIElement potentialPathElement, IGUIElement child) {
        IGUIElement guiElement = child;
        
        while (guiElement != null) {
            if (guiElement.equals(potentialPathElement)) {
                return true;
            }
            guiElement = guiElement.getParent();
        }
        
        return false;
    }

    /**
     * 
     */
    private static class ReducableCommonDenominator {
        
        /** the GUI element being the common denominator */
        private IGUIElement commonGuiElement;
        
        /** the number of tasks that match the common denominator */
        private int noOfTasks;

        /* (non-Javadoc)
         * @see java.lang.Object#toString()
         */
        @Override
        public String toString() {
            return noOfTasks + " tasks on " + commonGuiElement;
        }
        
    }
}
