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

package gov.va.med.esr.common.batchprocess.datasync;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.lang.Validate;

import gov.va.med.esr.common.model.person.PersonChangeLogEntry;
import gov.va.med.esr.common.model.person.id.PersonEntityKey;
import gov.va.med.esr.service.PersonService;
import gov.va.med.fw.batchprocess.AbstractDataQueriesProcess;
import gov.va.med.fw.batchprocess.DataProcessExecutionContext;
import gov.va.med.fw.batchprocess.DataQueryProcessExecutionContext;
import gov.va.med.fw.io.writer.FormattedFileWriter;
import gov.va.med.fw.service.ServiceException;

/**
 * Abstract implementation of AbstractDataQueryIncrementalProcess for Data
 * Synchronization needs.
 *  
 * Created Mar 6, 2006 3:04:36 PM
 * 
 * 
 * @author DNS   BOHMEG
 */
public abstract class AbstractDataSynchronizationProducerProcess extends AbstractDataQueriesProcess {
	public static String CONTEXT_ACQUIRED_OBJECTS = "acquiredObjects";
	public static String CONTEXT_FILE_STATS = "fileStats";
	
	private PersonService personService;

	private FormattedFileWriter fileWriter;
	private int entityCountPerFile;
	private int lineCountPerFile;
	private boolean blankLineAtEndOfFile = true;

	protected  void processData(DataQueryProcessExecutionContext context, List acquiredData) {
		Iterator itr = acquiredData.iterator();
		Object acquiredObject = null;
		Object processedObject = null;
		Object transformedData = null;
		List data = new ArrayList();
		
		while (itr.hasNext()) {
			acquiredObject = itr.next();		
			try {				
                if(!shouldProcessAcquiredObject(context, acquiredObject))
                    continue;
                
				processedObject = getProcessedObject(context, acquiredObject);
				if(processedObject == null) {
					if(logger.isWarnEnabled())
						logger.warn("Unable to retrieve processed object for data sync...it was retrieved as null....ignoring this entry");
					continue;
				}
				
				// note that one entity could be transformed into multiple objects
				transformedData = transformObject(context, processedObject, acquiredObject);
                writeToFile(context, transformedData, data, processedObject,acquiredObject );


                
			} catch (Exception e) {
                context.getExceptionData().add(e);
				context.getProcessStatistics().incrementNumberOfErrorRecords();
                data.clear();
				// do not propagate out just for a few erroneous records
				if (logger.isErrorEnabled())
					logger.error("Unable to synchronize Entity: " + acquiredObject
							+ " due to exception", e);
			}
			
			// inside this loop, update JobResult
			if(context.getProcessStatistics().isTotalNumberMod(DEFAULT_JOB_RESULT_UPDATE_INTERVAL))
				updateJobResult(context);
		}
	}

	protected void changeFileExtensions(String fromExtn, String toExtn){
    	try {
			fileWriter.changeFileExtensions(fromExtn, toExtn);
		} catch (IOException e) {
			if (logger.isErrorEnabled()){
				logger.error("Exception occured while convertin the files extension from "+fromExtn+" to "+toExtn, e);
			}
		}
    }
	
	protected void writeToFile(DataQueryProcessExecutionContext context,
                                Object transformedData, List data, 
                                Object processedObject, Object acquiredObject) {
        int transformedSize = 1;
        if (transformedData instanceof Collection) {
            Collection coll = (Collection) transformedData;
            transformedSize = coll.size();
            data.addAll(coll);
        } else {
            data.add(transformedData);
        }

        // flush data to file
        if (shouldCreateNewFile(context, transformedSize)) {
            // before creating new file, deal with old file info
            processEndOfFile(context);

            DataSynchronizationProducerStatistics stats = getFileStats(context);
            stats = createNewFileStats(context);
            fileWriter.writeData(data, stats.getStartTime());

        } else {
            if (!blankLineAtEndOfFile) {
                /*
                 * this is needed since there is no carriage return being done
                 * after each write. there is no carriage return after each
                 * write because that would cause a blank line to be added at
                 * the end of the file and subclasses (eg, for IVM and its silly
                 * IVM consuming application) can not handle that (and will not
                 * change to do so since that code also consumes files from
                 * HECLegacy). ugh.
                 */
                if (context.getProcessStatistics()
                        .getNumberOfSuccessfulRecords() > 0)
                    fileWriter.println(getFileStats(context).getStartTime());
            }
            fileWriter.appendData(data, getFileStats(context).getStartTime());
        }

        getFileStats(context).incrementEntityCount();
        addFileData(context, getFileStats(context), processedObject,
                acquiredObject);
        getFileStats(context).incrementObjectCount(transformedSize);

        context.getProcessStatistics().incrementNumberOfSuccessfulRecords();
        data.clear();

        postProcessAcquiredObject(context, getFileStats(context),
                processedObject, acquiredObject);

    }
    

    
    /**
     * @see gov.va.med.fw.batchprocess.AbstractDataProcess#handleDataProcessCompleted(gov.va.med.fw.batchprocess.DataProcessExecutionContext)
     */
    protected void handleDataProcessCompleted(DataProcessExecutionContext context) {
        super.handleDataProcessCompleted(context);
        processEndOfFile((DataQueryProcessExecutionContext) context);
    }

    private void processEndOfFile(DataQueryProcessExecutionContext context) {
        DataSynchronizationProducerStatistics stats = getFileStats(context);
        if (stats != null) {
            stats.setEndTime(new Date());
            postProcessDataFile(context, stats);
        }
    }

    protected void addFileData(DataQueryProcessExecutionContext context, DataSynchronizationProducerStatistics stats, Object processedObject, Object acquiredObject) {
        stats.addFileData(acquiredObject);
    }
	
	private DataSynchronizationProducerStatistics getFileStats(DataQueryProcessExecutionContext context) {				
		DataSynchronizationProducerStatistics stats = (DataSynchronizationProducerStatistics) context.getContextData().get(CONTEXT_FILE_STATS);
		if(stats == null) {
			stats = createNewFileStats(context);
		}
		return stats;	
	}
	
	private DataSynchronizationProducerStatistics createNewFileStats(DataQueryProcessExecutionContext context) {
		DataSynchronizationProducerStatistics stats = new DataSynchronizationProducerStatistics();
		Date startTime = new Date();
		stats.setStartTime(startTime);
		stats.setFileName(fileWriter.getFileSystemResource(startTime).getFilename());
		setFileStats(context, stats);
		return stats;
	}

	private void setFileStats(DataQueryProcessExecutionContext context, DataSynchronizationProducerStatistics stats) {
		context.getContextData().put(CONTEXT_FILE_STATS, stats);		
	}
	
	private boolean shouldCreateNewFile(DataQueryProcessExecutionContext context, int transformedSize) {
		boolean result = false;
		DataSynchronizationProducerStatistics stats = getFileStats(context);
		if(stats == null) {
			result = true;
		} else {
			// check counts
			if(entityCountPerFile != 0 && (stats.getEntityCount() + 1) > entityCountPerFile)
				result = true;
			else if(lineCountPerFile != 0 && (stats.getObjectCount() + transformedSize) > lineCountPerFile)
				result = true;			
		}		
		return result;
	}

    /**
     * This method is called at the end of each data file writing and before the new data file.
     * @param context
     * @param stats
     */
    protected void postProcessDataFile(DataQueryProcessExecutionContext context, DataSynchronizationProducerStatistics stats) {
        // default empty implementation
    }
    
    /**
     * This method after each acquired object is processed.
     * 
     * @param context
     * @param stats
     */
    protected void postProcessAcquiredObject(DataQueryProcessExecutionContext context, DataSynchronizationProducerStatistics stats, Object processedObject, Object acquiredObject) {
        // default empty implementation
    }
    
    
	/**
	 * Default implementation is to only sync one set of data per PersonEntityKey
	 * 
	 * @param context
	 * @param acquiredObject
	 * @return
	 */
	protected boolean shouldProcessAcquiredObject(DataQueryProcessExecutionContext context, Object acquiredObject) {
		PersonChangeLogEntry log = (PersonChangeLogEntry) acquiredObject;
		PersonEntityKey key = log.getPersonEntityKey();
		Map keysVisited = (Map) context.getContextData().get(CONTEXT_ACQUIRED_OBJECTS);
		if(keysVisited == null) {
			keysVisited = new HashMap();
			context.getContextData().put(CONTEXT_ACQUIRED_OBJECTS, keysVisited);
		}
		boolean shouldProcessAcquiredObject = true;
		if(keysVisited.containsKey(key))
			shouldProcessAcquiredObject = false;
		else
			keysVisited.put(key, null);
		return shouldProcessAcquiredObject;
	}
	
	// could be overriden by subclasses
	protected Object getProcessedObject(DataQueryProcessExecutionContext context, Object acquiredObject) throws ServiceException {
		// retrieve Person
		PersonChangeLogEntry log = (PersonChangeLogEntry) acquiredObject;
		PersonEntityKey personKey = log.getPersonEntityKey();
		if(logger.isDebugEnabled())
			logger.debug("Synchronizing data for Person: " + personKey.getKeyValueAsString());
		
		return personService.getPerson(personKey);
	}
	
	/**
	 * 
	 * @param context TODO
	 * @param processedObject The object that was retrieved during the processData
	 * @param acquiredObject The object that was acquired during the acquireData
	 * @return
	 * @throws ServiceException
	 */
	protected abstract Object transformObject(DataQueryProcessExecutionContext context, Object processedObject, Object acquiredObject) throws ServiceException;

	/**
	 * @return Returns the personService.
	 */
	public PersonService getPersonService() {
		return personService;
	}

	/**
	 * @param personService
	 *            The personService to set.
	 */
	public void setPersonService(PersonService personService) {
		this.personService = personService;
	}

	public void afterPropertiesSet() {
		super.afterPropertiesSet();
		Validate.notNull(personService, "personService must be non-null");
		Validate.notNull(fileWriter, "fileWriter must be non-null");
		
		/* just in case FormattedFileWriter was not configured correctly */
		if(!blankLineAtEndOfFile)
			this.fileWriter.setExtraCarriageReturnAfterEachDataWrite(false); 
	}

	/**
	 * @return Returns the fileWriter.
	 */
	public FormattedFileWriter getFileWriter() {
		return fileWriter;
	}

	/**
	 * @param fileWriter
	 *            The fileWriter to set.
	 */
	public void setFileWriter(FormattedFileWriter fileWriter) {
		this.fileWriter = fileWriter;
	}

	/**
	 * @return Returns the lineCountPerFile.
	 */
	public int getLineCountPerFile() {
		return lineCountPerFile;
	}

	/**
	 * @param lineCountPerFile The lineCountPerFile to set.
	 */
	public void setLineCountPerFile(int lineCountPerFile) {
		this.lineCountPerFile = lineCountPerFile;
	}

	/**
	 * @return Returns the entityCountPerFile.
	 */
	public int getEntityCountPerFile() {
		return entityCountPerFile;
	}

	/**
	 * @param entityCountPerFile The entityCountPerFile to set.
	 */
	public void setEntityCountPerFile(int entityCountPerFile) {
		this.entityCountPerFile = entityCountPerFile;
	}

	/**
	 * @return the blankLineAtEndOfFile
	 */
	public boolean isBlankLineAtEndOfFile() {
		return blankLineAtEndOfFile;
	}

	/**
	 * @param blankLineAtEndOfFile the blankLineAtEndOfFile to set
	 */
	public void setBlankLineAtEndOfFile(boolean blankLineAtEndOfFile) {
		this.blankLineAtEndOfFile = blankLineAtEndOfFile;
	}
}
