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

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

import org.teiid.core.types.DataTypeManager;
import org.teiid.core.util.EquivalenceUtil;
import org.teiid.core.util.HashCodeUtil;
import org.teiid.query.parser.SQLParserUtil;
import org.teiid.query.sql.LanguageObject;
import org.teiid.query.sql.LanguageVisitor;
import org.teiid.query.sql.lang.OrderBy;


/**
 * <p>An aggregate symbol represents an aggregate function. The * expression
 * is encoded by setting the expression to null.  This may ONLY be used with the COUNT function.</p>
 *
 * <p>The type of an aggregate symbol depends on the function and the type of the underlying
 * expression.  The type of a COUNT function is ALWAYS integer.  MIN and MAX functions take the
 * type of their contained expression.</p>
 */
public class AggregateSymbol extends Function implements DerivedExpression {
	
	private static final Expression[] EMPTY_ARGS = new Expression[0];

	public enum Type {
		COUNT,
		SUM,
		AVG,
		MIN,
		MAX,
		XMLAGG,
		TEXTAGG,
		ARRAY_AGG,
		JSONARRAY_AGG,
		ANY,
		SOME,
		EVERY,
		STDDEV_POP,
		STDDEV_SAMP,
		VAR_POP,
		VAR_SAMP,
		RANK,
		DENSE_RANK,
		ROW_NUMBER,
		FIRST_VALUE,
		LAST_VALUE,
        LEAD,
        LAG,
		STRING_AGG,
		USER_DEFINED;
	}
	
	private static final Map<String, Type> nameMap = new TreeMap<String, Type>(String.CASE_INSENSITIVE_ORDER);
	
	static {
		for (Type t : Type.values()) {
			if (t == Type.USER_DEFINED) {
				continue;
			}
			nameMap.put(t.name(), t);
		}
	}

	private Type aggregate;
	private boolean distinct;
	private OrderBy orderBy;
	private Expression condition;
	private boolean isWindowed;

	private static final Class<Integer> COUNT_TYPE = DataTypeManager.DefaultDataClasses.INTEGER;
	private static final Map<Class<?>, Class<?>> SUM_TYPES;
    private static final Map<Class<?>, Class<?>> AVG_TYPES;

	static {
		SUM_TYPES = new HashMap<Class<?>, Class<?>>();
		SUM_TYPES.put(DataTypeManager.DefaultDataClasses.BYTE, DataTypeManager.DefaultDataClasses.LONG);
		SUM_TYPES.put(DataTypeManager.DefaultDataClasses.SHORT, DataTypeManager.DefaultDataClasses.LONG);
		SUM_TYPES.put(DataTypeManager.DefaultDataClasses.INTEGER, DataTypeManager.DefaultDataClasses.LONG);
		SUM_TYPES.put(DataTypeManager.DefaultDataClasses.LONG, DataTypeManager.DefaultDataClasses.LONG);
		SUM_TYPES.put(DataTypeManager.DefaultDataClasses.BIG_INTEGER, DataTypeManager.DefaultDataClasses.BIG_INTEGER);
		SUM_TYPES.put(DataTypeManager.DefaultDataClasses.FLOAT, DataTypeManager.DefaultDataClasses.DOUBLE);
		SUM_TYPES.put(DataTypeManager.DefaultDataClasses.DOUBLE, DataTypeManager.DefaultDataClasses.DOUBLE);
		SUM_TYPES.put(DataTypeManager.DefaultDataClasses.BIG_DECIMAL, DataTypeManager.DefaultDataClasses.BIG_DECIMAL);
        
        AVG_TYPES = new HashMap<Class<?>, Class<?>>();
        AVG_TYPES.put(DataTypeManager.DefaultDataClasses.BYTE, SQLParserUtil.DECIMAL_AS_DOUBLE?DataTypeManager.DefaultDataClasses.DOUBLE:DataTypeManager.DefaultDataClasses.BIG_DECIMAL);
        AVG_TYPES.put(DataTypeManager.DefaultDataClasses.SHORT, SQLParserUtil.DECIMAL_AS_DOUBLE?DataTypeManager.DefaultDataClasses.DOUBLE:DataTypeManager.DefaultDataClasses.BIG_DECIMAL);
        AVG_TYPES.put(DataTypeManager.DefaultDataClasses.INTEGER, SQLParserUtil.DECIMAL_AS_DOUBLE?DataTypeManager.DefaultDataClasses.DOUBLE:DataTypeManager.DefaultDataClasses.BIG_DECIMAL);
        AVG_TYPES.put(DataTypeManager.DefaultDataClasses.LONG, SQLParserUtil.DECIMAL_AS_DOUBLE?DataTypeManager.DefaultDataClasses.DOUBLE:DataTypeManager.DefaultDataClasses.BIG_DECIMAL);
        AVG_TYPES.put(DataTypeManager.DefaultDataClasses.BIG_INTEGER, DataTypeManager.DefaultDataClasses.BIG_DECIMAL);
        AVG_TYPES.put(DataTypeManager.DefaultDataClasses.FLOAT, DataTypeManager.DefaultDataClasses.DOUBLE);
        AVG_TYPES.put(DataTypeManager.DefaultDataClasses.DOUBLE, DataTypeManager.DefaultDataClasses.DOUBLE);
        AVG_TYPES.put(DataTypeManager.DefaultDataClasses.BIG_DECIMAL, DataTypeManager.DefaultDataClasses.BIG_DECIMAL);        
	}

    /**
     * Constructor used for cloning 
     * @param name
     * @param canonicalName
     * @since 4.3
     */
    protected AggregateSymbol(String name, Type aggregateFunction, boolean isDistinct, Expression[] args) {
        super(name, args);
        this.aggregate = aggregateFunction;
        this.distinct = isDistinct;
    }
    
	/**
	 * Construct an aggregate symbol with all given data.
	 * @param aggregateFunction Aggregate function type ({@link org.teiid.language.SQLConstants.NonReserved#COUNT}, etc)
	 * @param isDistinct True if DISTINCT flag is set
	 * @param expression Contained expression
	 */
	public AggregateSymbol(String aggregateFunction, boolean isDistinct, Expression expression) {
		this(aggregateFunction, isDistinct, expression == null?EMPTY_ARGS:new Expression[] {expression}, null);
	}
    
	public AggregateSymbol(String aggregateFunction, boolean isDistinct, Expression[] args, OrderBy orderBy) {
		super(aggregateFunction, args);
		this.aggregate = nameMap.get(aggregateFunction);
		if (this.aggregate == null) {
			this.aggregate = Type.USER_DEFINED;
		}
		this.distinct = isDistinct;
		this.orderBy = orderBy;
	}
	
	/**
	 * Set the aggregate function.  If the aggregate function is an invalid value, an
	 * IllegalArgumentException is thrown.
	 * @param aggregateFunction Aggregate function type
	 * @see org.teiid.language.SQLConstants.NonReserved#COUNT
	 * @see org.teiid.language.SQLConstants.NonReserved#SUM
	 * @see org.teiid.language.SQLConstants.NonReserved#AVG
	 * @see org.teiid.language.SQLConstants.NonReserved#MIN
	 * @see org.teiid.language.SQLConstants.NonReserved#MAX
	 */
	public void setAggregateFunction(Type aggregateFunction) {
		this.aggregate = aggregateFunction;
	}

	/**
	 * Get the aggregate function type - this will map to one of the reserved words
	 * for the aggregate functions.
	 * @return Aggregate function type
	 */
	public Type getAggregateFunction() {
		return this.aggregate;
	}
	
	public boolean isRowValueFunction() {
	    switch (aggregate) {
	    case ROW_NUMBER:
	    case LEAD:
	    case LAG:
	        return true;
        default:
            return false;
	    }
	}

	/**
	 * Get the distinct flag.  If true, aggregate symbol will remove duplicates during
	 * computation.
	 * @return True if duplicates should be removed during computation
	 */
	public boolean isDistinct() {
		return this.distinct;
	}
	
	public void setDistinct(boolean distinct) {
		this.distinct = distinct;
	}

	/**
	 * Get the type of the symbol, which depends on the aggregate function and the
	 * type of the contained expression
	 * @return Type of the symbol
	 */
	public Class<?> getType() {
		switch (this.aggregate) {
		case COUNT:
			return COUNT_TYPE;
		case SUM:
			Class<?> expressionType = this.getArg(0).getType();
			return SUM_TYPES.get(expressionType);
		case AVG:
            expressionType = this.getArg(0).getType();
            return AVG_TYPES.get(expressionType);
		case ARRAY_AGG:
			if (this.getArg(0) == null) {
				return null;
			}
			return DataTypeManager.getArrayType(this.getArg(0).getType());
		case TEXTAGG:
			return DataTypeManager.DefaultDataClasses.BLOB;
		case USER_DEFINED:
			if (this.getFunctionDescriptor() == null) {
				return null;
			}
			return this.getFunctionDescriptor().getReturnType();
		case JSONARRAY_AGG:
			return DataTypeManager.DefaultDataClasses.CLOB;
		case STRING_AGG:
			return super.getType();
		}
		if (isBoolean()) {
			return DataTypeManager.DefaultDataClasses.BOOLEAN;
		}
		if (isEnhancedNumeric()) {
			return DataTypeManager.DefaultDataClasses.DOUBLE;
		}
		if (isAnalytical()) {
			return DataTypeManager.DefaultDataClasses.INTEGER;
		}
		if (this.getArgs().length == 0) {
			return null;
		}
		return this.getArg(0).getType();
	}
	
	public boolean isAnalytical() {
		switch (this.aggregate) {
		case RANK:
		case ROW_NUMBER:
		case DENSE_RANK:
		case FIRST_VALUE:
		case LAST_VALUE:
		case LEAD:
        case LAG:
			return true;
		default:
		    return false;
		}
	}

	public boolean isBoolean() {
		return this.aggregate == Type.EVERY 
				|| this.aggregate == Type.SOME 
				|| this.aggregate == Type.ANY;
	}
	
	public boolean isEnhancedNumeric() {
		return this.aggregate == Type.STDDEV_POP 
		|| this.aggregate == Type.STDDEV_SAMP
		|| this.aggregate == Type.VAR_SAMP
		|| this.aggregate == Type.VAR_POP;
	}

    public void acceptVisitor(LanguageVisitor visitor) {
        visitor.visit(this);
    }
    
    public OrderBy getOrderBy() {
		return orderBy;
	}
    
    public void setOrderBy(OrderBy orderBy) {
		this.orderBy = orderBy;
	}

	/**
	 * Return a deep copy of this object
	 */
	public Object clone() {
		AggregateSymbol copy = new AggregateSymbol(getName(), getAggregateFunction(), isDistinct(), LanguageObject.Util.deepClone(getArgs()));
		if (orderBy != null) {
			copy.setOrderBy(orderBy.clone());
		}
		if (condition != null) {
			copy.setCondition((Expression) condition.clone());
		}
		copy.isWindowed = this.isWindowed;
		copy.type = this.type;
		copy.setFunctionDescriptor(getFunctionDescriptor());
		return copy;
	}
    
    /** 
     * @see org.teiid.query.sql.symbol.ExpressionSymbol#hashCode()
     */
    public int hashCode() {
        int hasCode = HashCodeUtil.hashCode(aggregate.hashCode(), distinct);
        return HashCodeUtil.hashCode(hasCode, super.hashCode());
    }
    
    /** 
     * @see org.teiid.query.sql.symbol.ExpressionSymbol#equals(java.lang.Object)
     */
    public boolean equals(Object obj) {
        if (!(obj instanceof AggregateSymbol)) {
            return false;
        }
        
        AggregateSymbol other = (AggregateSymbol)obj;
        
        return super.equals(obj)
               && this.aggregate.equals(other.aggregate)
               && this.distinct == other.distinct
               && this.isWindowed == other.isWindowed
               && EquivalenceUtil.areEqual(this.condition, other.condition)
        	   && EquivalenceUtil.areEqual(this.getOrderBy(), other.getOrderBy());
    }
    
    public boolean isCardinalityDependent() {
    	if (isDistinct()) {
    		return false;
    	}
    	switch (getAggregateFunction()) {
		case MAX:
		case MIN:
		case ANY:
		case SOME:
		case EVERY:
			return false;
		case USER_DEFINED:
			return !getFunctionDescriptor().getMethod().getAggregateAttributes().usesDistinctRows();
		}
		return true;
    }
    
    public Expression getCondition() {
		return condition;
	}
    
    public void setCondition(Expression condition) {
		this.condition = condition;
	}

	public static boolean areAggregatesCardinalityDependent(Collection<AggregateSymbol> aggs) {
		for (AggregateSymbol aggregateSymbol : aggs) {
			if (aggregateSymbol.isCardinalityDependent()) {
				return true;
			}
		}
		return false;
	}
	
	public boolean respectsNulls() {
		switch (this.aggregate) {
		case TEXTAGG:
		case ARRAY_AGG:
		case JSONARRAY_AGG:
			return true;
		}
		return false;
	}
	
	public boolean canStage() {
		if (orderBy != null) {
			return false;
		}
		switch (this.aggregate) {
		case TEXTAGG:
		case ARRAY_AGG:
		case JSONARRAY_AGG:
		case STRING_AGG:
			return false;
		case USER_DEFINED:
			return this.getArgs().length == 1 && this.getFunctionDescriptor().getMethod().getAggregateAttributes().isDecomposable();
		}
		return true;
	}
	
	public boolean isWindowed() {
		return isWindowed;
	}
	
	public void setWindowed(boolean isWindowed) {
		this.isWindowed = isWindowed;
	}
	
}
