/*
 * 	
 *
 * Portions Copyright  2003-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 *
 * Copyright 2000 Motorola, Inc. All Rights Reserved.
 * This notice does not imply publication.
 */

package javax.microedition.rms;

import com.sun.midp.log.Logging;
import com.sun.midp.log.LogChannels;

/**
 * This class implements the RecordEnumeration interface.
 */
class RecordEnumerationImpl implements RecordEnumeration, RecordListener { 

    /** The associated record store for this enumeration */
    private RecordStore recordStore;

    /** The record filter this enumeration should use, or null if none */
    private RecordFilter filter;

    /** The record comparator this enumeration should use, or null if none */
    private RecordComparator comparator;

    /** True if this should listen to <code>recordStore</code> for changes */
    private boolean keepEnumUpdated;  // false by default

    /** Current pos within the enumeration */
    private int index;  // NO_SUCH_RECORD by default

    /** Array of recordId's of records included in the enumeration */
    private int[] records;

    /**
     * A constant recordId indicating the splice point between the
     * last and first records in the enumeration. Returned by
     * <code>nextElement()</code> and <code>prevElement()</code>
     * when the next or prev element does not exist.
     */
    private static final int NO_SUCH_RECORD = -1;


    /**
     * Builds an enumeration to traverse a set of records in the
     * given record store in an optionally specified order.<p>
     *
     * The filter, if non-null, will be used to determine what
     * subset of the record store records will be used.<p>
     *
     * The comparator, if non-null, will be used to determine the
     * order in which the records are returned.<p>
     *
     * If both the filter and comparator are null, the enumeration
     * will traverse all records in the record store in an undefined
     * order. This is the most efficient way to traverse all of the
     * records in a record store.
     *
     * @param inp_recordStore the RecordStore to enumerate.
     * @param inp_filter if non-null, will be used to determine what
     *        subset of the record store records will be used.
     * @param inp_comparator if non-null, will be used to determine the
     *        order in which the records are returned.
     * @param keepUpdated if true, the enumerator will keep its enumeration
     *        current with any changes in the records of the record store. 
     *        Use with caution as there are performance consequences.
     *
     * @see #rebuild
     */
    RecordEnumerationImpl(RecordStore inp_recordStore, 
                          RecordFilter inp_filter,
			  RecordComparator inp_comparator, 
			  boolean keepUpdated) {
	recordStore = inp_recordStore;
	filter = inp_filter;
	comparator = inp_comparator;
	keepEnumUpdated = keepUpdated;

	if (keepUpdated) {
	    inp_recordStore.addRecordListener(this);
	}

	rebuild();
    }


    /**
     * Returns the number of records available in this enumeration's
     * set. That is, the number of records that have matched the
     * filter criterion. Note that this forces the RecordEnumeration
     * to fully build the enumeration by applying the filter to all
     * records, which may take a non-trivial amount
     * of time if there are a lot of records in the record store.
     *
     * @return the number of records available in this enumeration's
     *         set. That is, the number of records that have matched 
     *         the filter criterion.
     */
    public synchronized int numRecords() {
	checkDestroyed();

	return records.length;
    }


    /**
     * Returns a copy of the <i>next</i> record in this enumeration,
     * where <i>next</i> is defined by the comparator and/or filter
     * supplied in the constructor of this enumerator. The byte array
     * returned is a copy of the record. Any changes made to this array
     * will NOT be reflected in the record store. After calling
     * this method, the enumeration is advanced to the next available
     * record.
     *
     * @exception InvalidRecordIDException no more records are available
     *
     * @return the next record in this enumeration.
     */
    public synchronized byte[] nextRecord() throws InvalidRecordIDException, 
	RecordStoreNotOpenException, RecordStoreException {
	checkDestroyed();
	return recordStore.getRecord(nextRecordId());
    }

    /**
     * Returns the recordId of the <i>next</i> record in this enumeration,
     * where <i>next</i> is defined by the comparator and/or filter
     * supplied in the constructor of this enumerator. After calling
     * this method, the enumeration is advanced to the next available
     * record.
     *
     * @exception InvalidRecordIDException no more records are available.
     *
     * @return the recordId of the next record in this enumeration.
     */
    public synchronized int nextRecordId()
	throws InvalidRecordIDException {
	checkDestroyed();
	if (index == records.length - 1) {
	    throw new InvalidRecordIDException();
	}
	if (index == NO_SUCH_RECORD) {
	    index = 0;
	} else {
	    index++;
	}
	return records[index];
    }


    /**
     * Returns a copy of the <i>previous</i> record in this enumeration,
     * where <i>previous</i> is defined by the comparator and/or filter
     * supplied in the constructor of this enumerator. The byte array
     * returned is a copy of the record. Any changes made to this array
     * will NOT be reflected in the record store. After calling
     * this method, the enumeration is advanced to the next (previous)
     * available record.
     *
     * @exception InvalidRecordIDException no more records are available.
     *
     * @return the previous record in this enumeration.
     */
    public synchronized byte[] previousRecord() throws 
        InvalidRecordIDException, RecordStoreNotOpenException, 
	RecordStoreException {

	checkDestroyed();
        return recordStore.getRecord(previousRecordId());
    }


    /**
     * Returns the recordId of the <i>previous</i> record in this enumeration,
     * where <i>previous</i> is defined by the comparator and/or filter
     * supplied in the constructor of this enumerator. After this method
     * is called, the enumeration is advanced to the next (previous)
     * available record.
     *
     * @exception InvalidRecordIDException when no more records are available.
     *
     * @return the recordId of the previous record in this enumeration.
     */
    public synchronized int previousRecordId()
	throws InvalidRecordIDException {
	checkDestroyed();
	if (index == 0 || records.length == 0) {
	    throw new InvalidRecordIDException();
	}
	if (index == NO_SUCH_RECORD) {
	    index = records.length - 1;
	} else {
	    index--;
	}
	return records[index];
    }


    /**
     * Returns true if more elements exist in the <i>next</i> direction.
     *
     * @return true if more elements exist in the <i>next</i> direction.
     */
    public boolean hasNextElement() {
	checkDestroyed();
	if (records.length == 0 || !recordStore.isOpen()) {
	    return false;
	}
	return (index != records.length - 1);
    }


    /**
     * Returns true if more elements exist in the <i>previous</i> direction.
     *
     * @return true if more elements exist in the <i>previous</i> direction.
     */
    public boolean hasPreviousElement() {
	checkDestroyed();
	if (records.length == 0 || !recordStore.isOpen()) {
	    return false;  // no records in the enumeration
	}
	return (index != 0);
    }


    /**
     * Returns the index point of the enumeration to the beginning.
     */
    public void reset() {
	checkDestroyed();
	index = NO_SUCH_RECORD;
    }


    /**
     * Request that the enumeration be updated to reflect the current
     * record set. Useful for when an application makes a number of 
     * changes to the record store, and then wants an existing 
     * RecordEnumeration to enumerate the new changes.
     *
     * @see #keepUpdated
     */
    public void rebuild() {
	checkDestroyed();

	int[] tmp = recordStore.getRecordIDs();
	reFilterSort(tmp);
    }


    /**
     * Used to set whether the enumeration should be registered
     * as a listener of the record store, and rebuild its internal
     * index with every record addition/deletion in the record store.
     * Note that this should be used carefully due to the potential 
     * performance cost associated with maintaining the 
     * enumeration with every change.
     *
     * @param keepUpdated if true, the enumerator will keep its enumeration
     *        current with any changes in the records of the record store.
     *        Use with caution as there are possible performance consequences.
     *        If false, the enumeration will not be kept current and may 
     *        return recordIds for records that have been deleted or miss 
     *        records that are added later. It may also return records out
     *        of order that have been modified after the enumeration was 
     *        built.
     *
     * @see #rebuild
     */
    public void keepUpdated(boolean keepUpdated) {
	checkDestroyed();
	if (keepUpdated != keepEnumUpdated) {
	    keepEnumUpdated = keepUpdated;
	    if (keepUpdated) {
	        recordStore.addRecordListener(this);
		rebuild();
	    } else {
	        recordStore.removeRecordListener(this);
	    }
	}
    }


    /**
     * Returns true if the enumeration keeps its enumeration
     * current with any changes in the records.
     *
     * @return true if the enumeration keeps its enumeration
     *         current with any changes in the records
     */
    public boolean isKeptUpdated() {
	checkDestroyed();
	return keepEnumUpdated;
    }


    /**
     * From the RecordListener interface.  This method is called if
     * a record is added to <code>recordStore</code>.
     *
     * @param inp_recordStore the record store to which a record was added
     * @param recordId the record ID of the new record
     */
    public synchronized void recordAdded(RecordStore inp_recordStore, 
					 int recordId) {
	checkDestroyed();
        filterAdd(recordId);
    }

    
    /**
     * From the RecordListener interface.  This method is called if
     * a record in <code>recordStore</code> is modified.
     *
     * @param inp_recordStore the record store in which a record was modified
     * @param recordId the record ID of the modified record.
     */
    public synchronized void recordChanged(RecordStore inp_recordStore, 
					       int recordId) {
	checkDestroyed();
	
	int recIndex = findIndexOfRecord(recordId);
	if (recIndex >= 0) {
	    removeRecordAtIndex(recIndex);
	} // else record not previously in the enumeration
	
        filterAdd(recordId);
    }
    

    /**
     * From the RecordListener interface.  This method is called when a
     * record in <code>recordStore</code> is deleted.
     *
     * @param inp_recordStore the record store from which a record was deleted
     * @param recordId the record id of the deleted record
     */
    public synchronized void recordDeleted(RecordStore inp_recordStore, 
					   int recordId) {
	checkDestroyed();

	/*
	 * Remove the deleted element from the records array.
	 * No resorting is required.
	 */

	int recIndex = findIndexOfRecord(recordId);
	
	if (recIndex < 0) {
	    return;  // not in the enumeration
	}

	// remove this record from the enumeration
	removeRecordAtIndex(recIndex);    
    }


    /**
     * Implements RecordEnumeration.destroy() interface.  Called
     * to signal that this enumeration will no longer be used, and that
     * its resources may be collected.
     */ 
    public synchronized void destroy() {
	checkDestroyed();
	if (keepEnumUpdated) {
	    recordStore.removeRecordListener(this);
	}

	filter = null;
	comparator = null;
	records = null;
	recordStore = null; // a signal that this is destroyed!
    }


    /**
     * Helper method that checks if this enumeration can be used.
     * If this enumeration has been destroyed, an exception is thrown.
     *
     * @exception IllegalStateException if RecordEnumeration has been 
     *            destroyed.
     */
    private void checkDestroyed() {
	if (recordStore == null) {
	    throw new IllegalStateException();
	}
    }

    /**
     * Used to add a record to an already filtered and sorted
     * <code>records</code> array.  More efficient than 
     * <code>reFilterSort</code> because it relies on 
     * <code>records</code> being in sorted order.
     *
     * First ensures that record <code>recordId</code> 
     * meets this enumeration's filter criteria.
     * If it does it is added to records as array element
     * 0.  If a comparator is defined for this enumeration,
     * the helper method <code>sortAdd</code> is called to 
     * properly position <code>recordId</code> within the ordered
     * <code>records</code> array.
     *
     * Should be called from within a 
     * synchronized (recordStore.rsLock) block.
     *
     * @param recordId the record to add to this enumeration
     */
    private void filterAdd(int recordId) {
	int insertPoint = -1;
	if (filter != null) {
	    try {
		if (!filter.matches(recordStore.getRecord(recordId))) {
		    if (Logging.REPORT_LEVEL <= Logging.WARNING) {
			Logging.report(Logging.WARNING, LogChannels.LC_RMS,
				       "Unexpected case in filterAdd: " + 
				       "recordId filtered out");
		    }
		    return;  // recordId filtered out
		}
	    } catch (RecordStoreException rse) {
		return;  // recordId does not exist
	    }
	}

	// the new record has been accepted by the filter
	int[] newrecs = new int[records.length + 1];
	newrecs[0] = recordId;  // insert new record at front of list
	System.arraycopy(records, 0, newrecs, 1, records.length);
	records = newrecs;
	if (comparator != null) {  // move the new record into place
	    try {
		insertPoint = sortInsert();
	    } catch (RecordStoreException rse) {
		// NOTE: - should never be here
		// throw a RSE?  destroy record enumeration?
		if (Logging.TRACE_ENABLED) {
		    Logging.trace(rse, "Unexpected case in filterAdd: " + 
				  "caught RSE");
		}
	    }
	}
	// keep index up to date as well
	if (index != NO_SUCH_RECORD && insertPoint <= index) {
	    index++;
	}
    }


    /**
     * Helper method called by <code>filterAdd</code>.
     * Moves the possibly unsorted element zero in the
     * <code>records</code> array to its sorted position
     * within the array.
     *
     * @return index of inserted element.
     * @exception RecordStoreException if an error occurs
     *            in the comparator function.
     */
    private int sortInsert() throws RecordStoreException {
	// bubble sort the first record in records into place
	int tmp;
	int i;
	int j;
	for (i = 0, j = 1; i < records.length - 1; i++, j++) {
	    if (comparator.compare(recordStore.getRecord(records[i]),
				   recordStore.getRecord(records[j])) ==
		RecordComparator.FOLLOWS) {
		// if i follows j swap them in records
		tmp = records[i];
		records[i] = records[j];
		records[j] = tmp;
	    } else {
		break;  // done sorting if compare returns EQUALS or PRECEDES 
	    }
	}
	return i; // final index of new record in records
    }
    
    
    /**
     * Find the index in records of record <code>recordId</code> 
     * and return it.  
     *
     * @param recordId the record index to find
     * @return the index of the record, or -1.
     */
    private int findIndexOfRecord(int recordId) {
	int idx;
	int recIndex = -1;
	for (idx = records.length - 1; idx >= 0; idx--) {
	    if (records[idx] == recordId) {
		recIndex = idx;
		break;
	    }
	} 
	return recIndex;
    }


    /**
     * Internal helper method which 
     * removes the array element at index <code>recIndex</code>
     * from the internal <code>records</code> array.  
     *
     * <code>recIndex</code> should be non negative.
     *
     * @param recIndex the array element to remove.
     */
    private void removeRecordAtIndex(int recIndex) {
	int[] tmp = new int[records.length - 1];
	System.arraycopy(records, 0, tmp, 0, recIndex);
	System.arraycopy(records, recIndex + 1, tmp, 
			 recIndex, (records.length - recIndex) - 1);
	records = tmp;
	
	/* 
	 * If a record prior to current index was deleted
	 * update index so nothing is skipped
	 */
	if (index != NO_SUCH_RECORD && recIndex <= index) {
	    index --;
	} else if (index == records.length) {
	    // last element in records removed
	    index --;
	}
    }


    /**
     * Internal helper method for filtering and sorting records if
     * necessary. Called from rebuild().
     *
     * Should be called from within a synchronized(recordStore.rsLock) block
     *
     * @param filtered array of record stores to filter and sort.
     */ 
    private void reFilterSort(int[] filtered) {
	int filteredIndex = 0;
	if (filter == null) {
	    /*
	     * If this enumeration doesn't have any filters, the
	     * recordId's returned by getRecordIDs should be 
	     * used as they are.
	     */
	    records = filtered;
	} else {
	    /*
	     * If a filter has been specified, filter the recordStore
	     * records to determine the subset to be used for this
	     * enumeration.
	     */
	    for (int i = 0; i < filtered.length; i++) {
		// if this record matches the filter keep it
		try {
		    if (filter.matches(recordStore.getRecord(filtered[i]))) {
			// need revisit : if element overlap is allowed
			if (filteredIndex != i) {
			    filtered[filteredIndex++] = filtered[i];
			} else {
			    filteredIndex++;
			}
		    }
		} catch (RecordStoreException rse) {
		    // if a record can't be found it doesn't match
		}
	    }
	    
	    records = new int[filteredIndex];
	    System.arraycopy(filtered, 0, records, 0, filteredIndex);
	}
	/*
	 * If a comparator has been specified, sort the remaining
	 * records by comparing records against each other using
	 * the comparator the application provides.
	 */
	if (comparator != null) {
	    try {
		QuickSort(records, 0, records.length - 1, comparator);
	    }
	    catch (RecordStoreException rse) {
		// NOTE: - should never be here
		// throw a RSE?  destroy record enumeration?

		if (Logging.TRACE_ENABLED) {
		    Logging.trace(rse, "Unexpected case in reFilterSort:" + 
				  " caught RSE");
		}
	    }
	}
	reset(); // reset the current index of this enumeration
    }
    
    
    /**
     * Quicksort helper function for sorting the records.
     *
     * @param a the array of recordId's to sort using comparator.
     * @param lowIndex the low bound of the range to sort.
     * @param highIndex the hight bound of the range to sort.
     * @param inp_comparator the RecordComparator to use to compare records.
     */
    private void QuickSort(int a[], int lowIndex, int highIndex, 
			   RecordComparator inp_comparator)
	throws RecordStoreException {

	/*
	 * A different sorting algorithm may be preferred, because a
	 * large recursive quicksort can consume lots of
	 * stack. Quicksort is very fast for most random sequences
	 * however...
	 */
	int left = lowIndex;	// the "left" index
	int right = highIndex;	// the "right" index

	/*
	 * First partition the data into two regions, where every
	 * element on the left side of the partition is less than
	 * every element on the right side of the element.
	 */
	if (highIndex > lowIndex) {
	    /* 
	     * Arbitrarily choose the initial pivot point to be the
	     * middle of the array.
	     */
	    int ind = (lowIndex + highIndex) / 2;
	    int pivotIndex = a[ind];
	    byte[] pivotData = recordStore.getRecord(pivotIndex);
	    
	    // loop through the array until the indices cross
	    while (left <= right) {
		/* 
		 * Starting on the left, scan right until the
		 * first element greater than or equal to the
		 * pivot element is found.
		 */
		while ((left < highIndex) && 
		       (inp_comparator.compare(recordStore.getRecord(a[left]),
					   pivotData) ==
			RecordComparator.PRECEDES)) {
		    left++;
		}
		/*
		 * Starting on the right, scan left until the
		 * first element that is less than or equal to the
		 * pivot element is found.
		 */
		while ((right > lowIndex) && 
		      (inp_comparator.compare(recordStore.getRecord(a[right]),
					  pivotData) ==
		       RecordComparator.FOLLOWS)) {
		    right--;
		}

		// if the indexes haven't crossed, swap the elements
		if (left <= right) {
		    int tmp = a[left];
		    a[left] = a[right];
		    a[right] = tmp;
		    left++;
		    right--;
		}
	    }
	    
	    // Sort the left side of the partition
	    if (lowIndex < right) {
		QuickSort(a, lowIndex, right, inp_comparator);
	    }
	    // Sort the right side of the partition
	    if (left < highIndex) {
		QuickSort(a, left, highIndex, inp_comparator);
	    }
	}
    }
}
