package is.karlmenn.code;

import org.slf4j.*;

import com.webobjects.eoaccess.*;
import com.webobjects.eocontrol.*;
import com.webobjects.foundation.*;

import er.extensions.eof.*;
import er.extensions.foundation.ERXArrayUtilities;

/**
 * This class is for performang operations that need to be done on a lot of rows.
 * 
 * @author Hugi Þórðarson
 */

public class KMMassiveOperation {

	private static final Logger logger = LoggerFactory.getLogger( KMMassiveOperation.class );

	/**
	 * Applies an operation to EOs.
	 * 
	 * @param entityClass The class of the EO Entity to apply your action to.
	 * @param batchSize The number of objects to work with at a time. Defaults to 200 if no batch size is specified.
	 * @param handler Handler that is applied to each object.
	 * @param qualifier Qualifier , if you only want to apply this to a subset of the objects.
	 * 
	 * FIXME: Might need to save changes on the nested editing context's changes to the parent editing context on each batch.
	 */
	public static void start( EOEditingContext ec, Class entityClass, EOQualifier qualifier, Integer batchSize, Operation handler ) {

		KMStopWatch taskTimer = new KMStopWatch();

		if( batchSize == null )
			batchSize = 200;

		EOEditingContext nestedEC = leanEditingContext( ec );

		String entityName = entityClass.getSimpleName();
		EOEntity entity = EOModelGroup.defaultGroup().entityNamed( entityName );

		Integer totalNumberOfRows = ERXEOControlUtilities.objectCountWithQualifier( nestedEC, entityName, qualifier );

		EOFetchSpecification fs = new EOFetchSpecification( entityName, null, null );
		fs.setQualifier( qualifier );
		fs.setFetchesRawRows( true );
		fs.setRawRowKeyPaths( entity.primaryKeyAttributeNames() );

		logger.debug( stringWithFormat( "Fetching Raw rows for entity {}. Will apply action to {} objects.", entityName, totalNumberOfRows ) );
		NSArray<NSDictionary> allRawRows = nestedEC.objectsWithFetchSpecification( fs );
		NSArray<NSArray<NSDictionary>> rawRowBatches = ERXArrayUtilities.batchedArrayWithSize( allRawRows, batchSize );
		int batchCount = rawRowBatches.count();

		logger.debug( stringWithFormat( "Fetch and batching complete, created {} batches of {} objects each.", batchCount, batchSize ) );

		int batchNumber = 0;

		for( NSArray<NSDictionary> batch : rawRowBatches ) {
			batchNumber++;

			KMStopWatch batchTimer = new KMStopWatch();

			nestedEC = leanEditingContext( ec );

			NSArray<EOEnterpriseObject> eoBatch = convertRawRowsToEOs( nestedEC, entityName, batch );

			for( EOEnterpriseObject eo : eoBatch ) {
				handler.handleObject( eo );
			}

			// Only logging below here
			long elapsedTaskTime = taskTimer.elapsed();
			long avgBatchTime = elapsedTaskTime / batchNumber;
			long currentBatchTime = batchTimer.elapsed();
			long estRemainingTime = avgBatchTime * (batchCount - batchNumber);
			Object[] info = new Object[] { batchNumber, currentBatchTime, avgBatchTime, elapsedTaskTime, estRemainingTime };
			logger.debug( stringWithFormat( "Processed batch {} in {} ms (avg {} ms). Elapsed time {} ms, est. remaining time {} ms.", info ) );
		}

		logger.debug( "finished, total time was: " + taskTimer.elapsed() );
	}

	/**
	 * Creates an editing context with as little overhead as possible. 
	 */
	private static EOEditingContext leanEditingContext( EOEditingContext parent ) {
		EOEditingContext nestedEC = ERXEC.newEditingContext( parent );
		nestedEC.setUndoManager( null );
		EOEditingContext.setInstancesRetainRegisteredObjects( false );
		return nestedEC;
	}

	/**
	 * The interface that must be implemented by object handlers. 
	 */
	public static interface Operation {
		public void handleObject( Object object );
	}

	/**
	 * Replaces variable markers in originalString with the objects in the object 
	 */
	private static String stringWithFormat( String originalString, Object... objects ) {
		String VARIABLE = "\\{\\}";
		String returnString = originalString;

		for( int i = 0; i < objects.length; i++ ) {
			returnString = returnString.replaceFirst( VARIABLE, objects[i].toString() );
		}

		return returnString;
	}

	/**
	 * Converts an array of EOF raw rows to EOs.
	 * 
	 * @param ec The editingcontext to use
	 * @param entityName name of the entity to fetch an EO from
	 * @param rawRows an array of raw rows to convert to EOs.
	 */
	private static NSArray<EOEnterpriseObject> convertRawRowsToEOs( EOEditingContext ec, String entityName, NSArray<NSDictionary> rawRows ) {
		NSMutableArray allQualifier = new NSMutableArray();

		for( NSDictionary pkDictionary : rawRows ) {
			EOQualifier q = EOQualifier.qualifierToMatchAllValues( pkDictionary );
			allQualifier.addObject( q );
		}

		EOQualifier q = new EOOrQualifier( allQualifier );
		EOFetchSpecification fs = new EOFetchSpecification( entityName, q, null );
		NSArray eos = ec.objectsWithFetchSpecification( fs );
		return eos;
	}

	/**
	 * Convenience class to measure the time an operation takes. 
	 */
	public static class KMStopWatch {

		private long startTime;
		private long endTime;

		public KMStopWatch() {
			start();
		}

		public void start() {
			startTime = System.currentTimeMillis();
		}

		public void end() {
			endTime = System.currentTimeMillis();
		}

		public long elapsed() {
			return System.currentTimeMillis() - startTime;
		}
	}
}