package org.eclipse.birt.data.engine.script;

import org.eclipse.birt.core.data.DataTypeUtil;
import org.eclipse.birt.core.exception.BirtException;
import org.eclipse.birt.core.script.ScriptContext;
import org.eclipse.birt.core.script.ScriptExpression;
import org.eclipse.birt.data.engine.api.IConditionalExpression;
import org.eclipse.birt.data.engine.api.IScriptExpression;
import org.eclipse.birt.data.engine.cache.BasicCachedArray;
import org.eclipse.birt.data.engine.core.DataException;
import org.eclipse.birt.data.engine.expression.ExprEvaluateUtil;
import org.eclipse.birt.data.engine.i18n.ResourceConstants;
import org.mozilla.javascript.Scriptable;

/**
 * The implementation of this class is used to evaluate TopN/BottomN expressions 
 * @author lzhu
 *
 */
public abstract class NEvaluator
{
	private BasicCachedArray valueList; 
	private BasicCachedArray rowIdList;
	private int firstPassRowNumberCounter = 0;
	private int secondPassRowNumberCounter = 0;
	private int qualifiedRowCounter = 0;
	
	//The "N" of topN/bottomN.
	private int N = -1;
	
	// whether we are doing N percent
	private boolean n_percent = false;
	
	// expression for operand (to be compared)
	private IScriptExpression op_expr;
	// expression for N 
	private IScriptExpression n_expr;
	
	private FilterPassController filterPassController;
	
	private String tempDir;

	/**
	 * Create a new instance to evaluate the top/bottom expression
	 * @param operator 
	 * @param op_expr operand expression
	 * @param n_expr expression to yield N 
	 * @return
	 */
	public static NEvaluator newInstance( String tempDir, int operator, IScriptExpression op_expr, 
			IScriptExpression n_expr, FilterPassController filterPassController  )
	 	throws DataException
	{
		NEvaluator instance = null;
		switch ( operator )
		{
			case IConditionalExpression.OP_TOP_N :
				instance = new TopNEvaluator();
				instance.n_percent = false;
				break;
			case IConditionalExpression.OP_TOP_PERCENT:
				instance = new TopNEvaluator( );
				instance.n_percent = true;
				break;
			case IConditionalExpression.OP_BOTTOM_N:
				instance = new BottomNEvaluator( );
				instance.n_percent = false;
				break;
			case IConditionalExpression.OP_BOTTOM_PERCENT:
				instance = new BottomNEvaluator( );
				instance.n_percent = true;
				break;
			default:
				assert false;		// shouldn't get here
				return null;
		}
		
		instance.op_expr = op_expr;
		instance.n_expr = n_expr;
		instance.filterPassController = filterPassController;
		instance.tempDir = tempDir;
		return instance;
	}
	
	
	/**
	 * Evaluate the given value
	 * @param value
	 * @param n
	 * @return
	 * @throws DataException
	 */
	public boolean evaluate( ScriptContext cx, Scriptable scope ) throws DataException
	{
		if( filterPassController.getForceReset() )
		{
			doReset();
			filterPassController.setForceReset( false );
		}
		
		if ( N == -1 )
		{
			// Create a new evaluator
			// Evaluate N (which is operand1) at this time
			Object n_object = null;
			try
			{
				n_object = ExprEvaluateUtil.evaluateRawExpression2( n_expr, scope, cx );
			}
			catch ( BirtException e1 )
			{
				throw DataException.wrap( e1 );
			}

			double n_value = -1;
			try
			{
				n_value = DataTypeUtil.toDouble( n_object ).doubleValue();
			}
			catch ( BirtException e )
			{
				// conversion error
				throw new DataException(ResourceConstants.INVALID_TOP_BOTTOM_ARGUMENT, e);
			}
			
			// First time; calculate N based on updated row count
			if( n_percent )
			{
				if( n_value < 0 || n_value > 100)
					throw new DataException(ResourceConstants.INVALID_TOP_BOTTOM_PERCENT_ARGUMENT);
				N = (int)Math.round( n_value / 100 * filterPassController.getRowCount() );
			}
			else
			{
				if( n_value < 0 )
					throw new DataException(ResourceConstants.INVALID_TOP_BOTTOM_N_ARGUMENT);
				N = (int)n_value;
			}
			
		}
		
		// Evaluate operand expression
		Object value = ScriptEvalUtil.evalExpr( op_expr, cx, scope, ScriptExpression.defaultID, 0 );
		
		if ( filterPassController.getPassLevel( ) == FilterPassController.FIRST_PASS )
		{
			return doFirstPass( value );
		}
		else if ( filterPassController.getPassLevel( ) == FilterPassController.SECOND_PASS )
		{
			return doSecondPass( );
		}
		return false;
	}

	/**
	 * Do the first pass. In the first pass we maintain a value list and a row id list that will
	 * host all top/bottom N values/rowIds so that in pass 2 we can use them to filter rows out.
	 * @param value
	 * @return
	 * @throws DataException
	 */
	private boolean doFirstPass( Object value ) throws DataException
	{
		firstPassRowNumberCounter++;
		if ( valueList == null )
		{
			valueList = new BasicCachedArray( tempDir, N );
			rowIdList = new BasicCachedArray( tempDir, N );
			
		}
		if ( value != null )
		{
			populateValueListAndRowIdList( value, N );
		}
		return true;
	}

	/**
	 * @param value
	 * @param N
	 * @throws DataException
	 */
	private void populateValueListAndRowIdList( Object value, int N ) throws DataException
	{
		assert N>=0;
		int activeCount = N < this.firstPassRowNumberCounter? N:this.firstPassRowNumberCounter;
		for( int i = 0; i < activeCount; i++ )
		{
			if( valueList.get( i ) == null )
			{
				valueList.set( i, value);
				rowIdList.set( i, new Integer( firstPassRowNumberCounter ) );
				break;
			}
			else 
			{
				Object result = this.doCompare( value, valueList.get( i ) );
				
				try
				{
					// filter in
					if ( DataTypeUtil.toBoolean( result ).booleanValue( ) == true )
					{
						for( int j = activeCount - 1; j > i; j--)
						{
							valueList.set( j, valueList.get( j - 1 ) );
							rowIdList.set( j, rowIdList.get( j - 1 ) );
						}
						valueList.set( i, value );
						rowIdList.set( i,
								new Integer( firstPassRowNumberCounter ) );
						break;
					}
				}
				catch ( BirtException e )
				{
					throw DataException.wrap(e);
				}
			}
		}
	}
	
	/**
	 * Do the second pass
	 * @param N
	 * @return
	 */
	private boolean doSecondPass( )
	{
		secondPassRowNumberCounter++;
		if( secondPassRowNumberCounter > this.filterPassController.getSecondPassRowCount() )
			this.filterPassController.setSecondPassRowCount( secondPassRowNumberCounter );
		else
			this.secondPassRowNumberCounter = this.filterPassController.getSecondPassRowCount();
		
		if ( qualifiedRowCounter < N )
		{
			for ( int i = 0; i < N; i++ )
			{
				int rowId = 0;
				if( rowIdList.get( i )!=null)
				{
					rowId = ((Integer)rowIdList.get( i )).intValue( );
				}
				if ( rowId == secondPassRowNumberCounter )
				{
					qualifiedRowCounter++;
					reset( );
					return true;

				}
			}
			return false;
		}
		else
		{
			reset( );
			return false;
		}
	}

	/**
	 * Reset all the member data to their default value.
	 */
	private void reset( )
	{
		if ( firstPassRowNumberCounter  == secondPassRowNumberCounter )
		{
			doReset();
		}
	}

	/**
	 * 
	 *
	 */
	private void doReset()
	{
		firstPassRowNumberCounter = 0;
		secondPassRowNumberCounter = 0;
		qualifiedRowCounter = 0;
		rowIdList = null;
		valueList = null;
		N = -1;
	}
	protected abstract Object doCompare( Object value1, Object value2 ) throws DataException;
}

/**
 * The class that provides "Top N" calculation service
 *
 */
class TopNEvaluator extends NEvaluator
{
	protected Object doCompare( Object value1, Object value2 ) throws DataException
	{
		return ScriptEvalUtil.evalConditionalExpr( value1, IConditionalExpression.OP_GT, value2, null);
	}
}

/**
 * The class that provides "Bottom N" calculation service
 *
 */
class BottomNEvaluator extends NEvaluator
{
	protected Object doCompare( Object value1, Object value2 ) throws DataException
	{
		return ScriptEvalUtil.evalConditionalExpr( value1, IConditionalExpression.OP_LT, value2, null);
	}
}
