/*
 * 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.
 * Modified by
 * @author  Data Virtuality GmbH
 * @date    $Date$
 *
 */
// https://issues.jboss.org/browse/TEIID-2506
// https://issues.jboss.org/browse/TEIID-2336
// https://issues.jboss.org/browse/TEIID-4953
// https://issues.jboss.org/browse/TEIID-5474
// https://issues.jboss.org/browse/TEIID-2939
// https://issues.redhat.com/browse/TEIID-5933
// https://issues.redhat.com/browse/TEIID-5443
package org.teiid.query.optimizer.relational.rules;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.teiid.api.exception.query.QueryMetadataException;
import org.teiid.api.exception.query.QueryPlannerException;
import org.teiid.core.TeiidComponentException;
import org.teiid.core.TeiidProcessingException;
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.ColumnMaskingHelper;
import org.teiid.query.optimizer.relational.OptimizerRule;
import org.teiid.query.optimizer.relational.RelationalPlanner;
import org.teiid.query.optimizer.relational.RowBasedSecurityHelper;
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.PlanNode;
import org.teiid.query.resolver.util.ResolverUtil;
import org.teiid.query.sql.lang.Command;
import org.teiid.query.sql.lang.Criteria;
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.WindowFunction;
import org.teiid.query.sql.util.SymbolMap;
import org.teiid.query.sql.visitor.AggregateSymbolCollectorVisitor;
import org.teiid.query.sql.visitor.GroupsUsedByElementsVisitor;
import org.teiid.query.util.CommandContext;

/**
 * Applies row/column security to a non-update plan
 * 
 * Should be run after rule assign output elements
 */
public class RuleApplySecurity implements OptimizerRule {

	@Override
	public PlanNode execute(PlanNode plan, QueryMetadataInterface metadata,
			CapabilitiesFinder capabilitiesFinder, RuleStack rules,
			AnalysisRecord analysisRecord, CommandContext context)
			throws QueryPlannerException, QueryMetadataException,
			TeiidComponentException {
		try {
			for (PlanNode sourceNode : NodeEditor.findAllNodes(plan, NodeConstants.Types.SOURCE)) {
				GroupSymbol group = sourceNode.getGroups().iterator().next();
				if (!RowBasedSecurityHelper.applyRowSecurity(metadata, group, context)) {
					continue;
				}
				List<ElementSymbol> cols = null;
				Command command = (Command) sourceNode.getProperty(Info.VIRTUAL_COMMAND);
				if (group.isExecutable()) {
					if (command == null) {
						continue; //proc relational, will instead apply at the proc level
					}
					if (cols == null) {
						cols = (List)command.getProjectedSymbols();
					}
				} else if (command != null && !command.returnsResultSet()) {
					continue; //should be handled in the planner
				}
				if (cols == null) {
		        	cols = ResolverUtil.resolveElementsInGroup(group, metadata);
				}
				
				//apply masks first
		        List<? extends Expression> masked = ColumnMaskingHelper.maskColumns(cols, group, metadata, context);
		        Map<ElementSymbol, Expression> mapping = null;
		        //TODO: we don't actually allow window function masks yet becuase they won't pass
		        //validation.  but if we do, we need to check for them here
		        List<WindowFunction> windowFunctions = new ArrayList<WindowFunction>(2);
		        for (int i = 0; i < masked.size(); i++) {
		        	Expression maskedCol = masked.get(i);
		        	AggregateSymbolCollectorVisitor.getAggregates(maskedCol, null, null, null, windowFunctions, null);
		        	if (maskedCol.equals(cols.get(i))) {
		        		continue;
		        	}
		        	if (mapping == null) {
		        		mapping = new HashMap<ElementSymbol, Expression>();
		        	}
		        	mapping.put(cols.get(i), SymbolMap.getExpression(maskedCol));
		        }
        	    PlanNode parentJoin = NodeEditor.findParent(sourceNode.getParent(), NodeConstants.Types.JOIN, NodeConstants.Types.SOURCE);
		        if (mapping != null) {
		        	//some element symbol has been replaced
	        		PlanNode project = null;
	        		if (group.isExecutable()) {
	        			project = NodeEditor.findParent(sourceNode, NodeConstants.Types.PROJECT);
		        		project.setProperty(NodeConstants.Info.PROJECT_COLS, masked);
	        		}
			        if (windowFunctions.isEmpty() && RuleMergeVirtual.checkProjectedSymbols(group, parentJoin, metadata, masked, Collections.singleton(group), true)){
		        		if (!group.isExecutable()) {
					FrameUtil.correctAliasedGroupSymbols(mapping);
		        			//just upwardly project - TODO: we could also handle some subquery simple projection situations here
		        			FrameUtil.convertFrame(sourceNode.getParent(), group, Collections.singleton(group), mapping, metadata);
					FrameUtil.correctSymbolMapKeys(mapping, sourceNode);
		        		}
	        		} else {
		        		if (!group.isExecutable()) {
		        			project = RelationalPlanner.createProjectNode(masked);
		        		}
	        			rules.getPlanner().planSubqueries(null, sourceNode.getGroups(), project, project.getSubqueryContainers(), true, false, context);
	        			project.addGroups(GroupsUsedByElementsVisitor.getGroups(project.getCorrelatedReferenceElements()));
	        			if (!group.isExecutable()) {
	        				//we need to insert a view to give a single place to evaluate the subqueries
	        				PlanNode root = sourceNode;
			            	if (sourceNode.getParent().getType() == NodeConstants.Types.ACCESS) {
			            		root = sourceNode.getParent();
			            	}
	        				root.addAsParent(project);
	        				addView(metadata, context, group, cols, project);
	        				parentJoin = null;
	        			}
		        	}
	        		if (!windowFunctions.isEmpty() && project != null) {
	        			project.setProperty(Info.HAS_WINDOW_FUNCTIONS, true);
	        		}
		        }
		        
	            //logically filters are applied below masking
		        Criteria filter = RowBasedSecurityHelper.getRowBasedFilters(metadata, group, context, false);
	            if (filter == null) {
	            	continue;
	            }
            	List<Criteria> crits = Criteria.separateCriteriaByAnd(filter);
            	PlanNode root = sourceNode;
            	if (sourceNode.getParent().getType() == NodeConstants.Types.ACCESS) {
            		root = sourceNode.getParent();
            	}
            	PlanNode parent = null;
        	    for (Criteria crit : crits) {
                    PlanNode critNode = RelationalPlanner.createSelectNode(crit, false);
                    if (parent == null) {
                    	parent = critNode;
                    }
                    rules.getPlanner().planSubqueries(null, sourceNode.getGroups(), critNode, critNode.getSubqueryContainers(), true, false, context);
                    critNode.addGroups(GroupsUsedByElementsVisitor.getGroups(critNode.getCorrelatedReferenceElements()));
                    root.addAsParent(critNode);
                }
        	    if (!RuleMergeVirtual.checkJoinCriteria(parent, group, parentJoin, metadata)) {
        	    	PlanNode project = RelationalPlanner.createProjectNode(cols);
        	    	parent.addAsParent(project);
        	    	//a view is needed to keep the logical placement of the criteria
    				addView(metadata, context, group, cols, project);
        	    }
			}
		} catch (TeiidProcessingException e) {
			throw new QueryPlannerException(e);
		}
		return plan;
	}

	private void addView(QueryMetadataInterface metadata,
			CommandContext context, GroupSymbol group,
			List<ElementSymbol> cols, PlanNode viewRoot) throws TeiidComponentException,
			QueryMetadataException, QueryPlannerException {
		GroupSymbol securityVeiw = new GroupSymbol("sec"); //$NON-NLS-1$
		Set<String> groups = context.getGroups();
		securityVeiw = RulePlaceAccess.recontextSymbol(securityVeiw, groups);
		List<ElementSymbol> newCols = RulePushAggregates.defineNewGroup(securityVeiw, cols, metadata);
		PlanNode newSourceNode = RuleDecomposeJoin.createSource(securityVeiw, viewRoot, newCols);
		Map<ElementSymbol, Expression> upperMapping = SymbolMap.createSymbolMap(cols, newCols).asMap();
		FrameUtil.convertFrame(newSourceNode.getParent(), group, Collections.singleton(securityVeiw), upperMapping, metadata);
	}

	@Override
	public String toString() {
		return "ApplySecurity"; //$NON-NLS-1$
	}

}
