/*

   Derby - Class org.apache.derby.impl.sql.compile.GroupByList

   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to you under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.

 */

package	org.apache.derby.impl.sql.compile;

import java.util.List;
import org.apache.derby.iapi.error.StandardException;
import org.apache.derby.iapi.reference.Limits;
import org.apache.derby.iapi.reference.SQLState;
import org.apache.derby.iapi.services.context.ContextManager;
import org.apache.derby.shared.common.sanity.SanityManager;

/**
 * A GroupByList represents the list of expressions in a GROUP BY clause in
 * a SELECT statement.
 *
 */

class GroupByList extends OrderedColumnList<GroupByColumn>
{
	int		numGroupingColsAdded = 0;
	boolean         rollup = false;

    public GroupByList(ContextManager cm) {
        super(GroupByColumn.class, cm);
    }


	/**
		Add a column to the list

		@param column	The column to add to the list
	 */
    void addGroupByColumn(GroupByColumn column)
	{
		addElement(column);
	}

	/**
		Get a column from the list

		@param position	The column to get from the list
	 */
    GroupByColumn getGroupByColumn(int position)
	{
		if (SanityManager.DEBUG)
		{
			SanityManager.ASSERT(position >=0 && position < size(),
					"position (" + position +
					") expected to be between 0 and " + size());
		}
        return elementAt(position);
	}


    void setRollup()
	{
		rollup = true;
	}
    boolean isRollup()
	{
		return rollup;
	}
                        

	/**
	 * Get the number of grouping columns that need to be added to the SELECT list.
	 *
	 * @return int	The number of grouping columns that need to be added to
	 *				the SELECT list.
	 */
    int getNumNeedToAddGroupingCols()
	{
		return numGroupingColsAdded;
	}

	/**
	 *  Bind the group by list.  Verify:
	 *		o  Number of grouping columns matches number of non-aggregates in
	 *		   SELECT's RCL.
	 *		o  Names in the group by list are unique
	 *		o  Names of grouping columns match names of non-aggregate
	 *		   expressions in SELECT's RCL.
	 *
	 * @param select		The SelectNode
     * @param aggregates    The aggregate list being built as we find AggregateNodes
	 *
	 * @exception StandardException		Thrown on error
	 */
    void bindGroupByColumns(SelectNode select, List<AggregateNode> aggregates)
					throws StandardException
	{
		FromList		 fromList = select.getFromList();
		ResultColumnList selectRCL = select.getResultColumns();
        SubqueryList dummySubqueryList = new SubqueryList(getContextManager());
		int				 numColsAddedHere = 0;

		/* Only 32677 columns allowed in GROUP BY clause */
        if (size() > Limits.DB2_MAX_ELEMENTS_IN_GROUP_BY)
		{
			throw StandardException.newException(SQLState.LANG_TOO_MANY_ELEMENTS);
		}

		/* Bind the grouping column */
        for (GroupByColumn groupByCol : this)
		{
			groupByCol.bindExpression(fromList,
                                      dummySubqueryList, aggregates);
		}

		
		int				rclSize = selectRCL.size();
        for (GroupByColumn groupingCol : this)
		{
			boolean				matchFound = false;

			/* Verify that this entry in the GROUP BY list matches a
			 * grouping column in the select list.
			 */
			for (int inner = 0; inner < rclSize; inner++)
			{
                ResultColumn selectListRC = selectRCL.elementAt(inner);
				if (!(selectListRC.getExpression() instanceof ColumnReference)) {
					continue;
				}
				
				ColumnReference selectListCR = (ColumnReference) selectListRC.getExpression();

				if (selectListCR.isEquivalent(groupingCol.getColumnExpression())) { 
					/* Column positions for grouping columns are 0-based */
					groupingCol.setColumnPosition(inner + 1);

					/* Mark the RC in the SELECT list as a grouping column */
					selectListRC.markAsGroupingColumn();
					matchFound = true;
					break;
				}
			}
			/* If no match found in the SELECT list, then add a matching
			 * ResultColumn/ColumnReference pair to the SelectNode's RCL.
			 * However, don't add additional result columns if the query
			 * specified DISTINCT, because distinct processing considers
			 * the entire RCL and including extra columns could change the
			 * results: e.g. select distinct a,b from t group by a,b,c
			 * should not consider column c in distinct processing (DERBY-3613)
			 */
			if (! matchFound && !select.hasDistinct() &&
			    groupingCol.getColumnExpression() instanceof ColumnReference) 
			{
			    	// only add matching columns for column references not 
			    	// expressions yet. See DERBY-883 for details. 
				ResultColumn newRC;

				/* Get a new ResultColumn */
                newRC = new ResultColumn(
                        groupingCol.getColumnName(),
                        groupingCol.getColumnExpression().getClone(),
                        getContextManager());
				newRC.setVirtualColumnId(selectRCL.size() + 1);
				newRC.markGenerated();
				newRC.markAsGroupingColumn();

				/* Add the new RC/CR to the RCL */
				selectRCL.addElement(newRC);

				/* Set the columnPosition in the GroupByColumn, now that it
				* has a matching entry in the SELECT list.
				*/
				groupingCol.setColumnPosition(selectRCL.size());
				
				// a new hidden or generated column is added to this RCL
				// i.e. that the size() of the RCL != visibleSize(). 
				// Error checking done later should be aware of this 
				// special case.
				selectRCL.setCountMismatchAllowed(true);

				/*
				** Track the number of columns that we have added
				** in this routine.  We track this separately
				** than the total number of columns added by this
				** object (numGroupingColsAdded) because we
				** might be bound (though not gagged) more than
				** once (in which case numGroupingColsAdded will
				** already be set).
				*/
				numColsAddedHere++;
			}
			if (groupingCol.getColumnExpression() instanceof JavaToSQLValueNode) 
			{
				// disallow any expression which involves native java computation. 
				// Not possible to consider java expressions for equivalence.
				throw StandardException.newException(					
						SQLState.LANG_INVALID_GROUPED_SELECT_LIST);
			}
		}

		/* Verify that no subqueries got added to the dummy list */
        if (dummySubqueryList.size() != 0) {
            throw StandardException.newException(
                    SQLState.LANG_SUBQUERY_IN_GROUPBY_LIST);
        }

		numGroupingColsAdded+= numColsAddedHere;
	}

	

	/**
	 * Find the matching grouping column if any for the given expression
	 * 
	 * @param node an expression for which we are trying to find a match
	 * in the group by list.
	 * 
	 * @return the matching GroupByColumn if one exists, null otherwise.
	 * 
	 * @throws StandardException
	 */
    GroupByColumn findGroupingColumn(ValueNode node)
	        throws StandardException
	{
        for (GroupByColumn gbc : this)
		{
			if (gbc.getColumnExpression().isEquivalent(node))
			{
				return gbc;
			}
		}
		return null;
	}
	
	/**
	 * Remap all ColumnReferences in this tree to be clones of the
	 * underlying expression.
	 *
	 * @exception StandardException			Thrown on error
	 */
    void remapColumnReferencesToExpressions() throws StandardException
	{
        /* This method is called when flattening a FromTable. */
        for (GroupByColumn gbc : this)
		{
            gbc.setColumnExpression(
                gbc.getColumnExpression().remapColumnReferencesToExpressions());
		}
	}


	/**
	 * Convert this object to a String.  See comments in QueryTreeNode.java
	 * for how this should be done for tree printing.
	 *
	 * @return	This object as a String
	 */
    @Override
	public String toString()
	{
		if (SanityManager.DEBUG) {
			return "numGroupingColsAdded: " + numGroupingColsAdded + "\n" +
				super.toString();
		} else {
			return "";
		}
	}


    void preprocess(int numTables,
                    FromList fromList,
                    SubqueryList whereSubquerys,
                    PredicateList wherePredicates) throws StandardException
	{
        for (GroupByColumn gbc : this)
		{
            gbc.setColumnExpression(
                    gbc.getColumnExpression().preprocess(
							numTables, fromList, whereSubquerys, wherePredicates));
		}		
	}
}
