class PushSelectCriteria implements OptimizerRule {

    @Override
    public PlanNode execute( QueryContext context,
                             PlanNode plan,
                             LinkedList<OptimizerRule> ruleStack ) {
        // Create set of nodes that no longer need to be considered
        Set<PlanNode> deadNodes = new HashSet<PlanNode>();

        // Loop while criteria nodes are still being moved ...
        boolean movedSomeNode = true;
        while (movedSomeNode) {

            // Reset flag to false for this iteration
            movedSomeNode = false;

            // Find all of the criteria (SELECT) nodes that can be pushed ...
            List<PlanNode> criteriaNodes = plan.findAllAtOrBelow(Type.SELECT);

            // Find all of the NULL, SOURCE, SET_OPERATION or JOIN nodes, ordered correctly; we'll use this
            // to look for the node on which each criteria can apply ...
            List<PlanNode> originatingNodes = plan.findAllAtOrBelow(ORIGINATING_TYPES);

            // Starting with the lowest one first ...
            Collections.reverse(criteriaNodes);
            for (PlanNode criteriaNode : criteriaNodes) {
                // Skip any node we've already tried and failed to move ...
                if (deadNodes.contains(criteriaNode)) continue;

                // Find the first originating node that has all of the required selectors for this criteria ...
                PlanNode originatingNode = findOriginatingNode(criteriaNode, originatingNodes);
                if (originatingNode == null || originatingNode == criteriaNode) {
                    deadNodes.add(criteriaNode);
                    continue;
                }

                // Try to push the criteria node down ...
                if (!pushTowardsOriginatingNode(criteriaNode, originatingNode)) {
                    // criteria node could not be moved ...
                    deadNodes.add(criteriaNode);
                    continue;
                }

                // The criteria node was indeed moved, but it may need to be adjusted ...
                boolean adjusted = false;
                switch (originatingNode.getType()) {
                    case SOURCE:

                        break;
                    case JOIN:
                        if (!criteriaNode.hasAncestorOfType(Type.ACCESS)) {
                            // Try to push down the join criteria (only when above ACCESS nodes) ...
                            adjusted = pushDownJoinCriteria(criteriaNode, originatingNode);
                        }
                        break;
                    default:
                        // Nothing to change ...
                }
                if (adjusted) {
                    // We changed something, so make sure we go through the loop again ...
                    movedSomeNode = true;
                } else {
                    // Nothing was changed from the push-down, so consider this criteria node as processed ...
                    deadNodes.add(criteriaNode);
                }
            }
        }
        return plan;
    }

}