/*******************************************************************************
 * Copyright (c) 2004, 2005 Actuate Corporation.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *  Actuate Corporation  - initial API and implementation
 *******************************************************************************/

package org.eclipse.birt.core.script.bre;

import java.util.Date;

import org.eclipse.birt.core.data.DataTypeUtil;
import org.eclipse.birt.core.exception.BirtException;
import org.mozilla.javascript.BaseFunction;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;

import com.ibm.icu.text.Collator;

/**
 * This class implements comparison methods.
 */
public class BirtComp extends ScriptableObject
{
	/**
	 * 
	 */
	private static final long serialVersionUID = 29055295217417372L;

	/**
	 * 
	 *
	 */
	public BirtComp()
	{
		this.defineProperty( "anyOf", new Function_AnyOf(),0 );
		this.defineProperty( "between", new Function_Between( true ),0 );
		this.defineProperty( "notBetween", new Function_Between( false ), 0 );
		this.defineProperty( "equalTo", new Function_Compare(Function_Compare.MODE_EQUAL),0 );
		this.defineProperty( "greaterThan", new Function_Compare(Function_Compare.MODE_GREATERTHAN),0 );
		this.defineProperty( "lessThan", new Function_Compare(Function_Compare.MODE_LESSTHAN),0 );
		this.defineProperty( "greaterOrEqual", new Function_Compare(Function_Compare.MODE_GREATEROREQUAL),0 );
		this.defineProperty( "lessOrEqual", new Function_Compare(Function_Compare.MODE_LESSOREQUAL),0 );
		this.defineProperty( "notEqual", new Function_Compare(Function_Compare.MODE_NOT_EQUAL),0 );
		this.defineProperty( "like", new Function_Compare(Function_Compare.MODE_LIKE),0 );
		this.defineProperty( "notLike", new Function_Compare(Function_Compare.MODE_NOT_LIKE),0 );
		this.defineProperty( "match", new Function_Compare(Function_Compare.MODE_MATCH),0 );
	}
	
	private static Collator myCollator = Collator.getInstance( );

	/**
	 * @param obj1
	 * @param obj2
	 * @return -1,0 and 1 standing for <,= and > respectively
	 * @throws BirtException 
	 * @throws DataException
	 */
	private static int compare( Object obj1, Object obj2 ) throws BirtException
	{
		if ( obj1 == null || obj2 == null )
		{
			// all non-null values are greater than null value
			if ( obj1 == null && obj2 != null )
				return -1;
			else if ( obj1 != null && obj2 == null )
				return 1;
			else
				return 0;
		}

		if ( isSameType( obj1, obj2 ) )
		{
			if ( obj1 instanceof Boolean )
			{
				if ( obj1.equals( obj2 ) )
					return 0;

				Boolean bool = (Boolean) obj1;
				if ( bool.equals( Boolean.TRUE ) )
					return 1;
				else
					return -1;
			}
			else if ( obj1 instanceof Comparable )
			{
				if ( obj1 instanceof String )
				{
					return myCollator.compare( obj1, obj2 );
				}
				else
				{
					return ( (Comparable) obj1 ).compareTo( obj2 );
				}
			}
			// most judgements should end here
			else
			{
				return myCollator.compare( obj1.toString( ), obj2.toString( ) );
			}
		}
		else if ( isNumericOrString( obj1 ) && isNumericOrString( obj2 ) )
		{
			return DataTypeUtil.toDouble( obj1 )
					.compareTo( DataTypeUtil.toDouble( obj2 ) );
		}
		else if ( isDateOrString( obj1 ) && isDateOrString( obj2 ) )
		{
			return DataTypeUtil.toDate( obj1 )
					.compareTo( DataTypeUtil.toDate( obj2 ) );
		}
		else
			throw new IllegalArgumentException( );

	}
	
	/**
	 * @param obj1
	 * @param obj2
	 * @return true x matches Javascript pattern y
	 * @throws BirtException
	 * @throws DataException
	 */
	private static boolean match( Object obj1, Object obj2 )
			throws BirtException
	{
		if ( obj2 == null )
		{
			throw new java.lang.IllegalArgumentException( "pattern value can not be null!" );
		}
		if ( obj1 == null )
		{
			return false;
		}
		String str = obj1.toString( );
		String pattern = obj2.toString( );
		return str.matches( pattern );
	}

	/**
	 * @param obj1
	 * @param obj2
	 * @return true x matches SQL pattern y
	 * @throws BirtException
	 * @throws DataException
	 */
	private static boolean like( Object obj1, Object obj2 )
	{
		if ( obj2 == null )
		{
			throw new java.lang.IllegalArgumentException( "pattern value can not be null!" );
		}
		if ( obj1 == null )
		{
			return false;
		}
		String str = obj1.toString( );
		String pattern = obj2.toString( );
		pattern = pattern.replaceAll( "%", ".*" );
		pattern = pattern.replaceAll( "_", "." );
		return str.matches( pattern );
	}

	private class Function_AnyOf extends BaseFunction implements Function
	{
		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;

		public Object call(Context cx, Scriptable scope, Scriptable thisObj, java.lang.Object[] args)
		{
			if( args == null || args.length < 2 )
				throw new IllegalArgumentException( "The number of arguement is incorrect.");
			
			args = BreUtility.convertToJavaObjects( args );
			
			for( int i = 1; i < args.length; i++ )
			{
				try
				{
					if( compare( args[0], args[i]) == 0 )
						return Boolean.TRUE;
				}
				catch ( Exception e )
				{
					//If two values cannot be compare, simply do nothing.
				}
			}
			return Boolean.FALSE;
		}
	}
	
	private class Function_Between extends BaseFunction implements Function
	{
		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;
		private boolean mode;
		
		/**
		 * @param mode: if true, use Between mode, else use NotBetween mode.
		 */
		Function_Between( boolean mode )
		{
			this.mode = mode;
		}
		
		/*
		 * (non-Javadoc)
		 * @see org.mozilla.javascript.BaseFunction#call(org.mozilla.javascript.Context, org.mozilla.javascript.Scriptable, org.mozilla.javascript.Scriptable, java.lang.Object[])
		 */
		public Object call(Context cx, Scriptable scope, Scriptable thisObj, java.lang.Object[] args)
		{
			if( args == null || args.length != 3 )
				throw new IllegalArgumentException( "The number of arguement is incorrect.");
			
			args = BreUtility.convertToJavaObjects( args );
			
			try
			{
				Object min, max;
				if ( compare( args[1], args[2] ) <= 0 )
				{
					min = args[1];
					max = args[2];
				}
				else
				{
					min = args[2];
					max = args[1];
				}
				return this.mode
						? new Boolean( compare( args[0], min ) >= 0
								&& compare( args[0], max ) <= 0 )
						: new Boolean( !( compare( args[0], min ) >= 0 && compare( args[0],
								max ) <= 0 ) );
			}
			catch ( BirtException e )
			{
				throw new IllegalArgumentException( e.getLocalizedMessage( ) );
			}
		}
	}
	
	private class Function_Compare extends BaseFunction implements Function
	{
		/**
		 * 
		 */
		public static final int MODE_EQUAL = 0;
		public static final int MODE_NOT_EQUAL = 1;
		public static final int MODE_GREATERTHAN = 2;
		public static final int MODE_LESSTHAN = 3;
		public static final int MODE_GREATEROREQUAL = 4;
		public static final int MODE_LESSOREQUAL = 5;
		public static final int MODE_LIKE = 6;
		public static final int MODE_MATCH = 7;
		public static final int MODE_NOT_LIKE = 8;
		
		private static final long serialVersionUID = 1L;
		private int mode;
		
		Function_Compare( int mode )
		{
			this.mode = mode;
		}
		
		public Object call(Context cx, Scriptable scope, Scriptable thisObj, java.lang.Object[] args)
		{
			if( args == null || args.length != 2 )
				throw new IllegalArgumentException( "The number of arguement is incorrect.");
			
			args = BreUtility.convertToJavaObjects( args );
			
			try
			{
				switch (this.mode) 
				{
					case MODE_EQUAL :
						return new Boolean( compare( args[0], args[1] ) == 0 );
					case MODE_NOT_EQUAL :
						return new Boolean( compare( args[0], args[1] ) != 0 );
					case MODE_GREATERTHAN :
						return new Boolean( compare( args[0], args[1] ) > 0 );
					case MODE_LESSTHAN:
						return new Boolean( compare( args[0], args[1] ) < 0 );
					case MODE_GREATEROREQUAL:
						return new Boolean( compare( args[0], args[1] ) >= 0);
					case MODE_LESSOREQUAL:
						return new Boolean( compare( args[0], args[1] ) <= 0);
					case MODE_LIKE :
						return new Boolean( like( args[0], args[1] ) );
					case MODE_MATCH :
						return new Boolean( match( args[0], args[1] ) );
					case MODE_NOT_LIKE :
						return new Boolean( !like( args[0], args[1] ) );
					default:
						return null;
				}
				
			}
			catch ( BirtException e )
			{
				throw new IllegalArgumentException( e.getLocalizedMessage( ) );
			}
		}
	}
	

	/**
	 * 
	 * @param resultExpr
	 * @param resultOp1
	 * @return
	 */
	private static boolean isSameType( Object resultExpr, Object resultOp1 )
	{
		return resultExpr.getClass( ).equals( resultOp1.getClass( ) );
	}

	/**
	 * 
	 * @param result
	 * @return
	 */
	private static boolean isNumericOrString( Object result )
	{
		return ( result instanceof Number ) || ( result instanceof String );
	}

	/**
	 * 
	 * @param result
	 * @return
	 */
	private static boolean isDateOrString( Object result )
	{
		return ( result instanceof Date ) || ( result instanceof String );
	}

	/*
	 * (non-Javadoc)
	 * @see org.mozilla.javascript.ScriptableObject#getClassName()
	 */
	public String getClassName( )
	{
		return "BirtComp";
	}

}
