/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package it.sephiroth.android.library.widget;

import android.database.DataSetObserver;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.view.View;
import android.view.ViewGroup;
import android.widget.*;

import java.util.ArrayList;
import java.util.Collections;

/*
 * Implementation notes:
 * 
 * <p>
 * Terminology:
 * <li> flPos - Flat list position, the position used by ListView
 * <li> gPos - Group position, the position of a group among all the groups
 * <li> cPos - Child position, the position of a child among all the children
 * in a group
 */

/**
 * A {@link android.widget.BaseAdapter} that provides data/Views in an expandable list (offers
 * features such as collapsing/expanding groups containing children). By
 * itself, this adapter has no data and is a connector to a
 * {@link android.widget.ExpandableListAdapter} which provides the data.
 * <p/>
 * Internally, this connector translates the flat list position that the
 * ListAdapter expects to/from group and child positions that the ExpandableListAdapter
 * expects.
 */
class ExpandableHListConnector extends BaseAdapter implements Filterable {
	/**
	 * The ExpandableListAdapter to fetch the data/Views for this expandable list
	 */
	private ExpandableListAdapter mExpandableListAdapter;

	/**
	 * List of metadata for the currently expanded groups. The metadata consists
	 * of data essential for efficiently translating between flat list positions
	 * and group/child positions. See {@link ExpandableHListConnector.GroupMetadata}.
	 */
	private ArrayList<GroupMetadata> mExpGroupMetadataList;

	/** The number of children from all currently expanded groups */
	private int mTotalExpChildrenCount;

	/** The maximum number of allowable expanded groups. Defaults to 'no limit' */
	private int mMaxExpGroupCount = Integer.MAX_VALUE;

	/** Change observer used to have ExpandableListAdapter changes pushed to us */
	private final DataSetObserver mDataSetObserver = new MyDataSetObserver();

	/**
	 * Constructs the connector
	 */
	public ExpandableHListConnector( ExpandableListAdapter expandableListAdapter ) {
		mExpGroupMetadataList = new ArrayList<GroupMetadata>();

		setExpandableListAdapter( expandableListAdapter );
	}

	/**
	 * Point to the {@link android.widget.ExpandableListAdapter} that will give us data/Views
	 *
	 * @param expandableListAdapter the adapter that supplies us with data/Views
	 */
	public void setExpandableListAdapter( ExpandableListAdapter expandableListAdapter ) {
		if( mExpandableListAdapter != null ) {
			mExpandableListAdapter.unregisterDataSetObserver( mDataSetObserver );
		}

		mExpandableListAdapter = expandableListAdapter;
		expandableListAdapter.registerDataSetObserver( mDataSetObserver );
	}

	/**
	 * Translates a flat list position to either a) group pos if the specified
	 * flat list position corresponds to a group, or b) child pos if it
	 * corresponds to a child.  Performs a binary search on the expanded
	 * groups list to find the flat list pos if it is an exp group, otherwise
	 * finds where the flat list pos fits in between the exp groups.
	 *
	 * @param flPos the flat list position to be translated
	 * @return the group position or child position of the specified flat list
	 * position encompassed in a {@link ExpandableHListConnector.PositionMetadata} object
	 * that contains additional useful info for insertion, etc.
	 */
	PositionMetadata getUnflattenedPos( final int flPos ) {
	    /* Keep locally since frequent use */
		final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
		final int numExpGroups = egml.size();
        
        /* Binary search variables */
		int leftExpGroupIndex = 0;
		int rightExpGroupIndex = numExpGroups - 1;
		int midExpGroupIndex = 0;
		GroupMetadata midExpGm;

		if( numExpGroups == 0 ) {
            /*
             * There aren't any expanded groups (hence no visible children
             * either), so flPos must be a group and its group pos will be the
             * same as its flPos
             */
			return PositionMetadata.obtain( flPos, ExpandableHListPosition.GROUP, flPos, - 1, null, 0 );
		}

        /*
         * Binary search over the expanded groups to find either the exact
         * expanded group (if we're looking for a group) or the group that
         * contains the child we're looking for. If we are looking for a
         * collapsed group, we will not have a direct match here, but we will
         * find the expanded group just before the group we're searching for (so
         * then we can calculate the group position of the group we're searching
         * for). If there isn't an expanded group prior to the group being
         * searched for, then the group being searched for's group position is
         * the same as the flat list position (since there are no children before
         * it, and all groups before it are collapsed).
         */
		while( leftExpGroupIndex <= rightExpGroupIndex ) {
			midExpGroupIndex = ( rightExpGroupIndex - leftExpGroupIndex ) / 2 + leftExpGroupIndex;
			midExpGm = egml.get( midExpGroupIndex );

			if( flPos > midExpGm.lastChildFlPos ) {
                /*
                 * The flat list position is after the current middle group's
                 * last child's flat list position, so search right
                 */
				leftExpGroupIndex = midExpGroupIndex + 1;
			}
			else if( flPos < midExpGm.flPos ) {
                /*
                 * The flat list position is before the current middle group's
                 * flat list position, so search left
                 */
				rightExpGroupIndex = midExpGroupIndex - 1;
			}
			else if( flPos == midExpGm.flPos ) {
                /*
                 * The flat list position is this middle group's flat list
                 * position, so we've found an exact hit
                 */
				return PositionMetadata.obtain( flPos, ExpandableHListPosition.GROUP, midExpGm.gPos, - 1, midExpGm, midExpGroupIndex );
			}
			else if( flPos <= midExpGm.lastChildFlPos
                    /* && flPos > midGm.flPos as deduced from previous
                     * conditions */ ) {
                /* The flat list position is a child of the middle group */
                
                /* 
                 * Subtract the first child's flat list position from the
                 * specified flat list pos to get the child's position within
                 * the group
                 */
				final int childPos = flPos - ( midExpGm.flPos + 1 );
				return PositionMetadata.obtain( flPos, ExpandableHListPosition.CHILD, midExpGm.gPos, childPos, midExpGm, midExpGroupIndex );
			}
		}

        /* 
         * If we've reached here, it means the flat list position must be a
         * group that is not expanded, since otherwise we would have hit it
         * in the above search.
         */


		/**
		 * If we are to expand this group later, where would it go in the
		 * mExpGroupMetadataList ?
		 */
		int insertPosition = 0;

		/** What is its group position in the list of all groups? */
		int groupPos = 0;
        
        /*
         * To figure out exact insertion and prior group positions, we need to
         * determine how we broke out of the binary search.  We backtrack
         * to see this.
         */
		if( leftExpGroupIndex > midExpGroupIndex ) {
            
            /*
             * This would occur in the first conditional, so the flat list
             * insertion position is after the left group. Also, the
             * leftGroupPos is one more than it should be (since that broke out
             * of our binary search), so we decrement it.
             */
			final GroupMetadata leftExpGm = egml.get( leftExpGroupIndex - 1 );

			insertPosition = leftExpGroupIndex;

            /*
             * Sums the number of groups between the prior exp group and this
             * one, and then adds it to the prior group's group pos
             */
			groupPos = ( flPos - leftExpGm.lastChildFlPos ) + leftExpGm.gPos;
		}
		else if( rightExpGroupIndex < midExpGroupIndex ) {

            /*
             * This would occur in the second conditional, so the flat list
             * insertion position is before the right group. Also, the
             * rightGroupPos is one less than it should be, so increment it.
             */
			final GroupMetadata rightExpGm = egml.get( ++ rightExpGroupIndex );

			insertPosition = rightExpGroupIndex;
            
            /*
             * Subtracts this group's flat list pos from the group after's flat
             * list position to find out how many groups are in between the two
             * groups. Then, subtracts that number from the group after's group
             * pos to get this group's pos.
             */
			groupPos = rightExpGm.gPos - ( rightExpGm.flPos - flPos );
		}
		else {
			// TODO: clean exit
			throw new RuntimeException( "Unknown state" );
		}

		return PositionMetadata.obtain( flPos, ExpandableHListPosition.GROUP, groupPos, - 1, null, insertPosition );
	}

	/**
	 * Translates either a group pos or a child pos (+ group it belongs to) to a
	 * flat list position.  If searching for a child and its group is not expanded, this will
	 * return null since the child isn't being shown in the ListView, and hence it has no
	 * position.
	 *
	 * @param pos a {@link ExpandableHListPosition} representing either a group position
	 *            or child position
	 * @return the flat list position encompassed in a {@link ExpandableHListConnector.PositionMetadata}
	 * object that contains additional useful info for insertion, etc., or null.
	 */
	PositionMetadata getFlattenedPos( final ExpandableHListPosition pos ) {
		final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
		final int numExpGroups = egml.size();

        /* Binary search variables */
		int leftExpGroupIndex = 0;
		int rightExpGroupIndex = numExpGroups - 1;
		int midExpGroupIndex = 0;
		GroupMetadata midExpGm;

		if( numExpGroups == 0 ) {
            /*
             * There aren't any expanded groups, so flPos must be a group and
             * its flPos will be the same as its group pos.  The
             * insert position is 0 (since the list is empty).
             */
			return PositionMetadata.obtain( pos.groupPos, pos.type, pos.groupPos, pos.childPos, null, 0 );
		}

        /*
         * Binary search over the expanded groups to find either the exact
         * expanded group (if we're looking for a group) or the group that
         * contains the child we're looking for.
         */
		while( leftExpGroupIndex <= rightExpGroupIndex ) {
			midExpGroupIndex = ( rightExpGroupIndex - leftExpGroupIndex ) / 2 + leftExpGroupIndex;
			midExpGm = egml.get( midExpGroupIndex );

			if( pos.groupPos > midExpGm.gPos ) {
                /*
                 * It's after the current middle group, so search right
                 */
				leftExpGroupIndex = midExpGroupIndex + 1;
			}
			else if( pos.groupPos < midExpGm.gPos ) {
                /*
                 * It's before the current middle group, so search left
                 */
				rightExpGroupIndex = midExpGroupIndex - 1;
			}
			else if( pos.groupPos == midExpGm.gPos ) {
                /*
                 * It's this middle group, exact hit
                 */

				if( pos.type == ExpandableHListPosition.GROUP ) {
                    /* If it's a group, give them this matched group's flPos */
					return PositionMetadata.obtain( midExpGm.flPos, pos.type, pos.groupPos, pos.childPos, midExpGm, midExpGroupIndex );
				}
				else if( pos.type == ExpandableHListPosition.CHILD ) {
                    /* If it's a child, calculate the flat list pos */
					return PositionMetadata.obtain( midExpGm.flPos + pos.childPos + 1, pos.type, pos.groupPos, pos.childPos, midExpGm, midExpGroupIndex );
				}
				else {
					return null;
				}
			}
		}

        /* 
         * If we've reached here, it means there was no match in the expanded
         * groups, so it must be a collapsed group that they're search for
         */
		if( pos.type != ExpandableHListPosition.GROUP ) {
            /* If it isn't a group, return null */
			return null;
		}
        
        /*
         * To figure out exact insertion and prior group positions, we need to
         * determine how we broke out of the binary search. We backtrack to see
         * this.
         */
		if( leftExpGroupIndex > midExpGroupIndex ) {
            
            /*
             * This would occur in the first conditional, so the flat list
             * insertion position is after the left group.
             * 
             * The leftGroupPos is one more than it should be (from the binary
             * search loop) so we subtract 1 to get the actual left group.  Since
             * the insertion point is AFTER the left group, we keep this +1
             * value as the insertion point
             */
			final GroupMetadata leftExpGm = egml.get( leftExpGroupIndex - 1 );
			final int flPos = leftExpGm.lastChildFlPos + ( pos.groupPos - leftExpGm.gPos );

			return PositionMetadata.obtain( flPos, pos.type, pos.groupPos, pos.childPos, null, leftExpGroupIndex );
		}
		else if( rightExpGroupIndex < midExpGroupIndex ) {

            /*
             * This would occur in the second conditional, so the flat list
             * insertion position is before the right group. Also, the
             * rightGroupPos is one less than it should be (from binary search
             * loop), so we increment to it.
             */
			final GroupMetadata rightExpGm = egml.get( ++ rightExpGroupIndex );
			final int flPos = rightExpGm.flPos - ( rightExpGm.gPos - pos.groupPos );
			return PositionMetadata.obtain( flPos, pos.type, pos.groupPos, pos.childPos, null, rightExpGroupIndex );
		}
		else {
			return null;
		}
	}

	@Override
	public boolean areAllItemsEnabled() {
		return mExpandableListAdapter.areAllItemsEnabled();
	}

	@Override
	public boolean isEnabled( int flatListPos ) {
		final PositionMetadata metadata = getUnflattenedPos( flatListPos );
		final ExpandableHListPosition pos = metadata.position;

		boolean retValue;
		if( pos.type == ExpandableHListPosition.CHILD ) {
			retValue = mExpandableListAdapter.isChildSelectable( pos.groupPos, pos.childPos );
		}
		else {
			// Groups are always selectable
			retValue = true;
		}

		metadata.recycle();

		return retValue;
	}

	public int getCount() {
        /*
         * Total count for the list view is the number groups plus the 
         * number of children from currently expanded groups (a value we keep
         * cached in this class)
         */
		return mExpandableListAdapter.getGroupCount() + mTotalExpChildrenCount;
	}

	public Object getItem( int flatListPos ) {
		final PositionMetadata posMetadata = getUnflattenedPos( flatListPos );

		Object retValue;
		if( posMetadata.position.type == ExpandableHListPosition.GROUP ) {
			retValue = mExpandableListAdapter.getGroup( posMetadata.position.groupPos );
		}
		else if( posMetadata.position.type == ExpandableHListPosition.CHILD ) {
			retValue = mExpandableListAdapter.getChild( posMetadata.position.groupPos, posMetadata.position.childPos );
		}
		else {
			// TODO: clean exit
			throw new RuntimeException( "Flat list position is of unknown type" );
		}

		posMetadata.recycle();

		return retValue;
	}

	public long getItemId( int flatListPos ) {
		final PositionMetadata posMetadata = getUnflattenedPos( flatListPos );
		final long groupId = mExpandableListAdapter.getGroupId( posMetadata.position.groupPos );

		long retValue;
		if( posMetadata.position.type == ExpandableHListPosition.GROUP ) {
			retValue = mExpandableListAdapter.getCombinedGroupId( groupId );
		}
		else if( posMetadata.position.type == ExpandableHListPosition.CHILD ) {
			final long childId = mExpandableListAdapter.getChildId( posMetadata.position.groupPos, posMetadata.position.childPos );
			retValue = mExpandableListAdapter.getCombinedChildId( groupId, childId );
		}
		else {
			// TODO: clean exit
			throw new RuntimeException( "Flat list position is of unknown type" );
		}

		posMetadata.recycle();

		return retValue;
	}

	public View getView( int flatListPos, View convertView, ViewGroup parent ) {
		final PositionMetadata posMetadata = getUnflattenedPos( flatListPos );

		View retValue;
		if( posMetadata.position.type == ExpandableHListPosition.GROUP ) {
			retValue = mExpandableListAdapter.getGroupView( posMetadata.position.groupPos, posMetadata.isExpanded(), convertView, parent );
		}
		else if( posMetadata.position.type == ExpandableHListPosition.CHILD ) {
			final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos;

			retValue = mExpandableListAdapter.getChildView( posMetadata.position.groupPos, posMetadata.position.childPos, isLastChild, convertView, parent );
		}
		else {
			// TODO: clean exit
			throw new RuntimeException( "Flat list position is of unknown type" );
		}

		posMetadata.recycle();

		return retValue;
	}

	@Override
	public int getItemViewType( int flatListPos ) {
		final PositionMetadata metadata = getUnflattenedPos( flatListPos );
		final ExpandableHListPosition pos = metadata.position;

		int retValue;
		if( mExpandableListAdapter instanceof HeterogeneousExpandableList ) {
			HeterogeneousExpandableList adapter = (HeterogeneousExpandableList) mExpandableListAdapter;
			if( pos.type == ExpandableHListPosition.GROUP ) {
				retValue = adapter.getGroupType( pos.groupPos );
			}
			else {
				final int childType = adapter.getChildType( pos.groupPos, pos.childPos );
				retValue = adapter.getGroupTypeCount() + childType;
			}
		}
		else {
			if( pos.type == ExpandableHListPosition.GROUP ) {
				retValue = 0;
			}
			else {
				retValue = 1;
			}
		}

		metadata.recycle();

		return retValue;
	}

	@Override
	public int getViewTypeCount() {
		if( mExpandableListAdapter instanceof HeterogeneousExpandableList ) {
			HeterogeneousExpandableList adapter = (HeterogeneousExpandableList) mExpandableListAdapter;
			return adapter.getGroupTypeCount() + adapter.getChildTypeCount();
		}
		else {
			return 2;
		}
	}

	@Override
	public boolean hasStableIds() {
		return mExpandableListAdapter.hasStableIds();
	}

	/**
	 * Traverses the expanded group metadata list and fills in the flat list
	 * positions.
	 *
	 * @param forceChildrenCountRefresh Forces refreshing of the children count
	 *                                  for all expanded groups.
	 * @param syncGroupPositions        Whether to search for the group positions
	 *                                  based on the group IDs. This should only be needed when calling
	 *                                  this from an onChanged callback.
	 */
	@SuppressWarnings( "unchecked" )
	private void refreshExpGroupMetadataList(
			boolean forceChildrenCountRefresh, boolean syncGroupPositions ) {
		final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
		int egmlSize = egml.size();
		int curFlPos = 0;
        
        /* Update child count as we go through */
		mTotalExpChildrenCount = 0;

		if( syncGroupPositions ) {
			// We need to check whether any groups have moved positions
			boolean positionsChanged = false;

			for( int i = egmlSize - 1; i >= 0; i-- ) {
				GroupMetadata curGm = egml.get( i );
				int newGPos = findGroupPosition( curGm.gId, curGm.gPos );
				if( newGPos != curGm.gPos ) {
					if( newGPos == android.widget.AdapterView.INVALID_POSITION ) {
						// Doh, just remove it from the list of expanded groups
						egml.remove( i );
						egmlSize--;
					}

					curGm.gPos = newGPos;
					if( ! positionsChanged ) positionsChanged = true;
				}
			}

			if( positionsChanged ) {
				// At least one group changed positions, so re-sort
				Collections.sort( egml );
			}
		}

		int gChildrenCount;
		int lastGPos = 0;
		for( int i = 0; i < egmlSize; i++ ) {
            /* Store in local variable since we'll access freq */
			GroupMetadata curGm = egml.get( i );
            
            /*
             * Get the number of children, try to refrain from calling
             * another class's method unless we have to (so do a subtraction)
             */
			if( ( curGm.lastChildFlPos == GroupMetadata.REFRESH ) || forceChildrenCountRefresh ) {
				gChildrenCount = mExpandableListAdapter.getChildrenCount( curGm.gPos );
			}
			else {
                /* Num children for this group is its last child's fl pos minus
                 * the group's fl pos
                 */
				gChildrenCount = curGm.lastChildFlPos - curGm.flPos;
			}
            
            /* Update */
			mTotalExpChildrenCount += gChildrenCount;
            
            /*
             * This skips the collapsed groups and increments the flat list
             * position (for subsequent exp groups) by accounting for the collapsed
             * groups
             */
			curFlPos += ( curGm.gPos - lastGPos );
			lastGPos = curGm.gPos;
            
            /* Update the flat list positions, and the current flat list pos */
			curGm.flPos = curFlPos;
			curFlPos += gChildrenCount;
			curGm.lastChildFlPos = curFlPos;
		}
	}

	/**
	 * Collapse a group in the grouped list view
	 *
	 * @param groupPos position of the group to collapse
	 */
	boolean collapseGroup( int groupPos ) {
		ExpandableHListPosition elGroupPos = ExpandableHListPosition.obtain( ExpandableHListPosition.GROUP, groupPos, - 1, - 1 );
		PositionMetadata pm = getFlattenedPos( elGroupPos );
		elGroupPos.recycle();
		if( pm == null ) return false;

		boolean retValue = collapseGroup( pm );
		pm.recycle();
		return retValue;
	}

	boolean collapseGroup( PositionMetadata posMetadata ) {
        /*
         * Collapsing requires removal from mExpGroupMetadataList 
         */
        
        /*
         * If it is null, it must be already collapsed. This group metadata
         * object should have been set from the search that returned the
         * position metadata object.
         */
		if( posMetadata.groupMetadata == null ) return false;

		// Remove the group from the list of expanded groups
		mExpGroupMetadataList.remove( posMetadata.groupMetadata );

		// Refresh the metadata
		refreshExpGroupMetadataList( false, false );

		// Notify of change
		notifyDataSetChanged();

		// Give the callback
		mExpandableListAdapter.onGroupCollapsed( posMetadata.groupMetadata.gPos );

		return true;
	}

	/**
	 * Expand a group in the grouped list view
	 *
	 * @param groupPos the group to be expanded
	 */
	boolean expandGroup( int groupPos ) {
		ExpandableHListPosition elGroupPos = ExpandableHListPosition.obtain( ExpandableHListPosition.GROUP, groupPos, - 1, - 1 );
		PositionMetadata pm = getFlattenedPos( elGroupPos );
		elGroupPos.recycle();
		boolean retValue = expandGroup( pm );
		pm.recycle();
		return retValue;
	}

	boolean expandGroup( PositionMetadata posMetadata ) {
        /*
         * Expanding requires insertion into the mExpGroupMetadataList 
         */

		if( posMetadata.position.groupPos < 0 ) {
			// TODO clean exit
			throw new RuntimeException( "Need group" );
		}

		if( mMaxExpGroupCount == 0 ) return false;

		// Check to see if it's already expanded
		if( posMetadata.groupMetadata != null ) return false;
        
        /* Restrict number of expanded groups to mMaxExpGroupCount */
		if( mExpGroupMetadataList.size() >= mMaxExpGroupCount ) {
            /* Collapse a group */
			// TODO: Collapse something not on the screen instead of the first one?
			// TODO: Could write overloaded function to take GroupMetadata to collapse
			GroupMetadata collapsedGm = mExpGroupMetadataList.get( 0 );

			int collapsedIndex = mExpGroupMetadataList.indexOf( collapsedGm );

			collapseGroup( collapsedGm.gPos );

            /* Decrement index if it is after the group we removed */
			if( posMetadata.groupInsertIndex > collapsedIndex ) {
				posMetadata.groupInsertIndex--;
			}
		}

		GroupMetadata expandedGm = GroupMetadata.obtain( GroupMetadata.REFRESH,
		                                                 GroupMetadata.REFRESH,
		                                                 posMetadata.position.groupPos,
		                                                 mExpandableListAdapter.getGroupId( posMetadata.position.groupPos ) );

		mExpGroupMetadataList.add( posMetadata.groupInsertIndex, expandedGm );

		// Refresh the metadata
		refreshExpGroupMetadataList( false, false );

		// Notify of change
		notifyDataSetChanged();

		// Give the callback
		mExpandableListAdapter.onGroupExpanded( expandedGm.gPos );

		return true;
	}

	/**
	 * Whether the given group is currently expanded.
	 *
	 * @param groupPosition The group to check.
	 * @return Whether the group is currently expanded.
	 */
	public boolean isGroupExpanded( int groupPosition ) {
		GroupMetadata groupMetadata;
		for( int i = mExpGroupMetadataList.size() - 1; i >= 0; i-- ) {
			groupMetadata = mExpGroupMetadataList.get( i );

			if( groupMetadata.gPos == groupPosition ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Set the maximum number of groups that can be expanded at any given time
	 */
	public void setMaxExpGroupCount( int maxExpGroupCount ) {
		mMaxExpGroupCount = maxExpGroupCount;
	}

	ExpandableListAdapter getAdapter() {
		return mExpandableListAdapter;
	}

	public Filter getFilter() {
		ExpandableListAdapter adapter = getAdapter();
		if( adapter instanceof Filterable ) {
			return ( (Filterable) adapter ).getFilter();
		}
		else {
			return null;
		}
	}

	ArrayList<GroupMetadata> getExpandedGroupMetadataList() {
		return mExpGroupMetadataList;
	}

	void setExpandedGroupMetadataList( ArrayList<GroupMetadata> expandedGroupMetadataList ) {

		if( ( expandedGroupMetadataList == null ) || ( mExpandableListAdapter == null ) ) {
			return;
		}

		// Make sure our current data set is big enough for the previously
		// expanded groups, if not, ignore this request
		int numGroups = mExpandableListAdapter.getGroupCount();
		for( int i = expandedGroupMetadataList.size() - 1; i >= 0; i-- ) {
			if( expandedGroupMetadataList.get( i ).gPos >= numGroups ) {
				// Doh, for some reason the client doesn't have some of the groups
				return;
			}
		}

		mExpGroupMetadataList = expandedGroupMetadataList;
		refreshExpGroupMetadataList( true, false );
	}

	@Override
	public boolean isEmpty() {
		ExpandableListAdapter adapter = getAdapter();
		return adapter != null ? adapter.isEmpty() : true;
	}

	/**
	 * Searches the expandable list adapter for a group position matching the
	 * given group ID. The search starts at the given seed position and then
	 * alternates between moving up and moving down until 1) we find the right
	 * position, or 2) we run out of time, or 3) we have looked at every
	 * position
	 *
	 * @return Position of the row that matches the given row ID, or
	 * {@link android.widget.AdapterView#INVALID_POSITION} if it can't be found
	 * @see AdapterView#findSyncPosition()
	 */
	int findGroupPosition( long groupIdToMatch, int seedGroupPosition ) {
		int count = mExpandableListAdapter.getGroupCount();

		if( count == 0 ) {
			return android.widget.AdapterView.INVALID_POSITION;
		}

		// If there isn't a selection don't hunt for it
		if( groupIdToMatch == android.widget.AdapterView.INVALID_ROW_ID ) {
			return android.widget.AdapterView.INVALID_POSITION;
		}

		// Pin seed to reasonable values
		seedGroupPosition = Math.max( 0, seedGroupPosition );
		seedGroupPosition = Math.min( count - 1, seedGroupPosition );

		long endTime = SystemClock.uptimeMillis() + AdapterView.SYNC_MAX_DURATION_MILLIS;

		long rowId;

		// first position scanned so far
		int first = seedGroupPosition;

		// last position scanned so far
		int last = seedGroupPosition;

		// True if we should move down on the next iteration
		boolean next = false;

		// True when we have looked at the first item in the data
		boolean hitFirst;

		// True when we have looked at the last item in the data
		boolean hitLast;

		// Get the item ID locally (instead of getItemIdAtPosition), so
		// we need the adapter
		ExpandableListAdapter adapter = getAdapter();
		if( adapter == null ) {
			return android.widget.AdapterView.INVALID_POSITION;
		}

		while( SystemClock.uptimeMillis() <= endTime ) {
			rowId = adapter.getGroupId( seedGroupPosition );
			if( rowId == groupIdToMatch ) {
				// Found it!
				return seedGroupPosition;
			}

			hitLast = last == count - 1;
			hitFirst = first == 0;

			if( hitLast && hitFirst ) {
				// Looked at everything
				break;
			}

			if( hitFirst || ( next && ! hitLast ) ) {
				// Either we hit the top, or we are trying to move down
				last++;
				seedGroupPosition = last;
				// Try going up next time
				next = false;
			}
			else if( hitLast || ( ! next && ! hitFirst ) ) {
				// Either we hit the bottom, or we are trying to move up
				first--;
				seedGroupPosition = first;
				// Try going down next time
				next = true;
			}

		}

		return android.widget.AdapterView.INVALID_POSITION;
	}

	protected class MyDataSetObserver extends DataSetObserver {
		@Override
		public void onChanged() {
			refreshExpGroupMetadataList( true, true );

			notifyDataSetChanged();
		}

		@Override
		public void onInvalidated() {
			refreshExpGroupMetadataList( true, true );

			notifyDataSetInvalidated();
		}
	}

	/**
	 * Metadata about an expanded group to help convert from a flat list
	 * position to either a) group position for groups, or b) child position for
	 * children
	 */
	static class GroupMetadata implements Parcelable, Comparable<GroupMetadata> {
		final static int REFRESH = - 1;

		/** This group's flat list position */
		int flPos;
        
        /* firstChildFlPos isn't needed since it's (flPos + 1) */

		/**
		 * This group's last child's flat list position, so basically
		 * the range of this group in the flat list
		 */
		int lastChildFlPos;

		/**
		 * This group's group position
		 */
		int gPos;

		/**
		 * This group's id
		 */
		long gId;

		private GroupMetadata() {
		}

		static GroupMetadata obtain( int flPos, int lastChildFlPos, int gPos, long gId ) {
			GroupMetadata gm = new GroupMetadata();
			gm.flPos = flPos;
			gm.lastChildFlPos = lastChildFlPos;
			gm.gPos = gPos;
			gm.gId = gId;
			return gm;
		}

		public int compareTo( GroupMetadata another ) {
			if( another == null ) {
				throw new IllegalArgumentException();
			}

			return gPos - another.gPos;
		}

		public int describeContents() {
			return 0;
		}

		public void writeToParcel( Parcel dest, int flags ) {
			dest.writeInt( flPos );
			dest.writeInt( lastChildFlPos );
			dest.writeInt( gPos );
			dest.writeLong( gId );
		}

		public static final Creator<GroupMetadata> CREATOR = new Creator<GroupMetadata>() {

			public GroupMetadata createFromParcel( Parcel in ) {
				GroupMetadata gm = GroupMetadata.obtain( in.readInt(), in.readInt(), in.readInt(), in.readLong() );
				return gm;
			}

			public GroupMetadata[] newArray( int size ) {
				return new GroupMetadata[size];
			}
		};

	}

	/**
	 * Data type that contains an expandable list position (can refer to either a group
	 * or child) and some extra information regarding referred item (such as
	 * where to insert into the flat list, etc.)
	 */
	static public class PositionMetadata {

		private static final int MAX_POOL_SIZE = 5;
		private static ArrayList<PositionMetadata> sPool = new ArrayList<PositionMetadata>( MAX_POOL_SIZE );

		/** Data type to hold the position and its type (child/group) */
		public ExpandableHListPosition position;

		/**
		 * Link back to the expanded GroupMetadata for this group. Useful for
		 * removing the group from the list of expanded groups inside the
		 * connector when we collapse the group, and also as a check to see if
		 * the group was expanded or collapsed (this will be null if the group
		 * is collapsed since we don't keep that group's metadata)
		 */
		public GroupMetadata groupMetadata;

		/**
		 * For groups that are collapsed, we use this as the index (in
		 * mExpGroupMetadataList) to insert this group when we are expanding
		 * this group.
		 */
		public int groupInsertIndex;

		private void resetState() {
			if( position != null ) {
				position.recycle();
				position = null;
			}
			groupMetadata = null;
			groupInsertIndex = 0;
		}

		/**
		 * Use {@link #obtain(int, int, int, int, ExpandableHListConnector.GroupMetadata, int)}
		 */
		private PositionMetadata() {
		}

		static PositionMetadata obtain(
				int flatListPos, int type, int groupPos, int childPos, GroupMetadata groupMetadata, int groupInsertIndex ) {
			PositionMetadata pm = getRecycledOrCreate();
			pm.position = ExpandableHListPosition.obtain( type, groupPos, childPos, flatListPos );
			pm.groupMetadata = groupMetadata;
			pm.groupInsertIndex = groupInsertIndex;
			return pm;
		}

		private static PositionMetadata getRecycledOrCreate() {
			PositionMetadata pm;
			synchronized( sPool ) {
				if( sPool.size() > 0 ) {
					pm = sPool.remove( 0 );
				}
				else {
					return new PositionMetadata();
				}
			}
			pm.resetState();
			return pm;
		}

		public void recycle() {
			resetState();
			synchronized( sPool ) {
				if( sPool.size() < MAX_POOL_SIZE ) {
					sPool.add( this );
				}
			}
		}

		/**
		 * Checks whether the group referred to in this object is expanded,
		 * or not (at the time this object was created)
		 *
		 * @return whether the group at groupPos is expanded or not
		 */
		public boolean isExpanded() {
			return groupMetadata != null;
		}
	}
}
