/********************************************************************
 * Copyright © 2005 VHA. All rights reserved
 ********************************************************************/

package gov.va.med.fw.batchprocess;

import java.util.List;

import gov.va.med.fw.persistent.QueryIncrementTracker;
import gov.va.med.fw.persistent.QueryInfo;
import gov.va.med.fw.persistent.ScrollableCallback;
import gov.va.med.fw.util.InvalidConfigurationException;

/**
 * Specialization of AbstractDataQueryProcess that allows for record processing
 * in increments at a time, rather than the entire List of data at once.
 * 
 * <p>Queries that can return millions of records should not use this default incremental
 * approach as it will execute potentially costly query over and over, with each execution
 * becoming slower (depending on DAO implementation).  A custom approach that only
 * allows the JDBC Fetch size to manage the memory usage is preferred in that case.
 * 
 * <p>This assumes the querying of data is idempotent (at least until all increments
 * are completed).  In other words, processing of data in increment #1 will not affect
 * the ability to get increment #2 acquired data (entire query is run again and again for each
 * increment).  
 * 
 * <p>Non-idempotent querying of data must set idempotent flag to false and this assumes the
 * underlying JDBC driver supports database cursors.  If the driver does not, an IllegalStateException
 * will be thrown.  If that is thrown, a custom solution needs to be crafted or implementation processing
 * must change (eg, use a database temp table).
 * 
 * Created Feb 17, 2006 3:19:59 PM
 * 
 * @author DNS   BOHMEG
 */
public abstract class AbstractDataQueryIncrementalProcess extends
		AbstractDataQueryProcess implements ScrollableCallback {
	protected static int JDBC_FETCH_SIZE = 100;
	
	private int fetchSize = 100; // default	
	private boolean handleEachFetchSeparately = false;
	
	/**
	 * this only applies to the query re-execution - not the holistic data processing
	 */
	private boolean isIdempotent = true;  // make getter and setters as well

	// not marked final for framework overrides
	protected void executeProcess(DataProcessExecutionContext context) throws Exception {
		List acquiredData = null;
        
        DataQueryProcessExecutionContext queryContext = (DataQueryProcessExecutionContext) context;
        queryContext.setCurrentDataQuery(getQueryDetail());
        getQueryDetail().setIncremental(true);
        		
		while(! isInterrupted(context)) {
			acquiredData = acquireData(queryContext);
			if(acquiredData == null || acquiredData.isEmpty())
				break;
			processData(context, acquiredData);
			if(handleEachFetchSeparately)
				handleDataProcessCompleted(context);
		}
		if(isAllProcessingComplete() && !handleEachFetchSeparately)
			handleDataProcessCompleted(context);
	}
	
	protected final void processData(DataProcessExecutionContext context, List acquiredData) {
		if(acquiredData == null || acquiredData.isEmpty())
			return;
        doProcessData(context, acquiredData);
	}
	
    /**
     * Calls processData and ensures that the job result is updated.
     * 
     */
    protected final void doProcessData(DataProcessExecutionContext context, List acquiredData) {
        DataQueryProcessExecutionContext queryContext = (DataQueryProcessExecutionContext) context; 
        processData(queryContext, acquiredData);
        if(shouldUpdateJobResult(queryContext))
            this.updateJobResult(queryContext);
    }
    
	/**
	 * To be overriden for classes invoking concurrent processes 
	 * @return
	 */
	
	protected boolean isAllProcessingComplete()
	{
	    return true;
	}
	
	protected abstract void processData(DataQueryProcessExecutionContext context, List acquiredData);
	
	public void handleScrolledData(QueryIncrementTracker tracker) {
		doProcessData((DataQueryProcessExecutionContext) tracker, tracker.getIncrementalData());
	}
	
	public boolean continueScrolling(QueryIncrementTracker tracker) {
		return !isInterrupted((DataProcessExecutionContext) tracker);
	}
	
	protected final List acquireData(DataQueryProcessExecutionContext context)
			throws Exception {
		int currentRecord = context.getCurrentRecord();
		
		List acquiredData = doAcquireData(context);
		if(acquiredData != null) {
			context.setCurrentRecord(currentRecord + acquiredData.size());		
			// let this happen indirectly by implementation use of incrementNumberOf*Records
			//context.getProcessStatistics().setNumberOfTotalRecords(context.getCurrentRecord());
			if(logger.isInfoEnabled() && acquiredData != null)
				logger.info("AbstractDataQueryIncrementalProcess acquired " + acquiredData.size() + " data records");
		}		
		return acquiredData;
	}
	
	// subclasses can override (eg, use executionArgs in context to filter data even more)
	protected List doAcquireData(DataQueryProcessExecutionContext context) throws Exception {
		List acquiredData = null;
		if(isIdempotent) {
			acquiredData = executeQuery(context);		
		} else {
			QueryInfo query = context.getCurrentDataQuery().getQuery();
			query.setFetchSize(fetchSize); // this process wins out on defining the fetchSize
			getDao().scroll(this, query, context);
		}
		return acquiredData;
	}
    
    // subclasses can override
    protected List executeQuery(DataQueryProcessExecutionContext context) throws Exception {
        DataQueryDetail currentQuery = context.getCurrentDataQuery();
        return getDao().find(currentQuery.getQuery().getQuery(), currentQuery.getQuery().getParamNames(),
        		currentQuery.getQuery().getParamValues(), context.getCurrentRecord(), fetchSize,
                    calculateJDBCFetchSize());            
    }    
    
        
    protected int calculateJDBCFetchSize() {
        return fetchSize > JDBC_FETCH_SIZE ? fetchSize : JDBC_FETCH_SIZE;
    }


	/**
	 * @return Returns the fetchSize.
	 */
	public int getFetchSize() {
		return fetchSize;
	}

	/**
	 * @param fetchSize The fetchSize to set.
	 */
	public void setFetchSize(int fetchSize) {
		this.fetchSize = fetchSize;
	}
	
	public void afterPropertiesSet() {
		super.afterPropertiesSet();
		if(fetchSize <= 0)
			throw new InvalidConfigurationException("fetchSize must be positive");
	}

	/**
	 * @return Returns the handleEachFetchSeparately.
	 */
	public boolean isHandleEachFetchSeparately() {
		return handleEachFetchSeparately;
	}

	/**
	 * @param handleEachFetchSeparately The handleEachFetchSeparately to set.
	 */
	public void setHandleEachFetchSeparately(boolean handleEachFetchSeparately) {
		this.handleEachFetchSeparately = handleEachFetchSeparately;
	}

	
	/**
	 * @return Returns the isIdempotent.
	 */
	public boolean isIdempotent() {
		return isIdempotent;
	}

	
	/**
	 * @param isIdempotent The isIdempotent to set.
	 */
	public void setIdempotent(boolean isIdempotent) {
		this.isIdempotent = isIdempotent;
	}

	protected boolean shouldUpdateJobResult(DataQueryProcessExecutionContext context) {
		return true;
	}	
}
