/*******************************************************************************
 * Copyright (c) 2004 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.report.designer.internal.ui.dialogs.js;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.birt.report.designer.internal.ui.dialogs.js.JSObjectMetaData.JSField;
import org.eclipse.birt.report.designer.internal.ui.dialogs.js.JSObjectMetaData.JSMethod;
import org.eclipse.birt.report.model.api.DesignEngine;
import org.eclipse.birt.report.model.api.metadata.IArgumentInfo;
import org.eclipse.birt.report.model.api.metadata.IArgumentInfoList;
import org.eclipse.birt.report.model.api.metadata.IClassInfo;
import org.eclipse.birt.report.model.api.metadata.IMemberInfo;
import org.eclipse.birt.report.model.api.metadata.IMethodInfo;

/**
 * A JSSyntaxContext represents a variables container. JSSyntaxContext also
 * provides methods to access avaible Type meta-data.
 */

public class JSSyntaxContext
{

	/**
	 * BIRT engine objects defined in DesignEngine.
	 */
	private static Map engineObjectMap = new HashMap( );

	// TODO use a smart cache.
	private static Map classMap = new HashMap( );

	/**
	 * Context variables map.
	 */
	private Map objectMetaMap = new HashMap( );

	private static JSMethod[] noMthods = new JSMethod[0];
	private static JSField[] noFields = new JSField[0];

	static
	{
		List engineClassesList = DesignEngine.getMetaDataDictionary( )
				.getClasses( );
		for ( Iterator iter = engineClassesList.iterator( ); iter.hasNext( ); )
		{
			IClassInfo element = (IClassInfo) iter.next( );
			engineObjectMap.put( element.getName( ),
					new EnginClassJSObject( element ) );
		}
	}

	// static methods

	public static JSObjectMetaData getEnginJSObject( String classType )
	{
		return engineObjectMap.containsKey( classType ) ? (JSObjectMetaData) engineObjectMap.get( classType )
				: null;
	}

	public static JSObjectMetaData[] getAllEnginJSObjects( )
	{
		return (JSObjectMetaData[]) engineObjectMap.values( )
				.toArray( new JSObjectMetaData[engineObjectMap.values( ).size( )] );
	}

	public static JSObjectMetaData getJavaClassMeta( String className )
			throws ClassNotFoundException
	{
		JSObjectMetaData meta = null;
		if ( !classMap.containsKey( className ) )
		{
			meta = new JavaClassJSObject( className );
			classMap.put( className, meta );
		}
		else
		{
			meta = (JSObjectMetaData) classMap.get( className );
		}
		return meta;
	}

	// public methods

	public void setVariable( String name, String className )
	{
		try
		{
			objectMetaMap.put( name, getJavaClassMeta( className ) );
		}
		catch ( Exception e )
		{
			if ( getEnginJSObject( className ) != null )
			{
				objectMetaMap.put( name, getEnginJSObject( className ) );
			}
			else
			{
				removeVariable( name );
			}
		}
	}

	public void setVariable( String name, Class clazz )
			throws ClassNotFoundException
	{
		objectMetaMap.put( name, new JavaClassJSObject( clazz.getName( ) ) );
	}

	public void removeVariable( String name )
	{
		objectMetaMap.remove( name );
	}

	public void clear( )
	{
		objectMetaMap.clear( );
	}

	public JSObjectMetaData getVariableMeta( String variableName )
	{
		if ( objectMetaMap.containsKey( variableName ) )
			return (JSObjectMetaData) objectMetaMap.get( variableName );
		else
			return getEnginJSObject( variableName );
	}

	// inner class

	private static class JavaClassJSObject implements JSObjectMetaData
	{

		private Class clazz;

		JavaClassJSObject( Class clazz )
		{
			this.clazz = clazz;
		}

		public JavaClassJSObject( String className )
				throws ClassNotFoundException
		{
			this.clazz = Class.forName( className );
			if ( clazz.isArray( ) )
				this.clazz = clazz.getComponentType( );
		}

		public String getName( )
		{
			return this.clazz.getName( );
		}

		public JSMethod[] getMethods( )
		{
			List jsMehods = new ArrayList( );
			// if ( this.clazz.isInterface( ) )
			// {
			// jsMehods.addAll( getMethods( Object.class.getMethods( ) ) );
			// }

			jsMehods.addAll( getMethods( this.clazz.getMethods( ) ) );

			Collections.sort( jsMehods );

			return (JSMethod[]) jsMehods.toArray( new JSMethod[jsMehods.size( )] );
		}

		private List getMethods( Method[] methods )
		{
			List jsMehods = new ArrayList( );
			List setMethodList = new ArrayList( );
			List getMethodList = new ArrayList( );
			for ( int i = 0; i < methods.length; i++ )
			{
				if ( methods[i].getName( ).startsWith( "set" ) //$NON-NLS-1$
						&& methods[i].getParameterTypes( ) != null
						&& methods[i].getParameterTypes( ).length == 1 )
				{
					setMethodList.add( methods[i].getName( ).substring( 3 ) );
					continue;
				}

				if ( methods[i].getName( ).startsWith( "get" ) //$NON-NLS-1$
						&& methods[i].getParameterTypes( ) != null
						&& methods[i].getParameterTypes( ).length == 0 )
				{
					getMethodList.add( methods[i].getName( ).substring( 3 ) );
					continue;
				}
				jsMehods.add( new JavaClassMethod( methods[i] ) );
			}

			for ( int i = 0; i < methods.length; i++ )
			{
				if ( methods[i].getName( ).startsWith( "set" ) //$NON-NLS-1$
						&& methods[i].getParameterTypes( ) != null
						&& methods[i].getParameterTypes( ).length == 1 )
				{
					if ( !getMethodList.contains( methods[i].getName( )
							.substring( 3 ) ) )
					{
						jsMehods.add( new JavaClassMethod( methods[i] ) );
					}
					continue;
				}

				if ( methods[i].getName( ).startsWith( "get" ) //$NON-NLS-1$
						&& methods[i].getParameterTypes( ) != null
						&& methods[i].getParameterTypes( ).length == 0 )
				{
					if ( !setMethodList.contains( methods[i].getName( )
							.substring( 3 ) ) )
					{
						jsMehods.add( new JavaClassMethod( methods[i] ) );
					}
					continue;
				}
			}
			return jsMehods;
		}

		public JSField[] getFields( )
		{
			List jsFields = new ArrayList( );
			jsFields.addAll( getFields( this.clazz ) );
			Collections.sort( jsFields );
			return (JSField[]) jsFields.toArray( new JSField[jsFields.size( )] );
		}

		private List getFields( Class clazz )
		{
			Method[] methods = clazz.getMethods( );
			List jsFields = new ArrayList( );
			List setMethodList = new ArrayList( );

			String methodName;
			for ( int i = 0; i < methods.length; i++ )
			{
				methodName = methods[i].getName( );
				if ( methodName.startsWith( "set" ) //$NON-NLS-1$
						&& methods[i].getParameterTypes( ) != null
						&& methods[i].getParameterTypes( ).length == 1 )
				{
					setMethodList.add( methodName.substring( 3 ) );
					continue;
				}
			}
			for ( int i = 0; i < methods.length; i++ )
			{
				methodName = methods[i].getName( );

				if ( methods[i].getName( ).startsWith( "get" ) //$NON-NLS-1$
						&& methods[i].getParameterTypes( ) != null
						&& methods[i].getParameterTypes( ).length == 0 )
				{
					if ( setMethodList.contains( methodName.substring( 3 ) ) )
					{
						Class type = methods[i].getReturnType( );
						JSField field = new JavaClassField( methods[i].getDeclaringClass( ),
								getFieldName( methodName ),
								type.getName( ),
								type.isArray( ) );
						if ( !jsFields.contains( field ) )
							jsFields.add( field );
					}
				}
			}

			Field[] fields = clazz.getFields( );
			for ( int i = 0; i < fields.length; i++ )
			{
				jsFields.add( new JavaClassField( fields[i] ) );
			}
			return jsFields;
		}

		private String getFieldName( String methodName )
		{
			if ( methodName.length( ) == 3 )
			{
				return ""; //$NON-NLS-1$
			}
			else if ( methodName.length( ) == 4 )
			{
				return methodName.substring( 3, 4 ).toLowerCase( );
			}
			else
			{
				return methodName.substring( 3, 4 ).toLowerCase( )
						+ methodName.substring( 4 );
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see java.lang.Object#toString()
		 */
		public String toString( )
		{
			return clazz.getName( );
		}

		public String getDescription( )
		{
			return null;
		}

		public int getVisibility( )
		{
			return VISIBILITY_PUBLIC;
		}

		private class JavaClassMethod implements JSMethod, Comparable
		{

			private Method method;
			private String displayText;

			public JavaClassMethod( Method method )
			{
				this.method = method;
			}

			public String getName( )
			{
				return method.getName( );
			}

			public JSObjectMetaData getReturn( )
			{
				try
				{
					return getJavaClassMeta( method.getReturnType( ).getName( ) );
				}
				catch ( ClassNotFoundException e )
				{
					return null;
				}
			}

			public String getDisplayText( )
			{
				if ( displayText == null )
				{
					StringBuffer strbuf = new StringBuffer( getName( ) );
					strbuf.append( "(" ); //$NON-NLS-1$
					Class[] parameters = method.getParameterTypes( );
					for ( int i = 0; i < parameters.length; i++ )
					{
						if ( i > 0 )
						{
							strbuf.append( ", " ); //$NON-NLS-1$
						}
						strbuf.append( getSimpleName( parameters[i] ) )
								.append( " " ) //$NON-NLS-1$
								.append( "arg" ) //$NON-NLS-1$
								.append( i + 1 );
					}
					strbuf.append( ") " ); //$NON-NLS-1$
					strbuf.append( getSimpleName( method.getReturnType( ) ) );
					strbuf.append( " - " ); //$NON-NLS-1$
					strbuf.append( getSimpleName( method.getDeclaringClass( ) ) );
					displayText = strbuf.toString( );
					strbuf = null;
				}
				return displayText;
			}

			public String getDescription( )
			{
				return null;
			}

			public int getVisibility( )
			{
				if ( ( method.getModifiers( ) & Modifier.STATIC ) != 0 )
				{
					return VISIBILITY_STATIC;
				}
				else if ( ( method.getModifiers( ) & Modifier.PUBLIC ) != 0 )
				{
					return VISIBILITY_PUBLIC;
				}
				else if ( ( method.getModifiers( ) & Modifier.PRIVATE ) != 0 )
				{
					return VISIBILITY_PRIVATE;

				}
				else if ( ( method.getModifiers( ) & Modifier.PROTECTED ) != 0 )
				{
					return VISIBILITY_PROTECTED;
				}
				else
				{
					return VISIBILITY_PUBLIC;
				}
			}

			public int compareTo( Object obj )
			{
				if ( obj instanceof JavaClassMethod
						&& ( (JavaClassMethod) obj ).getName( ) != null )
				{
					if ( getVisibility( ) == VISIBILITY_STATIC )
					{
						if ( ( (JavaClassMethod) obj ).getVisibility( ) == VISIBILITY_PUBLIC )
							return 1;
					}
					return getName( ).compareToIgnoreCase( ( (JavaClassMethod) obj ).getName( ) );
				}
				return 0;
			}

		}

		private class JavaClassField implements JSField, Comparable
		{

			private String name;
			private JSObjectMetaData type;
			private Field field;
			private Class declareClazz;
			private String displayText;
			private boolean isArray;

			public JavaClassField( Field field )
			{
				this.field = field;
				this.name = field.getName( );
				try
				{
					this.type = getJavaClassMeta( field.getType( ).getName( ) );
				}
				catch ( ClassNotFoundException e )
				{
				}
			}

			public JavaClassField( Class declareClazz, String name,
					String type, boolean isArray )
			{
				this.declareClazz = declareClazz;
				this.name = name;
				try
				{
					this.type = getJavaClassMeta( type );
				}
				catch ( ClassNotFoundException e )
				{
				}
				this.isArray = isArray;
			}

			public String getName( )
			{
				return this.name;
			}

			public JSObjectMetaData getType( )
			{
				return this.type;
			}

			/*
			 * (non-Javadoc)
			 * 
			 * @see java.lang.Object#equals(java.lang.Object)
			 */
			public boolean equals( Object obj )
			{
				if ( obj == null
						|| !( obj instanceof JavaClassField )
						|| ( (JavaClassField) obj ).getName( ) == null )
				{
					return false;
				}

				return ( (JavaClassField) obj ).getName( ).equals( this.name );
			}

			public String getDisplayText( )
			{
				if ( displayText == null )
				{
					StringBuffer strbuf = new StringBuffer( getName( ) );
					strbuf.append( " " ); //$NON-NLS-1$
					if ( field != null && field.getType( ) != null )
					{
						strbuf.append( getSimpleName( field.getType( ) ) );
						strbuf.append( " - " ); //$NON-NLS-1$
					}
					else if ( type != null )
					{
						strbuf.append( getSimpleName( this.type.getName( ) ) );
						if ( isArray )
						{
							strbuf.append( "[]" ); //$NON-NLS-1$
						}
						strbuf.append( " - " ); //$NON-NLS-1$
					}
					if ( field == null )
					{
						strbuf.append( getSimpleName( declareClazz ) );
					}
					else
					{
						strbuf.append( getSimpleName( field.getDeclaringClass( ) ) );
					}
					displayText = strbuf.toString( );
					strbuf = null;
				}
				return displayText;
			}

			public String getDescription( )
			{
				return null;
			}

			public int getVisibility( )
			{
				if ( field == null )
					return VISIBILITY_PUBLIC;

				if ( ( field.getModifiers( ) & Modifier.STATIC ) != 0 )
				{
					return VISIBILITY_STATIC;
				}
				else if ( ( field.getModifiers( ) & Modifier.PUBLIC ) != 0 )
				{
					return VISIBILITY_PUBLIC;
				}
				else if ( ( field.getModifiers( ) & Modifier.PRIVATE ) != 0 )
				{
					return VISIBILITY_PRIVATE;

				}
				else if ( ( field.getModifiers( ) & Modifier.PROTECTED ) != 0 )
				{
					return VISIBILITY_PROTECTED;
				}
				else
				{
					return VISIBILITY_PUBLIC;
				}
			}

			public int compareTo( Object obj )
			{
				if ( obj instanceof JavaClassField
						&& ( (JavaClassField) obj ).getName( ) != null )
				{
					if ( getVisibility( ) == VISIBILITY_STATIC )
					{
						if ( ( (JavaClassField) obj ).getVisibility( ) == VISIBILITY_PUBLIC )
							return 1;
					}
					return getName( ).compareToIgnoreCase( ( (JavaClassField) obj ).getName( ) );
				}
				return 0;
			}

		}

		private String getSimpleName( Class clazz )
		{
			String simpleName = null;
			if ( clazz.isArray( ) )
			{
				simpleName = clazz.getComponentType( ).getName( );
				if ( !clazz.getComponentType( ).isPrimitive( ) )
				{
					simpleName = simpleName.substring( simpleName.lastIndexOf( "." ) + 1 ); //$NON-NLS-1$
				}
				simpleName += "[]"; //$NON-NLS-1$
			}
			else
			{
				simpleName = clazz.getName( );
				if ( !clazz.isPrimitive( ) )
				{
					simpleName = simpleName.substring( simpleName.lastIndexOf( "." ) + 1 ); //$NON-NLS-1$
				}
			}
			return simpleName;
		}

		private Object getSimpleName( String name )
		{
			return name.substring( name.lastIndexOf( "." ) + 1 ); //$NON-NLS-1$
		}

	}

	private static class EnginClassJSObject implements JSObjectMetaData
	{

		private IClassInfo classInfo;
		private boolean showPublic;

		public EnginClassJSObject( IClassInfo classInfo )
		{
			this.classInfo = classInfo;
			// if type is DataSet or DataSource
			// show all methods and fields
			showPublic = "DataSet".equals( classInfo.getName( ) ) //$NON-NLS-1$
					|| "DataSource".equals( classInfo.getName( ) ); //$NON-NLS-1$
		}

		public String getName( )
		{
			if ( classInfo == null )
				return ""; //$NON-NLS-1$
			return classInfo.getName( );
		}

		// FIXME cache it.
		public JSMethod[] getMethods( )
		{
			if ( classInfo == null )
				return noMthods;
			List methods = classInfo.getMethods( );
			List all = new ArrayList( );
			for ( Iterator iter = methods.iterator( ); iter.hasNext( ); )
			{
				IMethodInfo method = (IMethodInfo) iter.next( );
				if ( showPublic || method.isStatic( ) )
					all.add( new EngineClassMethod( method ) );
			}
			Collections.sort( all );
			return (JSMethod[]) all.toArray( new JSMethod[all.size( )] );
		}

		public JSField[] getFields( )
		{
			if ( classInfo == null )
				return noFields;
			List members = classInfo.getMembers( );
			List all = new ArrayList( );
			for ( Iterator iter = members.iterator( ); iter.hasNext( ); )
			{
				IMemberInfo member = (IMemberInfo) iter.next( );
				if ( showPublic || member.isStatic( ) )
					all.add( new EngineClassField( member ) );
			}
			Collections.sort( all );
			return (JSField[]) all.toArray( new JSField[all.size( )] );
		}

		public String getDescription( )
		{
			return null;
		}

		public int getVisibility( )
		{
			return 0;
		}

		private class EngineClassMethod implements JSMethod, Comparable
		{

			private IMethodInfo method;
			private String displayText;

			public EngineClassMethod( IMethodInfo method )
			{
				this.method = method;
			}

			public String getName( )
			{
				return method.getDisplayName( );
			}

			public JSObjectMetaData getReturn( )
			{
				JSObjectMetaData meta = JSSyntaxContext.getEnginJSObject( method.getReturnType( ) );
				if ( meta == null )
					try
					{
						return getJavaClassMeta( method.getReturnType( ) );
					}
					catch ( ClassNotFoundException e )
					{
						return null;
					}
				return meta;
			}

			public String getDisplayText( )
			{
				if ( displayText == null )
				{
					StringBuffer strbuf = new StringBuffer( getName( ) );
					strbuf.append( "(" ); //$NON-NLS-1$
					for ( Iterator iter = method.argumentListIterator( ); iter.hasNext( ); )
					{
						IArgumentInfoList element = (IArgumentInfoList) iter.next( );
						int i = 0;
						for ( Iterator iterator = element.argumentsIterator( ); iterator.hasNext( ); )
						{
							if ( i++ > 0 )
							{
								strbuf.append( ", " ); //$NON-NLS-1$
							}
							IArgumentInfo argument = (IArgumentInfo) iterator.next( );
							strbuf.append( argument.getType( ) ).append( " " ) //$NON-NLS-1$
									.append( argument.getName( ) );
						}
						break;
					}
					strbuf.append( ") " ); //$NON-NLS-1$
					strbuf.append( classInfo.getName( ) );
					displayText = strbuf.toString( );
					strbuf = null;
				}
				return displayText;
			}

			public String getDescription( )
			{
				return null;
			}

			public int getVisibility( )
			{
				if ( method.isStatic( ) )
					return VISIBILITY_STATIC;
				return 0;
			}

			public int compareTo( Object obj )
			{
				if ( obj instanceof EngineClassMethod
						&& ( (EngineClassMethod) obj ).getName( ) != null )
				{
					return getName( ).compareToIgnoreCase( ( (EngineClassMethod) obj ).getName( ) );
				}
				return 0;
			}

		}

		private class EngineClassField implements JSField, Comparable
		{

			private IMemberInfo member;
			private String displayText;

			public EngineClassField( IMemberInfo member )
			{
				this.member = member;
			}

			public String getName( )
			{
				return member.getDisplayName( );
			}

			public JSObjectMetaData getType( )
			{
				JSObjectMetaData meta = JSSyntaxContext.getEnginJSObject( member.getDataType( ) );
				if ( meta == null )
					try
					{
						return getJavaClassMeta( member.getDataType( ) );
					}
					catch ( ClassNotFoundException e )
					{
						return null;
					}
				return meta;
			}

			public String getDisplayText( )
			{
				if ( displayText == null )
				{
					StringBuffer strbuf = new StringBuffer( getName( ) );
					strbuf.append( " " ); //$NON-NLS-1$
					strbuf.append( classInfo.getName( ) );
					displayText = strbuf.toString( );
					strbuf = null;
				}
				return displayText;
			}

			public String getDescription( )
			{
				return null;
			}

			public int getVisibility( )
			{
				return 0;
			}

			public int compareTo( Object obj )
			{
				if ( obj instanceof EngineClassField
						&& ( (EngineClassField) obj ).getName( ) != null )
				{
					return getName( ).compareToIgnoreCase( ( (EngineClassField) obj ).getName( ) );
				}
				return 0;
			}
		}
	}
}
