/*
 * 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.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

import org.teiid.api.exception.query.QueryMetadataException;
import org.teiid.api.exception.query.QueryPlannerException;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.id.IDGenerator;
import org.teiid.logging.LogConstants;
import org.teiid.logging.LogManager;
import org.teiid.query.QueryPlugin;
import org.teiid.query.analysis.AnalysisRecord;
import org.teiid.query.mapping.xml.MappingDocument;
import org.teiid.query.mapping.xml.MappingNode;
import org.teiid.query.mapping.xml.MappingNodeLogger;
import org.teiid.query.mapping.xml.MappingRecursiveElement;
import org.teiid.query.mapping.xml.MappingSourceNode;
import org.teiid.query.mapping.xml.ResultSetInfo;
import org.teiid.query.metadata.QueryMetadataInterface;
import org.teiid.query.optimizer.CommandPlanner;
import org.teiid.query.optimizer.capabilities.CapabilitiesFinder;
import org.teiid.query.processor.ProcessorPlan;
import org.teiid.query.processor.xml.Program;
import org.teiid.query.processor.xml.XMLPlan;
import org.teiid.query.processor.xml.XMLProcessorEnvironment;
import org.teiid.query.sql.lang.Command;
import org.teiid.query.sql.lang.Criteria;
import org.teiid.query.sql.lang.OrderBy;
import org.teiid.query.sql.lang.Query;
import org.teiid.query.sql.lang.Select;
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.MultipleElementSymbol;
import org.teiid.query.sql.visitor.ElementCollectorVisitor;
import org.teiid.query.util.CommandContext;


/**
 * <p> This prepares an {@link org.teiid.query.processor.xml.XMLPlan XMLPlan} from
 * a Mapping Document structure of {@link org.teiid.query.mapping.xml.MappingNode MappingNodes}.
 * </p>
 */
public final class XMLPlanner implements CommandPlanner{

	/**
	 * Default constructor.  Since this object is stateless the constructor
	 * has nothing to do.
	 */
	public XMLPlanner() {}

	/**
	 * @see org.teiid.query.optimizer.CommandPlanner#optimize
	 */
	public ProcessorPlan optimize(Command command,IDGenerator idGenerator,QueryMetadataInterface metadata,CapabilitiesFinder capFinder,AnalysisRecord analysisRecord,CommandContext context)
		throws QueryPlannerException, QueryMetadataException, TeiidComponentException {

        XMLPlannerEnvironment env = new XMLPlannerEnvironment(metadata);

		return XMLPlanner.preparePlan(command, metadata, analysisRecord, env, idGenerator, capFinder, context);
	}

    /**
     * This method takes in a Command object of the user's query and returns a XML plan
     * as a XMLNode object.
     * @param command The Command object for which query plan is to be returned
     * @param metadata The metadata needed for planning
     * @param planEnv XMLPlanner.XMLPlannerEnvironment object which holds various data
     * @return The XML plan returned as an XMLPlan object
     * @throws QueryPlannerException
     * @throws QueryMetadataException
     * @throws TeiidComponentException
     */
    static XMLPlan preparePlan(Command command, QueryMetadataInterface metadata, AnalysisRecord analysisRecord, XMLPlannerEnvironment planEnv, IDGenerator idGenerator, CapabilitiesFinder capFinder, CommandContext context)
        throws QueryPlannerException, QueryMetadataException, TeiidComponentException {

        Query xmlQuery = (Query) command;        
        boolean debug = analysisRecord.recordDebug();
        if(debug) {
            analysisRecord.println("============================================================================"); //$NON-NLS-1$
            analysisRecord.println("XML COMMAND: " + xmlQuery); //$NON-NLS-1$
        }

        // lookup mapping node for the user command
        GroupSymbol group = xmlQuery.getFrom().getGroups().iterator().next();

        MappingDocument doc = (MappingDocument)metadata.getMappingNode(group.getMetadataID());
        doc = doc.clone();
        
        // make a copy of the document
        planEnv.mappingDoc = doc;
		planEnv.documentGroup = group;
        planEnv.xmlCommand = (Query) command;
        planEnv.analysisRecord = analysisRecord;
        planEnv.capFinder = capFinder;
        planEnv.idGenerator = idGenerator;
        planEnv.context = context;

        LogManager.logTrace(LogConstants.CTX_XML_PLANNER, new Object[]{"Mapping document tree", new MappingNodeLogger(planEnv.mappingDoc)}); //$NON-NLS-1$
        if(debug) {
            debugDocumentInfo("Start", planEnv); //$NON-NLS-1$
        }
                
		prePlan(planEnv, debug);

        // Generate program to create document
        Program programPlan = XMLPlanToProcessVisitor.planProgram(planEnv.mappingDoc, planEnv);
        
        // create plan from program and initialized environment
        XMLProcessorEnvironment env = new XMLProcessorEnvironment(programPlan, planEnv.documentGroup);
        XMLPlan plan = new XMLPlan(env);
    	plan.setXMLSchemas(metadata.getXMLSchemas(group.getMetadataID()));
        if(debug) {
            analysisRecord.println(""); //$NON-NLS-1$
            analysisRecord.println(plan.toString());
            analysisRecord.println("============================================================================"); //$NON-NLS-1$
        }

        return plan;
	}

    private static void debugDocumentInfo(String msgTag, XMLPlannerEnvironment planEnv) {
        planEnv.analysisRecord.println("\n"+msgTag+":============================================================================"); //$NON-NLS-1$ //$NON-NLS-2$
        planEnv.analysisRecord.println("MAPPING DOCUMENT:\n" + MappingNode.toStringNodeTree(planEnv.mappingDoc)); //$NON-NLS-1$
    }

     
    /**
     * Pre planning - steps that occur before planning of the XML Query plan.
     * @param root root of mapping document
     * @param rsCrits Map of String mapping class result set name to Criteria object
     * @param rsOrderBy Map of String mapping class result set name to OrderBy clause
     * @param planEnv an instance of XMLPlannerEnvironment for passing around information
     * @param metadata QueryMetadataInterface instance
     * @throws QueryPlannerException for any logical exception detected during planning
     * @throws QueryMetadataException if metadata encounters exception
     * @throws TeiidComponentException unexpected exception
     */
	private static void prePlan(XMLPlannerEnvironment planEnv, boolean debug)
		throws QueryPlannerException, QueryMetadataException, TeiidComponentException {
        
        //extract source nodes
        planEnv.mappingDoc = SourceNodeGenaratorVisitor.extractSourceNodes(planEnv.mappingDoc);
        
        if (debug) {
            debugDocumentInfo("After Source Node Generation", planEnv); //$NON-NLS-1$
        }
        
        //raise input set criteria if possible
        SourceNodePlannerVisitor.raiseInputSet(planEnv.mappingDoc, planEnv);
        
        //Place the orderbys 
        placeOrderBys(planEnv.xmlCommand.getOrderBy(), planEnv);
        
        //prepare any user-specified criteria in command
        Criteria crit = planEnv.xmlCommand.getCriteria();
        CriteriaPlanner.placeUserCriteria(crit, planEnv);

        //Plan the various relational result sets along with criteria and order by
        XMLQueryPlanner.prePlanQueries(planEnv.mappingDoc, planEnv);
        
        if (debug) {
            debugDocumentInfo("After Pre Plan Queries", planEnv); //$NON-NLS-1$
        }
        
		preMarkExcluded(planEnv.xmlCommand, planEnv.mappingDoc);

        if (debug) {
            debugDocumentInfo("After Mark Exclude", planEnv); //$NON-NLS-1$
        }
        
        //Remove excluded document subtrees
        removeExcluded(planEnv.mappingDoc);
        
        if (debug) {
            debugDocumentInfo("After Exclude", planEnv); //$NON-NLS-1$
        }

        //Resolve all the "elements" against the result sets
        NameInSourceResolverVisitor.resolveElements(planEnv.mappingDoc, planEnv);
        
        //Validate and resolve the criteria specified on the mapping nodes.
        ValidateMappedCriteriaVisitor.validateAndCollectCriteriaElements(planEnv.mappingDoc, planEnv);

        XMLProjectionMinimizer.minimizeProjection(planEnv);
        
        if (debug) {
            debugDocumentInfo("After Projection Minimization", planEnv); //$NON-NLS-1$
        }
        
        // Autostage queries. try to auto-stage the planned queries
        // removal of this step should not affect overall processing
        XMLStagaingQueryPlanner.stageQueries(planEnv.mappingDoc, planEnv);
        
        //Plan the various relational result sets
        XMLQueryPlanner.optimizeQueries(planEnv.mappingDoc, planEnv);
        
		//Handle nillable nodes
        planEnv.mappingDoc = HandleNillableVisitor.execute(planEnv.mappingDoc);
	}
    
    static void removeExcluded(MappingNode node) {
        for (Iterator<MappingNode> i = node.getChildren().iterator(); i.hasNext();) {
            MappingNode child = i.next();
            if (!(node instanceof MappingRecursiveElement) && child.isExcluded()) {
                i.remove();
            } else {
                removeExcluded(child);
            }
        }
    }

    /**
     * Is selection is not "select *", then mark all those un used nodes as excluded. The way
     * we do this mark the nodes selected and their parents as selected, then we sweep to mark
     * everybody who is not marked "included" as excluded.
     * @param xmlCommand
     */
    static MappingDocument preMarkExcluded(Query xmlCommand, MappingDocument doc) {
        Select select = xmlCommand.getSelect();
        Expression firstSymbol = select.getSymbol(0);

        // 0. mark the nodes to be excluded
        if(firstSymbol instanceof MultipleElementSymbol && ((MultipleElementSymbol)firstSymbol).getGroup() == null) {
            return doc;
        }
        
        // Get all the valid nodes to be marked as included.
        Collection<ElementSymbol> validElements = ElementCollectorVisitor.getElements(select, true);
        HashSet<String> elements = new HashSet<String>(validElements.size());
        for (ElementSymbol element : validElements) {
            elements.add(element.getName());
        }
        
        // keep the nodes given mark the rest of the nodes to be eliminated
        return MarkExcludeVisitor.markExcludedNodes(doc, elements);
     }    
        
   
	/**
	 * Get the result sets of all orderby's elements
	 * @param orderBy The fully resolved order by clause
	 * @param planEnv XMLPlannerEnvironment
	 * @param metadata QueryMetadataInterface
	 * @return Map The hash map of result sets and corresponding order by clauses
	 * @throws QueryPlannerException
	 * @throws QueryMetadataException
	 * @throws TeiidComponentException
	 */
	private static void placeOrderBys(OrderBy orderBy, XMLPlannerEnvironment planEnv)
		throws QueryPlannerException, QueryMetadataException, TeiidComponentException {
        //prepare fully resolved Order By elements
        
        if (orderBy == null) {
            return;
        }
        
        List<Expression> elements = orderBy.getSortKeys();
		List<Boolean> types = orderBy.getTypes();

		for (int i = 0; i< elements.size(); i++) {
			ElementSymbol elemSymbol = (ElementSymbol) elements.get(i);
			
            String nodeName = planEnv.getGlobalMetadata().getFullName(elemSymbol.getMetadataID());
            MappingNode elementNode = MappingNode.findNode(planEnv.mappingDoc, nodeName); 

            // make sure that the name in source is defined for this node, so that it can be used
            // in the orderby. static nodes do not qualify for ordering.
            if (elementNode.getNameInSource() == null){
                Object[] params = new Object[] {elementNode, orderBy};
                String msg = QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30288, params);
                 throw new QueryPlannerException(QueryPlugin.Event.TEIID30288, msg);
            }
            
            MappingSourceNode sourceNode = elementNode.getSourceNode();
            ResultSetInfo rs = sourceNode.getResultSetInfo();
            OrderBy by = rs.getOrderBy();
            if (by == null) {
                by = new OrderBy();
            }
            ElementSymbol mappedSymbol = (ElementSymbol)sourceNode.getSymbolMap().get(new ElementSymbol(elementNode.getNameInSource()));
            by.addVariable(mappedSymbol.clone(), types.get(i));
            rs.setOrderBy(by);
		}
	}
        
}