/*******************************************************************************
 * 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.data.ui.dataset;

import java.lang.reflect.InvocationTargetException;
import java.text.Collator;
import java.text.ParseException;
import java.text.RuleBasedCollator;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.eclipse.birt.core.data.ExpressionUtil;
import org.eclipse.birt.core.exception.BirtException;
import org.eclipse.birt.data.engine.api.DataEngine;
import org.eclipse.birt.data.engine.api.IQueryResults;
import org.eclipse.birt.data.engine.api.IResultIterator;
import org.eclipse.birt.data.engine.api.IResultMetaData;
import org.eclipse.birt.data.engine.api.querydefn.BaseQueryDefinition;
import org.eclipse.birt.data.engine.api.querydefn.InputParameterBinding;
import org.eclipse.birt.data.engine.api.querydefn.QueryDefinition;
import org.eclipse.birt.data.engine.api.querydefn.ScriptExpression;
import org.eclipse.birt.report.data.adapter.api.DataRequestSession;
import org.eclipse.birt.report.data.adapter.api.DataSessionContext;
import org.eclipse.birt.report.designer.data.ui.util.DataSetProvider;
import org.eclipse.birt.report.designer.data.ui.util.Utility;
import org.eclipse.birt.report.designer.internal.ui.util.ExceptionHandler;
import org.eclipse.birt.report.designer.nls.Messages;
import org.eclipse.birt.report.designer.ui.ReportPlugin;
import org.eclipse.birt.report.designer.ui.dialogs.properties.AbstractPropertyPage;
import org.eclipse.birt.report.designer.ui.preferences.DateSetPreferencePage;
import org.eclipse.birt.report.model.api.DataSetHandle;
import org.eclipse.birt.report.model.api.DataSetParameterHandle;
import org.eclipse.birt.report.model.api.DesignElementHandle;
import org.eclipse.birt.report.model.api.OdaDataSetParameterHandle;
import org.eclipse.birt.report.model.api.PropertyHandle;
import org.eclipse.birt.report.model.api.activity.NotificationEvent;
import org.eclipse.birt.report.model.api.core.Listener;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Preferences;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.dialogs.IMessageProvider;
import org.eclipse.jface.dialogs.ProgressMonitorDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ColumnPixelData;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.ITableLabelProvider;
import org.eclipse.jface.viewers.TableLayout;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerSorter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.progress.UIJob;
/**
 * Property page to preview the resultset.
 *  
 */

public class ResultSetPreviewPage extends AbstractPropertyPage
		implements
			Listener
{
	private TableViewer resultSetTableViewer = null;
	private transient Table resultSetTable = null;
	private boolean modelChanged = true;
	private boolean needsUpdateUI = true;
	private int columnCount = -1;
	private List recordList = null;
	private List errorList = null;
	private String[] columnBindingNames;
	private int previousMaxRow = -1;
	/**
	 * The constructor.
	 */
	public ResultSetPreviewPage( )
	{
		super( );
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.birt.report.designer.ui.IPropertyPage#createPageControl(org.eclipse.swt.widgets.Composite)
	 */
	public Control createPageControl( Composite parent )
	{
		resultSetTable = new Table( parent, SWT.FULL_SELECTION | SWT.MULTI );
		resultSetTable.setHeaderVisible( true );
		resultSetTable.setLinesVisible( true );
		( (DataSetHandle) getContainer( ).getModel( ) ).addListener( this );
		
		resultSetTable.addMouseListener( new MouseAdapter( ) {

			public void mouseUp( MouseEvent e )
			{
				// if not mouse left button
				if ( e.button != 1 )
				{
					MenuManager menuManager = new MenuManager( );
				
					ResultSetTableAction copyAction = ResultSetTableActionFactory.createResultSetTableAction( resultSetTable,
							ResultSetTableActionFactory.COPY_ACTION );
					ResultSetTableAction selectAllAction = ResultSetTableActionFactory.createResultSetTableAction( resultSetTable,
							ResultSetTableActionFactory.SELECTALL_ACTION );
					menuManager.add( copyAction );
					menuManager.add( selectAllAction );

					menuManager.update( );

					copyAction.update( );
					selectAllAction.update( );

					Menu contextMenu = menuManager.createContextMenu( resultSetTable );

					contextMenu.setEnabled( true );
					contextMenu.setVisible( true );
				}
			}
		} );
		
		createResultSetTableViewer( );
		
		return resultSetTable;
	}
	
	private void createResultSetTableViewer( )
	{
		resultSetTableViewer = new TableViewer( resultSetTable );
		resultSetTableViewer.setSorter( null );
		resultSetTableViewer.setContentProvider( new IStructuredContentProvider( ) {

			public Object[] getElements( Object inputElement )
			{
				if ( inputElement instanceof List )
				{
					return ( (List) inputElement ).toArray( );
				}

				return new Object[0];
			}

			public void inputChanged( Viewer viewer, Object oldInput,
					Object newInput )
			{
			}

			public void dispose( )
			{
			}
		} );
		resultSetTableViewer.setLabelProvider( new ITableLabelProvider( ) {

			public Image getColumnImage( Object element, int columnIndex )
			{
				return null;
			}

			public String getColumnText( Object element, int columnIndex )
			{
				return ( (String[]) element )[columnIndex];
			}

			public void addListener( ILabelProviderListener listener )
			{
			}

			public void dispose( )
			{
			}

			public boolean isLabelProperty( Object element, String property )
			{
				return false;
			}

			public void removeListener( ILabelProviderListener listener )
			{
			}
		} );
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.birt.report.designer.ui.IPropertyPage#pageActivated()
	 */
	public void pageActivated( )
	{
		getContainer( ).setMessage( Messages.getString( "dataset.editor.preview" ),//$NON-NLS-1$
				IMessageProvider.NONE );

		if ( modelChanged )
		{
			modelChanged = false;

			new UIJob( "" ) {

				public IStatus runInUIThread( IProgressMonitor monitor )
				{
					runUpdateResults( );
					return Status.OK_STATUS;
				}
			}.schedule( );
		}
	}

	private void runUpdateResults( )
	{
		if ( resultSetTable != null && !resultSetTable.isDisposed( ) )
		{

			// Set thread context class loader so Rhino can find POJOs in workspace
			// projects
			ClassLoader oldContextLoader = Thread.currentThread( )
					.getContextClassLoader( );
			ClassLoader parentLoader = oldContextLoader;
			if ( parentLoader == null )
				parentLoader = this.getClass( ).getClassLoader( );
			ClassLoader newContextLoader = DataSetProvider.getCustomScriptClassLoader( parentLoader );
			Thread.currentThread( ).setContextClassLoader( newContextLoader );
			
			clearResultSetTable( );
			DataSessionContext context;
			try
			{
				context = new DataSessionContext( DataSessionContext.MODE_DIRECT_PRESENTATION,
						( (DataSetHandle) getContainer( ).getModel( ) ).getModuleHandle( ) );
				DataRequestSession session = DataRequestSession.newSession( context );
				
				Map appContext = new HashMap();
				appContext.put( DataEngine.MEMORY_DATA_SET_CACHE,
						new Integer( this.getMaxRowPreference( ) ) );
	
				context.setAppContext( appContext );
				
				updateResultsProcess( executeProcess( session ) );
				session.shutdown( );
			}
			catch ( BirtException e )
			{
			     ExceptionHandler.handle( e );
			}
			updateResultSetTableUI( );
			
			// Restore old thread context class loader
			Thread.currentThread( ).setContextClassLoader( oldContextLoader );
		}
	}
	
	protected final void clearResultSetTable( )
	{
		//Clear the columns
		TableColumn[] columns = resultSetTable.getColumns( );
		for ( int n = 0; n < columns.length; n++ )
		{
			columns[n].dispose( );
		}
		//clear everything else
		resultSetTable.removeAll( );
	}
	
	/**
	 * Get resultSet
	 * 
	 * @return
	 */
	private IQueryResults executeProcess( DataRequestSession session )
	{
		errorList = new ArrayList( );

		try
		{
			IQueryResults resultSet = DataSetProvider.getCurrentInstance( )
					.execute( ( (DataSetEditor) getContainer( ) ).getHandle( ),
							session );

			if ( resultSet != null )
			{
				IResultMetaData metaData = resultSet.getResultMetaData( );
				columnCount = metaData.getColumnCount( );

				// Create a new Report Query definition for retrieving the
				// columns
				QueryDefinition query = new QueryDefinition( (BaseQueryDefinition) resultSet.getPreparedQuery( )
						.getReportQueryDefn( ) );
				query.setDataSetName( ( (DataSetEditor) getContainer( ) ).getHandle( )
						.getQualifiedName( ) );

				int maxRow = getMaxRowPreference( );
				query.setMaxRows( maxRow );
				PropertyHandle handle = ( (DataSetEditor) getContainer( ) ).getHandle( )
						.getPropertyHandle( DataSetHandle.PARAMETERS_PROP );

				if ( handle != null )
				{
					Iterator paramIter = handle.iterator( );
					while ( paramIter.hasNext( ) )
					{
						DataSetParameterHandle paramDefn = (DataSetParameterHandle) paramIter.next( );
						if ( paramDefn.isInput( ) )
						{
							String defaultValue = null;
							if ( paramDefn instanceof OdaDataSetParameterHandle
									&& ( (OdaDataSetParameterHandle) paramDefn ).getParamName( ) != null )
								defaultValue = ExpressionUtil.createJSParameterExpression( ( (OdaDataSetParameterHandle) paramDefn ).getParamName( ) );
							else
								defaultValue = paramDefn.getDefaultValue( );
							if ( defaultValue != null )
							{
								InputParameterBinding binding = new InputParameterBinding( paramDefn.getName( ),
										new ScriptExpression( defaultValue ) );
								query.addInputParamBinding( binding );
							}
						}
					}
				}

				columnBindingNames = new String[columnCount];
				ScriptExpression[] expressions = new ScriptExpression[columnCount];

				for ( int n = 0; n < columnCount; n++ )
				{
					columnBindingNames[n] = metaData.getColumnName( n + 1 );
					expressions[n] = new ScriptExpression( ExpressionUtil.createJSDataSetRowExpression( columnBindingNames[n] ) );

					query.addResultSetExpression( columnBindingNames[n],
							expressions[n] );
				}

				boolean clearCache = false;
				if ( maxRow != this.previousMaxRow )
				{
					this.previousMaxRow = maxRow;
					clearCache = true;
				}			
				return DataSetProvider.getCurrentInstance( )
						.execute( ( (DataSetEditor) getContainer( ) ).getHandle( ),
								query,
								true,
								true,
								clearCache,
								session );
			}
		}
		catch ( BirtException e )
		{
			errorList.add( e );
			return null;
		}
		
		return null;
	}

	private int getMaxRowPreference( )
	{
		int maxRow;
		Preferences preferences = ReportPlugin.getDefault( )
				.getPluginPreferences( );
		if ( preferences.contains( DateSetPreferencePage.USER_MAXROW ) )
		{
			maxRow = preferences.getInt( DateSetPreferencePage.USER_MAXROW );
		}
		else
		{
			maxRow = DateSetPreferencePage.DEFAULT_MAX_ROW;
			preferences.setValue( DateSetPreferencePage.USER_MAXROW,
					maxRow );
		}
		return maxRow;
	}
	
	/**
	 * Show ProgressMonitorDialog
	 * 
	 */
	private void updateResultsProcess( final IQueryResults resultSet )
	{
		needsUpdateUI = true;
		IRunnableWithProgress runnable = new IRunnableWithProgress( ) {

			public void run( IProgressMonitor monitor )
					throws InvocationTargetException, InterruptedException
			{
				monitor.beginTask( "", IProgressMonitor.UNKNOWN );
				populateRecords( resultSet );
				monitor.done( );
			}
		};

		try
		{
			new ProgressMonitorDialog( PlatformUI.getWorkbench( )
					.getDisplay( )
					.getActiveShell( ) ) {

				protected void cancelPressed( )
				{
					super.cancelPressed( );
					needsUpdateUI = false;

					try
					{
						resultSet.close( );
					}
					catch ( BirtException e )
					{
					}
				}

			}.run( true, true, runnable );
		}
		catch ( InvocationTargetException e )
		{
			ExceptionHandler.handle( e );
		}
		catch ( InterruptedException e )
		{
			ExceptionHandler.handle( e );
		}
	}
	
	/**
	 * Populate records to be retrieved when re-render resultSetTable
	 * 
	 * @param metaData
	 * @param query
	 * @throws BirtException
	 */
	private void populateRecords( IQueryResults actualResultSet )
	{
		try
		{
			if ( actualResultSet != null )
			{
				recordList = new ArrayList( );
				IResultIterator iter = actualResultSet.getResultIterator( );

				while ( iter.next( ) && columnCount > 0 )
				{
					String[] record = new String[columnCount];
					for ( int n = 0; n < record.length; n++ )
					{
						record[n] = iter.getString( columnBindingNames[n] );
					}
					recordList.add( record );
				}
				actualResultSet.close( );
			}
		}
		catch ( BirtException e )
		{
			errorList.add( e );
		}
	}
	
	private void updateResultSetTableUI( )
	{
		if ( !needsUpdateUI )
			return;

		if ( !errorList.isEmpty( ) )
		{
			ExceptionHandler.handle( (Exception) errorList.get( 0 ) );
		}
		else
		{
			createColumns( );
			insertRecords( );
		}
	}
	
	private void createColumns( )
	{
		DataSetViewData[] columnsModel = ( (DataSetEditor) getContainer( ) ).getCurrentItemModel( );
		TableColumn column = null;
		TableLayout layout = new TableLayout( );

		for ( int n = 0; n < columnCount; n++ )
		{
			column = new TableColumn( resultSetTable, SWT.LEFT );

			column.setText( getColumnDisplayName( columnsModel, n ) );
			column.setResizable( true );
			layout.addColumnData( new ColumnPixelData( 120, true ) );
			addColumnSortListener( column, n );
			column.pack( );
		}
		resultSetTable.setLayout( layout );
		resultSetTable.layout( true );
	}
	
	private void insertRecords( )
	{
		resultSetTableViewer.setInput( recordList );
	}
	
	private String getColumnDisplayName( DataSetViewData[] columnsModel,
			int index )
	{
		if ( columnsModel == null
				|| columnsModel.length == 0 || index < 0
				|| index > columnsModel.length )
		{
			return "";//$NON-NLS-1$
		}

		return columnsModel[index].getDisplayName( );
	}

	/**
	 * Add listener to a column
	 * @param column
	 * @param n
	 */
	private void addColumnSortListener( TableColumn column, final int index )
	{
		column.addSelectionListener( new SelectionListener( ) {

			private boolean asc = false;

			public void widgetSelected( SelectionEvent e )
			{
				sort( index, asc );
				asc = !asc;
			}

			public void widgetDefaultSelected( SelectionEvent e )
			{

			}
		} );
	}

	/**
	 * Carry out sort operation against certain column
	 * @param columnIndex the column based on which the sort operation would be carried out 
	 * @param asc the sort direction
	 */	
	private void sort( final int columnIndex, final boolean asc )
	{
		resultSetTable.setSortColumn( resultSetTable.getColumn( columnIndex ) );
		resultSetTable.setSortDirection( asc == true ? SWT.DOWN : SWT.UP );

		try
		{
			this.resultSetTableViewer.setSorter( new ViewerSorter( new RuleBasedCollator( ( (RuleBasedCollator) Collator.getInstance( ) ).getRules( ) ) {

				public int compare( String source, String target )
				{
					int result = 0;
					result = super.compare( source, target );

					if ( !asc )
						return result;
					else
						return result *= -1;
				}
			} ) {

//				@Override
				public int compare( Viewer viewer, Object e1, Object e2 )
				{
					e1 = ( (String[]) e1 )[columnIndex] == null ? ""
							: ( (String[]) e1 )[columnIndex];
					e2 = ( (String[]) e2 )[columnIndex] == null ? ""
							: ( (String[]) e2 )[columnIndex];
					return super.compare( viewer, e1, e2 );
				}

			} );
		}
		catch ( ParseException e )
		{
			ExceptionHandler.handle( e );
		}
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.birt.model.core.Listener#elementChanged(org.eclipse.birt.model.api.DesignElementHandle,
	 *      org.eclipse.birt.model.activity.NotificationEvent)
	 */
	public void elementChanged( DesignElementHandle focus, NotificationEvent ev )
	{
		if ( focus.equals( getContainer( ).getModel( ) )
				|| ( (DataSetEditor) this.getContainer( ) ).modelChanged( ) )
		{
			modelChanged = true;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.birt.report.designer.ui.dialogs.properties.IPropertyPage#performCancel()
	 */
	public boolean performCancel( )
	{
		( (DataSetHandle) getContainer( ).getModel( ) ).removeListener( this );
		return super.performCancel( );
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.eclipse.birt.report.designer.ui.dialogs.properties.IPropertyPage#performOk()
	 */
	public boolean performOk( )
	{
		( (DataSetHandle) getContainer( ).getModel( ) ).removeListener( this );
		return super.performOk( );
	}

	/* (non-Javadoc)
	 * @see org.eclipse.birt.report.designer.ui.dialogs.properties.IPropertyPage#getToolTip()
	 */
	public String getToolTip( )
	{
		return Messages.getString("dataset.resultset.preview.tooltip"); //$NON-NLS-1$
	}

}

/**
 * The Action factory
 */
final class ResultSetTableActionFactory
{
	public static final int COPY_ACTION = 1;
	public static final int SELECTALL_ACTION = 2;
	
	public static ResultSetTableAction createResultSetTableAction(
			Table resultSetTable, int operationID )
	{
		assert resultSetTable != null;
		
		ResultSetTableAction rsTableAction = null;
		
		if ( operationID == COPY_ACTION )
		{
			rsTableAction = new CopyAction( resultSetTable );
		}
		else if ( operationID == SELECTALL_ACTION )
		{
			rsTableAction = new SelectAllAction( resultSetTable );
		}
		
		return rsTableAction;
	}
}

/**
 * An implementation of Action
 */
abstract class ResultSetTableAction extends Action
{
	protected Table resultSetTable = null;

	public ResultSetTableAction( Table resultSetTable, String actionName )
	{
		super( actionName );
		this.resultSetTable = resultSetTable;
	}

	/**
	 * This method update the state of the action. Particularly, it will disable
	 * the action under certain circumstance.
	 */	
	public abstract void update( );
}

/**
 * Copy action.
 */
final class CopyAction extends ResultSetTableAction
{

	/**
	 * @param resultSetTable
	 *            the ResultSetTable against which the action is applied to
	 */
	public CopyAction( Table resultSetTable )
	{
		super( resultSetTable, Messages.getString( "CopyAction.text" ) );
		this.setImageDescriptor( Utility.getImageDescriptor( ISharedImages.IMG_TOOL_COPY ) );
	}

	/*
	 * @see org.eclipse.jface.action.IAction#run()
	 */
	public void run( )
	{
		StringBuffer textData = new StringBuffer();
		
		for ( int i = 0; i < resultSetTable.getColumnCount( ); i++ )
		{
			textData.append( resultSetTable.getColumn( i ).getText( ) + "\t" );
		}
		textData.append("\n");
		
		TableItem[] tableItems = resultSetTable.getSelection( );
		for ( int i = 0; i < tableItems.length; i++ )
		{
			for ( int j = 0; j < resultSetTable.getColumnCount( ); j++ )
			{
				textData.append( tableItems[i].getText( j ) + "\t" );
			}
			textData.append("\n");
		}
		
		Clipboard clipboard = new Clipboard( resultSetTable.getDisplay( ) );
		clipboard.setContents( new Object[]{
			textData.toString()
		}, new Transfer[]{
			TextTransfer.getInstance( )
		} );
		clipboard.dispose( );
	}

	/*
	 * @see org.eclipse.birt.report.designer.internal.ui.dialogs.ResultSetTableAction#update()
	 */
	public void update( )
	{
		if ( resultSetTable.getItems( ).length < 1 || resultSetTable.getSelectionCount() < 1)
		{
			this.setEnabled( false );
		}
	}
}

/**
 * Select All Action
 */
final class SelectAllAction extends ResultSetTableAction
{

	/**
	 * @param resultSetTable
	 *            the ResultSetTable against which the action is applied to
	 */
	public SelectAllAction( Table resultSetTable )
	{
		super( resultSetTable, Messages.getString( "SelectAllAction.text" ) );
	}

	/*
	 * @see org.eclipse.jface.action.IAction#run()
	 */
	public void run( )
	{
		resultSetTable.selectAll( );
	}

	/*
	 * @see org.eclipse.birt.report.designer.internal.ui.dialogs.ResultSetTableAction#update()
	 */
	public void update( )
	{
		if ( resultSetTable.getItems( ).length < 1 )
		{
			this.setEnabled( false );
		}
	}
}
