/*
 * 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.xml;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;

import org.teiid.api.exception.query.QueryMetadataException;
import org.teiid.api.exception.query.QueryParserException;
import org.teiid.api.exception.query.QueryPlannerException;
import org.teiid.api.exception.query.QueryResolverException;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.query.mapping.relational.QueryNode;
import org.teiid.query.mapping.xml.MappingBaseNode;
import org.teiid.query.mapping.xml.MappingDocument;
import org.teiid.query.mapping.xml.MappingNode;
import org.teiid.query.mapping.xml.MappingSourceNode;
import org.teiid.query.mapping.xml.MappingVisitor;
import org.teiid.query.mapping.xml.Navigator;
import org.teiid.query.mapping.xml.ResultSetInfo;
import org.teiid.query.metadata.TempMetadataStore;
import org.teiid.query.optimizer.relational.RelationalPlanner;
import org.teiid.query.parser.QueryParser;
import org.teiid.query.resolver.QueryResolver;
import org.teiid.query.resolver.util.ResolverUtil;
import org.teiid.query.resolver.util.ResolverVisitor;
import org.teiid.query.sql.lang.*;
import org.teiid.query.sql.symbol.Constant;
import org.teiid.query.sql.symbol.ElementSymbol;
import org.teiid.query.sql.symbol.Expression;
import org.teiid.query.sql.symbol.ExpressionSymbol;
import org.teiid.query.sql.symbol.GroupSymbol;
import org.teiid.query.sql.symbol.Reference;
import org.teiid.query.sql.visitor.ExpressionMappingVisitor;
import org.teiid.query.sql.visitor.GroupCollectorVisitor;


/** 
 * This class has code to with planning the automatic XML staging queries.
 */
public class XMLStagaingQueryPlanner {
    
    static void stageQueries(MappingDocument doc, final XMLPlannerEnvironment planEnv) 
        throws QueryPlannerException, QueryMetadataException, TeiidComponentException {

        MappingVisitor queryPlanVisitor = new MappingVisitor() {
            public void visit(MappingSourceNode sourceNode) {
                try {
                    stagePlannedQuery(sourceNode, planEnv);
                } catch (Exception e) {
                     throw new TeiidRuntimeException(e);
                }
            }
        };
        planWalk(doc, queryPlanVisitor);
    }
    
    private static void planWalk(MappingDocument doc, MappingVisitor visitor) 
        throws QueryPlannerException, QueryMetadataException, TeiidComponentException {
    
        try {
            Navigator walker = new Navigator(true, visitor);
            doc.acceptVisitor(walker);
        } catch (TeiidRuntimeException e) {
            if (e.getCause() instanceof QueryPlannerException) {
                throw (QueryPlannerException)e.getCause();
            }           
            else if (e.getCause() instanceof QueryMetadataException) {
                throw (QueryMetadataException)e.getCause();
            }
            else if (e.getCause() instanceof TeiidComponentException) {
                throw (TeiidComponentException)e.getCause();
            }
            else {
                throw e;
            }
        }
    }    
    
    static boolean stagePlannedQuery(MappingSourceNode sourceNode, XMLPlannerEnvironment planEnv) 
        throws QueryPlannerException, QueryMetadataException, TeiidComponentException, QueryResolverException, QueryParserException {
    
        Option option = planEnv.xmlCommand.getOption();
        
        // make sure we do not plan staging table for root mapping class.
        if (sourceNode.isRootSourceNode()) {
            return false;
        }
        
        //TODO: if this source is source for a recursive node, we do not want to stage
        if (sourceNode.getAliasResultName() != null) {
            return false;
        }
        
        String groupName = sourceNode.getActualResultSetName();
        ResultSetInfo rsInfo = sourceNode.getResultSetInfo();

        // If this node has been eligible for raising, it will be eligible for staging.
        if (rsInfo.hasInputSet() && !rsInfo.isCriteriaRaised()) {
            return false;
        }
        
        // no bindings - no references
        QueryNode queryNode = QueryUtil.getQueryNode(groupName, planEnv.getGlobalMetadata());
        if (queryNode.getBindings() != null && !queryNode.getBindings().isEmpty()) {
            return false;
        }
        
        // get the original transformation of the mapping class before planning. 
        Query stagableQuery = (Query)QueryUtil.getQueryFromQueryNode(groupName, planEnv);
        
        Collection<GroupSymbol> groups = GroupCollectorVisitor.getGroupsIgnoreInlineViews(stagableQuery, false);
        
        //check for already staged queries
        if (groups.size() == 1) {
            GroupSymbol group = groups.iterator().next();
            group = QueryUtil.createResolvedGroup(group.clone(), planEnv.getGlobalMetadata());
            if (planEnv.isStagingTable(group.getMetadataID()) && stagableQuery.getCriteria() == null) {
                return false;
            }
        }
        
        Criteria crit = ((Query)rsInfo.getCommand()).getCriteria();

        GroupSymbol parent = null;
        LinkedHashSet<ElementSymbol> outerReferences = new LinkedHashSet<ElementSymbol>();
        LinkedHashSet<ElementSymbol> fkColumns = new LinkedHashSet<ElementSymbol>();
        //see if we can perform a dependent join
        for (Criteria conjunct : Criteria.separateCriteriaByAnd(crit)) {
        	if (!(conjunct instanceof CompareCriteria)) {
        		continue;
        	}
        	CompareCriteria cc = (CompareCriteria)conjunct;
        	if (cc.getOperator() != CompareCriteria.EQ) {
        		continue;
        	}
        	if (!(cc.getLeftExpression() instanceof ElementSymbol) 
        			|| !(cc.getRightExpression() instanceof ElementSymbol)) {
        		continue;
        	}
    		ElementSymbol les = (ElementSymbol)cc.getLeftExpression();
    		ElementSymbol res = (ElementSymbol)cc.getRightExpression();
    		if (les.getGroupSymbol().getNonCorrelationName().equalsIgnoreCase(groupName)) {
				parent = res.getGroupSymbol();
				outerReferences.add(res.clone());
				fkColumns.add(les.clone());
    		} else if (res.getGroupSymbol().getNonCorrelationName().equalsIgnoreCase(groupName)) {
				parent = les.getGroupSymbol();
				outerReferences.add(les.clone());
				fkColumns.add(res.clone());
    		}
        }
        String stagingGroupName = planEnv.getStagedResultName(groupName);  

        boolean recursive = false;
        MappingSourceNode msn = sourceNode;
        while (!recursive && msn != null) {
        	MappingNode mappingNode = msn.getChildren().get(0);
			if (mappingNode instanceof MappingBaseNode) {
            	recursive = ((MappingBaseNode)mappingNode).isRootRecursiveNode();
        	}
        	msn = msn.getParentSourceNode();
        }
        
        if (parent != null && !recursive) {
            stagableQuery = (Query)stagableQuery.clone();
            String parentName = parent.getNonCorrelationName();
            
            String parentStagingName = planEnv.getStagedResultName(parentName);
            GroupSymbol parentTempTable = new GroupSymbol(XMLQueryPlanner.getTempTableName(parentStagingName));
        	ResultSetInfo parentRsInfo = planEnv.getStagingTableResultsInfo(parentStagingName);
        	String stagingRoot = sourceNode.getParentSourceNode().getSource();
        	
        	boolean parentStaged = parentRsInfo.getPlan() != null;
        	//TODO: check to see if the parent was manually staged
        	
        	if (!parentStaged) {
                //TODO: if not a level 1 child we could use the old auto staging logic instead
        		
        		//switch the parent over to the source
        		parentRsInfo = sourceNode.getParentSourceNode().getResultSetInfo();
        		
        		if (parentRsInfo.getTempTable() == null) {
	        		//create a temp table to represent the resultset
	                List<Expression> projectedSymbols = parentRsInfo.getCommand().getProjectedSymbols();
	                ArrayList<Expression> elements = new ArrayList<Expression>(projectedSymbols.size());
	        		for (Expression singleElementSymbol : projectedSymbols) {
	        			singleElementSymbol = (Expression) singleElementSymbol.clone();
						ResolverVisitor.resolveLanguageObject(singleElementSymbol, planEnv.getGlobalMetadata());
						elements.add(singleElementSymbol);
					}
	                TempMetadataStore store = planEnv.getGlobalMetadata().getMetadataStore();
	                // create a new group name and to the temp store
	                GroupSymbol newGroup = new GroupSymbol(SourceNodePlannerVisitor.getNewName("#" + planEnv.getAliasName(parentName) + "_RS", store)); //$NON-NLS-1$ //$NON-NLS-2$
	                newGroup.setMetadataID(store.addTempGroup(newGroup.getName(), elements, false, true));
	
	                parentStagingName = newGroup.getName();
	                parentTempTable = newGroup;
        		} else {
        			parentStagingName = parentRsInfo.getTempTable();
        			parentTempTable = new GroupSymbol(parentRsInfo.getTempTable());
	                parentStaged = true;
        		}
        	} else {
        		stagingRoot = parentRsInfo.getStagingRoot();
        	}

            Query query = new Query();
            query.setSelect(new Select(Arrays.asList(new ExpressionSymbol("expr", new Constant(1))))); //$NON-NLS-1$

            query.setFrom(new From(Arrays.asList(new UnaryFromClause(parentTempTable))));
            
            Map symbolMap = new HashMap();
            String inlineViewName = planEnv.getAliasName(rsInfo.getResultSetName());
            XMLQueryPlanner.updateSymbolMap(symbolMap, rsInfo.getResultSetName(), inlineViewName, planEnv.getGlobalMetadata());
            XMLQueryPlanner.updateSymbolMap(symbolMap, parentName, parentTempTable.getName(), planEnv.getGlobalMetadata());
            
            crit = (Criteria) crit.clone();
            ExpressionMappingVisitor.mapExpressions(crit, symbolMap);
            
            if (!stagableQuery.getSelect().isDistinct()) {
            	query.setHaving(crit);
                //group by is added so that subquery planning sees that we are distinct
                query.setGroupBy(new GroupBy(new ArrayList<ElementSymbol>(outerReferences)));
                ExpressionMappingVisitor.mapExpressions(query.getGroupBy(), symbolMap);
            } else {
            	query.setCriteria(crit);
            }
            ExistsCriteria ec = new ExistsCriteria();
            ec.setSubqueryHint(new ExistsCriteria.SubqueryHint());
            ec.getSubqueryHint().setDepJoin();
            ec.setCommand(query);
            Criteria existing = stagableQuery.getCriteria();
            stagableQuery.setCriteria(Criteria.combineCriteria(existing, ec));
            if (!XMLQueryPlanner.planStagaingQuery(false, groupName, stagingGroupName, stagableQuery, planEnv)) {
            	return false;
            }
            if (!parentStaged) {
            	//need to associate temp load/get/drop with the rsinfo for use by the execsqlinstruction
            	Insert insert = new Insert();
            	insert.setGroup(parentTempTable);
            	int valCount = parentRsInfo.getCommand().getProjectedSymbols().size();
            	ArrayList<Reference> vals = new ArrayList<Reference>(valCount);
            	for (int i = 0; i < valCount; i++) {
            		vals.add(new Reference(i+1));
            	}
            	insert.setValues(vals);
	        	QueryResolver.resolveCommand(insert, planEnv.getGlobalMetadata());
	        	
	        	Command tempCommand = QueryParser.getQueryParser().parseCommand("select * from " + parentStagingName); //$NON-NLS-1$
	        	QueryResolver.resolveCommand(tempCommand, planEnv.getGlobalMetadata());
	        	
            	Command dropCommand = QueryParser.getQueryParser().parseCommand("drop table " + parentStagingName); //$NON-NLS-1$
	        	QueryResolver.resolveCommand(dropCommand, planEnv.getGlobalMetadata());

	            parentRsInfo.setTempTable(parentStagingName);
	            parentRsInfo.setTempSelect(tempCommand);
	            parentRsInfo.setTempInsert(insert);
	            parentRsInfo.setTempDrop(dropCommand);
            }
            LogManager.logDetail(LogConstants.CTX_XML_PLANNER, "Using a dependent join to load the mapping class", groupName); //$NON-NLS-1$
            // add to the document that a staging table has been added
            sourceNode.addStagingTable(stagingGroupName);
            GroupSymbol tempGroup = new GroupSymbol(XMLQueryPlanner.getTempTableName(stagingGroupName));
        	ResolverUtil.resolveGroup(tempGroup, planEnv.getGlobalMetadata());
        	Collection<GroupSymbol> temp = Arrays.asList(tempGroup);
        	List<ElementSymbol> fk = new ArrayList<ElementSymbol>(fkColumns.size());
        	for (ElementSymbol elementSymbol : fkColumns) {
        		ElementSymbol es = new ElementSymbol(elementSymbol.getShortName());
        		ResolverVisitor.resolveLanguageObject(es, temp, planEnv.getGlobalMetadata());
        		fk.add(es);
			}
            ResultSetInfo stagedInfo = planEnv.getStagingTableResultsInfo(stagingGroupName);
        	stagedInfo.setStagingRoot(stagingRoot);
        	stagedInfo.setAutoStaged(true);
        	stagedInfo.addFkColumns(fk);
        	stagedInfo.setTempTable(tempGroup.getName());
        	
        	rsInfo.setAutoStaged(true);
            return true;
        }

        //id as mapping class
        Object metadataID = planEnv.getGlobalMetadata().getGroupID(sourceNode.getResultName());
        if (RelationalPlanner.isNoCacheGroup(planEnv.getGlobalMetadata(), metadataID, option)) {
            return false;
        }

        //id as generated mapping class name
        metadataID = planEnv.getGlobalMetadata().getGroupID(sourceNode.getActualResultSetName());
        if (RelationalPlanner.isNoCacheGroup(planEnv.getGlobalMetadata(), metadataID, option)) {
            return false;
        }

        stagableQuery = (Query)stagableQuery.clone();

        // stage the transformation query if it is successful
        if (!XMLQueryPlanner.planStagaingQuery(true, groupName, stagingGroupName, stagableQuery, planEnv)) {
            return false;
        }
        
        // add to the document that a staging table has been added
        sourceNode.addStagingTable(stagingGroupName);
        ResultSetInfo stagedInfo = planEnv.getStagingTableResultsInfo(stagingGroupName);
    	stagedInfo.setAutoStaged(true);
    	stagedInfo.setTempTable(XMLQueryPlanner.getTempTableName(stagingGroupName));
    	
    	rsInfo.setAutoStaged(true);
        return true;
    }

}
