/***********************************************************************
 * 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.chart.computation.withaxes;

import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;

import org.eclipse.birt.chart.computation.DataSetIterator;
import org.eclipse.birt.chart.engine.i18n.Messages;
import org.eclipse.birt.chart.exception.ChartException;
import org.eclipse.birt.chart.factory.RunTimeContext;
import org.eclipse.birt.chart.model.ChartWithAxes;
import org.eclipse.birt.chart.model.component.Axis;
import org.eclipse.birt.chart.model.component.Series;
import org.eclipse.birt.chart.model.data.SeriesDefinition;
import org.eclipse.birt.chart.plugin.ChartEnginePlugin;
import org.eclipse.emf.common.util.EList;

/**
 * Implements a double lookup data structure for stacked series. It also
 * maintains a min/max value for each unit needed to build the scale.
 */
public final class StackedSeriesLookup
{

	private final Hashtable htAxisToStackGroups;

	private final Hashtable htSeriesToStackGroup;

	private int iCachedUnitCount = 0;

	/**
	 * The constructor.
	 */
	StackedSeriesLookup( RunTimeContext rtc )
	{
		htAxisToStackGroups = new Hashtable( );
		htSeriesToStackGroup = new Hashtable( );
	}

	/**
	 * 
	 * @param ax
	 * @return
	 */
	public final ArrayList getStackGroups( Axis ax )
	{
		return (ArrayList) htAxisToStackGroups.get( ax );
	}

	/**
	 * 
	 * @param ax
	 * @return
	 */
	public final int getSeriesCount( Axis ax )
	{
		final ArrayList alSG = (ArrayList) htAxisToStackGroups.get( ax );
		if ( alSG == null || alSG.isEmpty( ) )
		{
			return 0;
		}

		int iCount = 0;
		StackGroup sg;
		for ( int i = 0; i < alSG.size( ); i++ )
		{
			sg = (StackGroup) alSG.get( i );
			iCount += sg.alSeries.size( );
		}
		return iCount;
	}

	/**
	 * @param se
	 * @return The stack group associated with a specified Series
	 */
	public final StackGroup getStackGroup( Series se )
	{
		return (StackGroup) htSeriesToStackGroup.get( se );
	}

	/**
	 * @param sg
	 * @param iUnitIndex
	 * @return An AxisUnit corresponding to a given stack group and specified
	 *         unit index
	 */
	public final AxisSubUnit getSubUnit( StackGroup sg, int iUnitIndex )
	{
		if ( sg == null || !htSeriesToStackGroup.contains( sg ) )
		{
			return null;
		}

		// IF NOT YET INITIALIZED, DO SO LAZILY
		if ( sg.alUnitPositions == null )
		{
			sg.alUnitPositions = new ArrayList( 8 );
		}

		// IF NOT YET CONTAINED, ADD LAZILY
		if ( sg.alUnitPositions.size( ) <= iUnitIndex )
		{
			sg.alUnitPositions.add( new AxisSubUnit( ) );
		}

		return (AxisSubUnit) sg.alUnitPositions.get( iUnitIndex );
	}

	/**
	 * Returns an AxisUnit needed to 'remember' the position of the next stacked
	 * bar to be rendered. If a series is not 'stackable' or not 'set as
	 * stacked', this method will return 'null'.
	 * 
	 * @param ax
	 * @param se
	 * @param iUnitIndex
	 * 
	 * @return
	 */
	public final AxisSubUnit getUnit( Series se, int iUnitIndex )
	{
		// LOOKUP STACKED GROUP FOR SERIES
		StackGroup sg = (StackGroup) htSeriesToStackGroup.get( se );
		if ( sg == null )
		{
			return null;
		}

		// IF NOT YET INITIALIZED, DO SO LAZILY
		if ( sg.alUnitPositions == null )
		{
			sg.alUnitPositions = new ArrayList( 8 );
		}

		// IF NOT YET CONTAINED, ADD LAZILY
		if ( sg.alUnitPositions.size( ) <= iUnitIndex )
		{
			sg.alUnitPositions.add( new AxisSubUnit( ) );
		}

		return (AxisSubUnit) sg.alUnitPositions.get( iUnitIndex );
	}

	/**
	 * 
	 */
	public final void resetSubUnits( )
	{
		Enumeration e = htSeriesToStackGroup.elements( );
		StackGroup sg;
		AxisSubUnit asu;

		while ( e.hasMoreElements( ) )
		{
			sg = (StackGroup) e.nextElement( );
			if ( sg.alUnitPositions != null )
			{
				for ( int i = 0; i < sg.alUnitPositions.size( ); i++ )
				{
					asu = (AxisSubUnit) sg.alUnitPositions.get( i );
					asu.reset( );
				}
			}
		}
	}

	/**
	 * 
	 * @param cwa
	 * @return
	 * @throws UndefinedValueException
	 * @throws UnexpectedInputException
	 */
	static final StackedSeriesLookup create( ChartWithAxes cwa,
			RunTimeContext rtc ) throws ChartException,
			IllegalArgumentException
	{
		if ( cwa == null ) // NPE CHECK
		{
			return null;
		}

		final StackedSeriesLookup ssl = new StackedSeriesLookup( rtc );
		final Axis axBase = cwa.getBaseAxes( )[0];
		final Axis[] axaOrthogonal = cwa.getOrthogonalAxes( axBase, true );

		EList el;
		List alSeries;
		int iSeriesCount;
		StackGroup sg, sgSingle;
		Series se;
		boolean bStackedSet;
		SeriesDefinition sd;
		int iSharedUnitIndex, iSharedUnitCount, iDataSetCount;
		ArrayList alSGCopies;
		DataSetIterator dsi = null;

		for ( int i = 0; i < axaOrthogonal.length; i++ ) // EACH AXIS
		{
			iSharedUnitIndex = 0;
			iSharedUnitCount = 0;
			sgSingle = null; // RESET PER AXIS
			el = axaOrthogonal[i].getSeriesDefinitions( );
			alSGCopies = new ArrayList( 4 );
			iSharedUnitCount = 0;

			for ( int j = 0; j < el.size( ); j++ ) // EACH SERIES DEFINITION
			{
				sd = (SeriesDefinition) el.get( j );
				alSeries = sd.getRunTimeSeries( );
				iSeriesCount = alSeries.size( );
				if ( iSeriesCount > 1 )
				{
					bStackedSet = false;
					sg = null;

					for ( int k = 0; k < iSeriesCount; k++ ) // EACH SERIES
					{
						se = (Series) alSeries.get( k );
						dsi = new DataSetIterator( se.getDataSet( ) );
						iDataSetCount = dsi.size( );
						if ( ssl.iCachedUnitCount == 0 )
						{
							ssl.iCachedUnitCount = iDataSetCount;
						}
						else if ( ssl.iCachedUnitCount != iDataSetCount )
						{
							throw new IllegalArgumentException( MessageFormat.format( Messages.getResourceBundle( 
									rtc.getULocale( ) )
									.getString( "exception.runtime.dataset.count.mismatch" ), //$NON-NLS-1$
									new Object[]{
											new Integer( ssl.iCachedUnitCount ),
											new Integer( iDataSetCount )
									} )

							);
						}
						if ( se.canBeStacked( ) )
						{
							if ( !se.isSetStacked( ) )
							{
								throw new ChartException( ChartEnginePlugin.ID,
										ChartException.UNDEFINED_VALUE,
										"exception.unset.series.stacked.property", //$NON-NLS-1$
										new Object[]{
											se
										},
										Messages.getResourceBundle( 
												rtc.getULocale( ) ) );
							}
							if ( se.canShareAxisUnit( ) )
							{
								if ( se.isStacked( ) )
								{
									if ( k > 0 && !bStackedSet )
									{
										throw new IllegalArgumentException( MessageFormat.format( Messages.getResourceBundle( 
												rtc.getULocale( ) )
												.getString( "exception.stacked.unstacked.mix.series" ), //$NON-NLS-1$ 
												new Object[]{
													sd
												} )

										);
									}
									if ( k == 0 ) // ONE GROUP FOR ALL STACKED
									// SERIES
									{
										sg = new StackGroup( iSharedUnitIndex++ );
										alSGCopies.add( sg );
										iSharedUnitCount++;
									}
									bStackedSet = true;
									ssl.htSeriesToStackGroup.put( se, sg );
									sg.addSeries( se ); // REQUIRE REVERSE
									// LOOKUP
								}
								else
								{
									if ( k > 0 && bStackedSet )
									{
										throw new IllegalArgumentException( MessageFormat.format( Messages.getResourceBundle( 
												rtc.getULocale( ) )
												.getString( "exception.stacked.unstacked.mix.series" ), //$NON-NLS-1$ 
												new Object[]{
													sd
												} )

										);
									}
									sg = new StackGroup( iSharedUnitIndex++ ); // NEW
									// GROUP
									// FOR
									// EACH
									// UNSTACKED
									// SERIES
									alSGCopies.add( sg );
									iSharedUnitCount++;
									ssl.htSeriesToStackGroup.put( se, sg );
									sg.addSeries( se ); // REQUIRE REVERSE
									// LOOKUP
								}
							}
							else
							{
								if ( se.isStacked( ) )
								{
									if ( k > 0 && !bStackedSet )
									{
										throw new IllegalArgumentException( MessageFormat.format( Messages.getResourceBundle( 
												rtc.getULocale( ) )
												.getString( "exception.stacked.unstacked.mix.series" ), //$NON-NLS-1$ 
												new Object[]{
													sd
												} )

										);
									}
									if ( k == 0 ) // ONE GROUP FOR ALL STACKED
									// SERIES
									{
										sg = new StackGroup( -1 ); // UNSET
										// BECAUSE
										// DOESNT
										// SHARE AXIS
										// UNITS
										alSGCopies.add( sg );
									}
									bStackedSet = true;
									ssl.htSeriesToStackGroup.put( se, sg );
									sg.addSeries( se ); // REQUIRE REVERSE
									// LOOKUP
								}
								else
								{
									if ( k > 0 && bStackedSet )
									{
										throw new IllegalArgumentException( MessageFormat.format( Messages.getResourceBundle( 
												rtc.getULocale( ) )
												.getString( "exception.stacked.unstacked.mix.series" ), //$NON-NLS-1$ 
												new Object[]{
													sd
												} )

										);
									}
									sg = new StackGroup( -1 ); // NEW GROUP FOR
									// EACH UNSTACKED
									// SERIES
									alSGCopies.add( sg );
									ssl.htSeriesToStackGroup.put( se, sg );
									sg.addSeries( se ); // REQUIRE REVERSE
									// LOOKUP
								}
							}
						}
						else
						// e.g. each custom series in its own stack (not stacked
						// but
						{
							sg = new StackGroup( -1 ); // ONE PER UNSTACKED
							// SERIES (SHARED INDEX
							// IS UNSET)
							alSGCopies.add( sg );
							ssl.htSeriesToStackGroup.put( se, sg );
							sg.addSeries( se ); // REQUIRE REVERSE LOOKUP
						}
					}
				}
				else
				// ONE OR LESS SERIES USE THE SINGLE STACK GROUP
				{
					for ( int k = 0; k < iSeriesCount; k++ ) // EACH SERIES
					// (iSeriesCount
					// SHOULD BE ONE)
					{
						se = (Series) alSeries.get( k );
						dsi = new DataSetIterator( se.getDataSet( ) );
						iDataSetCount = dsi.size( );
						if ( ssl.iCachedUnitCount == 0 )
						{
							ssl.iCachedUnitCount = iDataSetCount;
						}
						else if ( ssl.iCachedUnitCount != iDataSetCount )
						{
							throw new IllegalArgumentException( MessageFormat.format( Messages.getResourceBundle( 
									rtc.getULocale( ) )
									.getString( "exception.runtime.dataset.count.mismatch" ), //$NON-NLS-1$
									new Object[]{
											new Integer( ssl.iCachedUnitCount ),
											new Integer( iDataSetCount )
									} )

							);
						}
						if ( se.canBeStacked( ) )
						{
							if ( se.canShareAxisUnit( ) )
							{
								if ( se.isStacked( ) )
								{
									if ( sgSingle == null )
									{
										sgSingle = new StackGroup( iSharedUnitIndex++ );
										alSGCopies.add( sgSingle );
										iSharedUnitCount++;
									}
									ssl.htSeriesToStackGroup.put( se, sgSingle );
									sgSingle.addSeries( se ); // REQUIRE
									// REVERSE
									// LOOKUP
								}
								else
								{
									sg = new StackGroup( iSharedUnitIndex++ ); // ONE
									// PER
									// UNSTACKED
									// SERIES
									// (SHARED
									// INDEX
									// IS
									// SET)
									iSharedUnitCount++;
									alSGCopies.add( sg );
									ssl.htSeriesToStackGroup.put( se, sg );
									sg.addSeries( se ); // REQUIRE REVERSE
									// LOOKUP
								}
							}
							else
							// e.g. each line series in its own stack
							{
								sg = new StackGroup( -1 ); // ONE PER UNSTACKED
								// SERIES (SHARED
								// INDEX IS UNSET)
								alSGCopies.add( sg );
								ssl.htSeriesToStackGroup.put( se, sg );
								sg.addSeries( se ); // REQUIRE REVERSE LOOKUP
							}
						}
						else
						// e.g. each custom series in its own stack (not stacked
						// but
						{
							sg = new StackGroup( -1 ); // ONE PER UNSTACKED
							// SERIES (SHARED INDEX
							// IS UNSET)
							alSGCopies.add( sg );
							ssl.htSeriesToStackGroup.put( se, sg );
							sg.addSeries( se ); // REQUIRE REVERSE LOOKUP
						}
					}
				}
			}

			if ( iSharedUnitCount < 1 )
				iSharedUnitCount = 1;
			for ( int j = 0; j < alSGCopies.size( ); j++ )
			{
				sg = (StackGroup) alSGCopies.get( j );
				// if (sg.getSharedIndex() >= 0)
				{
					sg.updateCount( iSharedUnitCount );
				}
			}

			ssl.htAxisToStackGroups.put( axaOrthogonal[i], alSGCopies );
		}

		return ssl;
	}

	/**
	 * @return
	 */
	public final int getUnitCount( )
	{
		return iCachedUnitCount;
	}
}