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

import org.teiid.core.TeiidException;
import org.teiid.core.TeiidRuntimeException;
import org.teiid.query.mapping.relational.QueryNode;
import org.teiid.query.mapping.xml.MappingAttribute;
import org.teiid.query.mapping.xml.MappingCriteriaNode;
import org.teiid.query.mapping.xml.MappingElement;
import org.teiid.query.mapping.xml.MappingNode;
import org.teiid.query.mapping.xml.MappingRecursiveElement;
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.resolver.util.ResolverUtil;
import org.teiid.query.sql.LanguageObject;
import org.teiid.query.sql.lang.Criteria;
import org.teiid.query.sql.lang.Query;
import org.teiid.query.sql.lang.Select;
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.visitor.ElementCollectorVisitor;
import org.teiid.query.sql.visitor.ExpressionMappingVisitor;

public class XMLProjectionMinimizer {

	/**
	 * Implements projection minimization through two passes over the document
	 */
	static void minimizeProjection(final XMLPlannerEnvironment planEnv) {
	    final HashMap<MappingSourceNode, LinkedHashSet<ElementSymbol>> neededElements = new HashMap<MappingSourceNode, LinkedHashSet<ElementSymbol>>();
	    
	    //collect included elements
	    MappingVisitor visitor = new Navigator(true, new MappingVisitor() {
	    	@Override
	    	public void visit(MappingAttribute attribute) {
	    		collectElementSymbol(attribute);
	    	}
	
			private void collectElementSymbol(MappingNode node) {
				if (node.isExcluded() || node.getElementSymbol() == null) {
	    			return;
	    		}
	    		MappingSourceNode msn = node.getSourceNode();
	    		ElementSymbol es = node.getElementSymbol();
	    		collectElementSymbol(msn, es);
			}
	
			private void collectElementSymbol(
					MappingSourceNode msn, ElementSymbol es) {
				msn = getActualSourceNode(msn);
				LinkedHashSet<ElementSymbol> elems = neededElements.get(msn);
	    		if (elems == null) {
	    			elems = new LinkedHashSet<ElementSymbol>();
	    			neededElements.put(msn, elems);
	    		}
	    		elems.add(es);
			}
	    	
	    	@Override
	    	public void visit(MappingElement element) {
	    		collectElementSymbol(element);
	    	}
	    	
	    	@Override
	    	public void visit(MappingSourceNode element) {
	    		try {
	        		QueryNode node = QueryUtil.getQueryNode(element.getResultName(), planEnv.getGlobalMetadata());
	                            
	                Collection<ElementSymbol> bindings = QueryUtil.getBindingElements(node);
	                MappingSourceNode parent = element.getParentSourceNode();
	                collectElementSymbols(element, bindings, parent);
	    		} catch (TeiidException e) {
	    			 throw new TeiidRuntimeException(e);
	    		}
	    	}
	
			private void collectElementSymbols(
					MappingSourceNode element,
					Collection<ElementSymbol> bindings, MappingSourceNode parent) {
				for (ElementSymbol elementSymbol : bindings) {
					if (element != null) {
						elementSymbol = element.getMappedSymbol(elementSymbol);
					}
					MappingSourceNode nextParent = parent;
					while (nextParent != null) {
						if (nextParent.getActualResultSetName().equalsIgnoreCase(elementSymbol.getGroupSymbol().getNonCorrelationName())) {
							collectElementSymbol(nextParent, elementSymbol);
							break;
						}
						nextParent = nextParent.getParentSourceNode();
					}
				}
			}
	    	
	    	@Override
	    	public void visit(MappingCriteriaNode element) {
	    		Criteria crit = element.getCriteriaNode();
	    		if (crit == null) {
	    			return;
	    		}
	    		collectElementSymbols(null, ElementCollectorVisitor.getElements(crit, true), element.getSourceNode());
	    	}
	    	
	    	@Override
	    	public void visit(MappingRecursiveElement element) {
	    		Criteria crit = element.getCriteriaNode();
	    		if (crit == null) {
	    			return;
	    		}
	    		collectElementSymbols(null, ElementCollectorVisitor.getElements(crit, true), element.getSourceNode());
	    	}
	
	    });
	    planEnv.mappingDoc.acceptVisitor(visitor);
	    
	    visitor = new Navigator(true, new MappingVisitor() {
	    	@Override
	    	public void visit(MappingSourceNode element) {
	    		try {
	        		ResultSetInfo rsInfo = element.getResultSetInfo();
	        		Query rsQuery = (Query)rsInfo.getCommand();
	        		if (rsQuery.getSelect().isDistinct()) {
	        			return;
	        		}
	        		LinkedHashSet<ElementSymbol> elements = neededElements.get(element);
	        		if (elements != null) {
	            		rsQuery.setSelect(new Select(LanguageObject.Util.deepClone(elements, ElementSymbol.class)));
	        		} else {
	        			String alias = element.getAliasResultName();
	        			if (alias == null) {
	        				rsQuery.setSelect(new Select(Arrays.asList(new ExpressionSymbol("foo", new Constant(1))))); //$NON-NLS-1$
	        			} else {
	        				MappingSourceNode actual = getActualSourceNode(element);
	        				elements = neededElements.get(actual);
	        				if (elements != null) {
								Map reverseMap = QueryUtil.createSymbolMap(new GroupSymbol(element.getAliasResultName()), 
										rsInfo.getResultSetName(),
										ResolverUtil.resolveElementsInGroup(QueryUtil.createResolvedGroup(element.getAliasResultName(), planEnv.getGlobalMetadata()), planEnv.getGlobalMetadata()));
								Select select = new Select(new ArrayList<Expression>(elements));
								ExpressionMappingVisitor.mapExpressions(select, reverseMap);
								rsQuery.setSelect(select);
							}
	        			}
	        		}
	    		} catch (TeiidException e) {
	    			 throw new TeiidRuntimeException(e);
	    		}
	    	}

	    });
	    planEnv.mappingDoc.acceptVisitor(visitor);
	}
	
	private static MappingSourceNode getActualSourceNode(MappingSourceNode element) {
		if (element.getAliasResultName() == null) {
			return element;
		}
		String actual = element.getActualResultSetName();
		MappingSourceNode parent = element.getParentSourceNode();
		while (parent != null) {
			if (parent.getActualResultSetName().equalsIgnoreCase(actual) ) {
				return parent;
			}
			parent = parent.getParentSourceNode();
		}
		return null;
	}

}
