/*
 * JBoss, Home of Professional Open Source.
 * See the COPYRIGHT.txt file distributed with this work for information
 * regarding copyright ownership.  Some portions may be licensed
 * to Red Hat, Inc. under one or more contributor license agreements.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301 USA.
 */

package org.teiid.query.optimizer.relational.rules;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.teiid.api.exception.query.QueryMetadataException;
import org.teiid.api.exception.query.QueryPlannerException;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.util.Assertion;
import org.teiid.query.analysis.AnalysisRecord;
import org.teiid.query.metadata.QueryMetadataInterface;
import org.teiid.query.optimizer.capabilities.CapabilitiesFinder;
import org.teiid.query.optimizer.relational.OptimizerRule;
import org.teiid.query.optimizer.relational.RuleStack;
import org.teiid.query.optimizer.relational.plantree.NodeConstants;
import org.teiid.query.optimizer.relational.plantree.NodeConstants.Info;
import org.teiid.query.optimizer.relational.plantree.NodeEditor;
import org.teiid.query.optimizer.relational.plantree.NodeFactory;
import org.teiid.query.optimizer.relational.plantree.PlanNode;
import org.teiid.query.sql.lang.JoinType;
import org.teiid.query.sql.lang.OrderBy;
import org.teiid.query.sql.lang.OrderByItem;
import org.teiid.query.sql.lang.SetQuery;
import org.teiid.query.sql.symbol.AliasSymbol;
import org.teiid.query.sql.symbol.ElementSymbol;
import org.teiid.query.sql.symbol.Expression;
import org.teiid.query.sql.symbol.GroupSymbol;
import org.teiid.query.sql.symbol.Symbol;
import org.teiid.query.sql.util.SymbolMap;
import org.teiid.query.util.CommandContext;


/**
 * Will attempt to raise null nodes to their highest points 
 */
public final class RuleRaiseNull implements OptimizerRule {

	public PlanNode execute(PlanNode plan, QueryMetadataInterface metadata, CapabilitiesFinder capFinder, RuleStack rules, AnalysisRecord analysisRecord, CommandContext context)
		throws QueryPlannerException, QueryMetadataException, TeiidComponentException {

        List<PlanNode> nodes = NodeEditor.findAllNodes(plan, NodeConstants.Types.NULL);
        
        //create a new list to iterate over since the original will be modified
        for (PlanNode nullNode : new LinkedList<PlanNode>(nodes)) {
            while (nullNode.getParent() != null && nodes.contains(nullNode)) {
                // Attempt to raise the node
                PlanNode newRoot = raiseNullNode(plan, nodes, nullNode, metadata, capFinder);
                if(newRoot != null) {
                    plan = newRoot;
                } else {
                    break;
                }
            } 
            
            if (nullNode.getParent() == null) {
                nodes.remove(nullNode);
            }                            
        }
                
        return plan;
	}
    
    /**
     * @param nullNode
     * @param metadata
     * @param capFinder
     * @return null if the raising should not continue, else the newRoot
     */
    PlanNode raiseNullNode(PlanNode rootNode, List<PlanNode> nodes, PlanNode nullNode, QueryMetadataInterface metadata, CapabilitiesFinder capFinder) 
    throws QueryPlannerException, QueryMetadataException, TeiidComponentException {
        
        PlanNode parentNode = nullNode.getParent();
        
        switch(parentNode.getType()) {
            case NodeConstants.Types.JOIN:
            {
                JoinType jt = (JoinType)parentNode.getProperty(NodeConstants.Info.JOIN_TYPE);
                if (jt == JoinType.JOIN_CROSS || jt == JoinType.JOIN_INNER) {
                    return raiseNullNode(rootNode, parentNode, nullNode, nodes);
                }
                //for outer joins if the null node is on the outer side, then the join itself is null
                //if the null node is on the inner side, then the join can be removed but the null values 
                //coming from the inner side will need to be placed into the frame
                if (jt == JoinType.JOIN_LEFT_OUTER) {
                    if (nullNode == parentNode.getFirstChild()) {
                        return raiseNullNode(rootNode, parentNode, nullNode, nodes);
                    } 
                    raiseNullThroughJoin(metadata, parentNode, parentNode.getLastChild());
                    return null;
                } 
                if (jt == JoinType.JOIN_RIGHT_OUTER) {
                    if (nullNode == parentNode.getLastChild()) {
                        return raiseNullNode(rootNode, parentNode, nullNode, nodes);
                    } 
                    raiseNullThroughJoin(metadata, parentNode, parentNode.getFirstChild());
                    return null;
                }   
                if (jt == JoinType.JOIN_FULL_OUTER) {
                    if (nullNode == parentNode.getLastChild()) {
                        raiseNullThroughJoin(metadata, parentNode, parentNode.getLastChild());
                    } else {
                        raiseNullThroughJoin(metadata, parentNode, parentNode.getFirstChild());
                    }
                    return null;
                }
                break;
            }            
            case NodeConstants.Types.SET_OP:
            {
                boolean isLeftChild = parentNode.getFirstChild() == nullNode;
                SetQuery.Operation operation = (SetQuery.Operation)parentNode.getProperty(NodeConstants.Info.SET_OPERATION);
                boolean raiseOverSetOp = (operation == SetQuery.Operation.INTERSECT || (operation == SetQuery.Operation.EXCEPT && isLeftChild));
                
                if (raiseOverSetOp) {
                    return raiseNullNode(rootNode, parentNode, nullNode, nodes);
                }
                
                boolean isAll = parentNode.hasBooleanProperty(NodeConstants.Info.USE_ALL);
                
                if (isLeftChild) {
                    PlanNode firstProject = NodeEditor.findNodePreOrder(parentNode, NodeConstants.Types.PROJECT);
                    
                    if (firstProject == null) { // will only happen if the other branch has only null nodes
                        return raiseNullNode(rootNode, parentNode, nullNode, nodes);
                    }

                    List<Expression> newProjectSymbols = (List<Expression>)firstProject.getProperty(NodeConstants.Info.PROJECT_COLS);
                    List<Expression> oldProjectSymbols = (List<Expression>)nullNode.getProperty(NodeConstants.Info.PROJECT_COLS);
                    
                    for (int i = 0; i < newProjectSymbols.size(); i++) {
                        Expression newSes = newProjectSymbols.get(i);
                        Expression oldSes = oldProjectSymbols.get(i);
                        if (!(newSes instanceof Symbol) || !Symbol.getShortName(newSes).equals(Symbol.getShortName(oldSes))) {
                            if (newSes instanceof AliasSymbol) {
                                newSes = ((AliasSymbol)newSes).getSymbol();
                            }
                            newProjectSymbols.set(i, new AliasSymbol(Symbol.getShortName(oldSes), newSes));
                        }
                    }
                    
                    PlanNode sort = NodeEditor.findParent(parentNode, NodeConstants.Types.SORT, NodeConstants.Types.SOURCE);
                    
                    if (sort != null) { //correct the sort to the new columns as well
                        OrderBy sortOrder = (OrderBy)sort.getProperty(NodeConstants.Info.SORT_ORDER);
                        for (OrderByItem item : sortOrder.getOrderByItems()) {
                            Expression sortElement = item.getSymbol();
                            sortElement = newProjectSymbols.get(oldProjectSymbols.indexOf(sortElement));
                            item.setSymbol(sortElement);
                        }
                    }
                    
                    PlanNode sourceNode = NodeEditor.findParent(parentNode, NodeConstants.Types.SOURCE);
                    if (sourceNode != null && NodeEditor.findNodePreOrder(sourceNode, NodeConstants.Types.PROJECT) == firstProject) {
                        SymbolMap symbolMap = (SymbolMap)sourceNode.getProperty(NodeConstants.Info.SYMBOL_MAP);
                        symbolMap = SymbolMap.createSymbolMap(symbolMap.getKeys(), newProjectSymbols);
                        sourceNode.setProperty(NodeConstants.Info.SYMBOL_MAP, symbolMap);
                    }                                        
                }
                
                NodeEditor.removeChildNode(parentNode, nullNode);

                PlanNode grandParent = parentNode.getParent();
                
                if (!isAll) { //ensure that the new child is distinct
                	PlanNode nestedSetOp = NodeEditor.findNodePreOrder(parentNode.getFirstChild(), NodeConstants.Types.SET_OP, NodeConstants.Types.SOURCE);
                	if (nestedSetOp != null) {
                		nestedSetOp.setProperty(NodeConstants.Info.USE_ALL, false);
                	} else if (NodeEditor.findNodePreOrder(parentNode.getFirstChild(), NodeConstants.Types.DUP_REMOVE, NodeConstants.Types.SOURCE) == null) {
                		parentNode.getFirstChild().addAsParent(NodeFactory.getNewNode(NodeConstants.Types.DUP_REMOVE));
                	}
                }
                
                if (grandParent == null) {
                    PlanNode newRoot = parentNode.getFirstChild();
                    parentNode.removeChild(newRoot);
                    return newRoot;
                }

                //remove the set op
                NodeEditor.removeChildNode(grandParent, parentNode);
                
                PlanNode sourceNode = NodeEditor.findParent(grandParent.getFirstChild(), NodeConstants.Types.SOURCE, NodeConstants.Types.SET_OP);
                                
                if (sourceNode != null) {
                    return RuleMergeVirtual.doMerge(sourceNode, rootNode, false, metadata, capFinder);
                }
                return null;
            }
            case NodeConstants.Types.GROUP:
            {
                //if there are grouping columns, then we can raise
                if (parentNode.hasCollectionProperty(NodeConstants.Info.GROUP_COLS)) {
                    return raiseNullNode(rootNode, parentNode, nullNode, nodes);
                }
                break; //- the else case could be implemented, but it's a lot of work for little gain, since the null node can't raise higher
            }
			case NodeConstants.Types.PROJECT: 
			{
				// check for project into
				PlanNode upperProject = NodeEditor.findParent(parentNode.getParent(), NodeConstants.Types.PROJECT, NodeConstants.Types.SOURCE);	
				
				if (upperProject == null
						|| upperProject.getProperty(NodeConstants.Info.INTO_GROUP) == null) {
	                return raiseNullNode(rootNode, parentNode, nullNode, nodes);
				}
				break;
			}
			case NodeConstants.Types.SOURCE:
			{
				PlanNode upperProject = parentNode.getParent();
				if (upperProject != null && upperProject.getType() == NodeConstants.Types.PROJECT && upperProject.hasProperty(Info.INTO_GROUP)) {
					break; //an insert plan
				}
                return raiseNullNode(rootNode, parentNode, nullNode, nodes);
			}
            default:
            {
                return raiseNullNode(rootNode, parentNode, nullNode, nodes);
            }                      
        }  
        return null;
    }

    private PlanNode raiseNullNode(PlanNode rootNode, PlanNode parentNode, PlanNode nullNode, List<PlanNode> nodes) {
        if (parentNode.getType() == NodeConstants.Types.SOURCE) {
            nullNode.getGroups().clear();
        } else if (parentNode.getType() == NodeConstants.Types.PROJECT) {
            nullNode.setProperty(NodeConstants.Info.PROJECT_COLS, parentNode.getProperty(NodeConstants.Info.PROJECT_COLS));
        }
        nullNode.addGroups(parentNode.getGroups());
        parentNode.removeChild(nullNode);
        nodes.removeAll(NodeEditor.findAllNodes(parentNode, NodeConstants.Types.NULL));
        if (parentNode.getParent() != null) {
            parentNode.getParent().replaceChild(parentNode, nullNode);
        } else {
            rootNode = nullNode;
        }
        return rootNode;
    }

    /** 
     * Given a joinNode that should be an outer join and a null node as one of its children, replace elements in 
     * the current frame from the null node groups with null values 
     * 
     * @param metadata
     * @param joinNode
     * @param nullNode
     * @throws QueryPlannerException
     * @throws QueryMetadataException
     * @throws TeiidComponentException
     */
    static void raiseNullThroughJoin(QueryMetadataInterface metadata,
                                      PlanNode joinNode,
                                      PlanNode nullNode) throws QueryPlannerException,
                                                         QueryMetadataException,
                                                         TeiidComponentException {
        Assertion.assertTrue(joinNode.getType() == NodeConstants.Types.JOIN);
        Assertion.assertTrue(nullNode.getType() == NodeConstants.Types.NULL);
        Assertion.assertTrue(nullNode.getParent() == joinNode);
        
        PlanNode frameStart = joinNode.getParent();
        
        NodeEditor.removeChildNode(joinNode, nullNode);
        NodeEditor.removeChildNode(joinNode.getParent(), joinNode);
        
        for (GroupSymbol group : nullNode.getGroups()) {
            Map<ElementSymbol, Expression> nullSymbolMap = FrameUtil.buildSymbolMap(group, null, metadata);
            FrameUtil.convertFrame(frameStart, group, null, nullSymbolMap, metadata);
        }
        
    }    
    
    public String toString() {
		return "RaiseNull"; //$NON-NLS-1$
	}
	
}