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

package gov.va.med.fw.io.parser;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.ConvertUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.MapIterator;
import org.apache.commons.collections.OrderedMap;
import org.apache.commons.collections.map.ListOrderedMap;
import org.springframework.core.io.FileSystemResource;

import com.infomata.data.DataFile;
import com.infomata.data.DataRow;

import gov.va.med.fw.batchprocess.DataFileProcessExecutionContext;
import gov.va.med.fw.io.ClassMapping;
import gov.va.med.fw.io.DelimitedFormat;
import gov.va.med.fw.io.FileFieldMetaData;
import gov.va.med.fw.io.FixedWidthFormat;
import gov.va.med.fw.io.RawFileDataContainer;
import gov.va.med.fw.io.RawFileDataContainerUtils;
import gov.va.med.fw.io.SimpleDelimiterFormat;
import gov.va.med.fw.model.lookup.Lookup;
import gov.va.med.fw.util.InvalidConfigurationException;
import gov.va.med.fw.util.StringUtils;
import gov.va.med.fw.util.builder.BuilderException;

/**
 * Builder implementation that hydrates objects by parsing a formatted input
 * file. Allows for more than one ClassMapping to be used for a given file,
 * triggered via a sectionHeader (eg, #Apples, #Oranges, etc.). In that case,
 * objects are created and added to the returned List in their order of
 * visitation.
 * 
 * <p>
 * The same InputDataFormat applies to each row, regardless of ClassMapping.
 * 
 * <p>
 * If no InputDataFormat is specified, default delimiter will be assumed.
 * 
 * Created Feb 15, 2006 2:16:54 PM
 * 
 * @author DNS   BOHMEG
 */
public class FormattedFileParserBuilder extends AbstractFileParserBuilder {

	/**
	 * serialVersionUID long
	 */
	private static final long serialVersionUID = 6896484962490107620L;

	private static String DEFAULT_DELIMITER = "^";

	private Map classMappings = new HashMap();

	private String delimiter;

	private InputDataFormat inputDataFormat;

	private ClassMapping classMapping;

	private boolean failOnUnmappedData = true;

	/*
	 * (non-Javadoc)
	 * 
	 * @see gov.va.med.fw.util.parser.AbstractFileParserBuilder#build(java.io.BufferedReader)
	 */
	public synchronized final List build(
			DataFileProcessExecutionContext context,
			FileSystemResource fileSystemResource) throws BuilderException {
		List data = new ArrayList();
		List listenerData = null;
		if (context.hasFileParserListener())
			listenerData = new ArrayList();

		DataFile reader = DataFile.createReader(null);
		Object instance = null;
		reader.setDataFormat(inputDataFormat);
		String currentRowRawData = null;
		String nextRowRawData = null;
		try {
			reader.open(fileSystemResource.getFile());
			DataRow row = null;
			DataRow nextRow = null;
			ClassMapping currentClassMapping = classMapping;
			boolean isUsingSectionHeaders = false;
			while (shouldContinueParsing(context)) {
				try {
					if (nextRow != null) {
						row = nextRow;
						currentRowRawData = nextRowRawData;
						context.setCurrentRowRawData(currentRowRawData);
						nextRowRawData = null;
						nextRow = null;
					}
					else {
						row = reader.next();
						currentRowRawData = inputDataFormat.getInputRawData();
						context.setCurrentRowRawData(currentRowRawData);
					}

					if (row == null)
						break;

					instance = null;
					// if not blank line
					if (row.size() != 0) {
						// get first field and see if it is a section header
						String val = (String) row.iterator().next();
						ClassMapping targetClassMapping = (ClassMapping) classMappings
								.get(val);
						if (targetClassMapping != null) {
							// ok, there was a match on a section header, use
							// that ClassMapping and move to next row
							isUsingSectionHeaders = true;
							currentClassMapping = targetClassMapping;
							if (targetClassMapping.isSectionHeaderOnSeparateLine()) {
								row = reader.next();
								currentRowRawData = inputDataFormat.getInputRawData();
								context.setCurrentRowRawData(currentRowRawData);
								if (row == null)
									throw new IllegalStateException(
											"Missing data row in file on line below section header: "
													+ val);
								if (row.size() == 0)
									throw new IllegalStateException(
											"Blank line not allowed below section header: "
													+ val);
							}
							else {
								// wrapped lines are allowed when using section
								// headers at beginning of lines so peek at next
								// line(s)
								boolean keepPeeking = true;
								while (keepPeeking) {
									keepPeeking = false;
									nextRow = reader.next();

									if (nextRow != null) {
										if (nextRow.size() == 0)
											continue;

										String nextRowFirstVal = (String) nextRow
												.iterator().next();
										if (!classMappings.containsKey(nextRowFirstVal)) {
											// assume this is a wrapped line so
											// process nextRow with this row
											String combinedData = currentRowRawData
													+ inputDataFormat.getInputRawData();
											row = inputDataFormat.parseLine(combinedData);
											nextRow = null; // forces getting
											// another row when
											// logic needs it
											// combine raw data
											currentRowRawData = combinedData;
											context.setCurrentRowRawData(combinedData);
											keepPeeking = true;
										}
										else {
											nextRowRawData = inputDataFormat
													.getInputRawData();
										}
									}
								}
							}
						}

						if (currentClassMapping == null) {
							if (logger.isWarnEnabled())
								logger.warn("Unable to parse input data for row: "
										+ decorateRowRawData(currentRowRawData));

							if (failOnUnmappedData)
								throw new IllegalStateException(
										"Unable to determine ClassMapping for row with first value: "
												+ val);
						}

						instance = populateBean(currentClassMapping, row,
								isUsingSectionHeaders, currentRowRawData);
						if (context.hasFileParserListener()) {
							// peek at next row
							if (nextRow == null) {
								nextRow = reader.next();
								nextRowRawData = inputDataFormat.getInputRawData();
							}
							context.getFileParserListener().beanCreationSuccess(
									context, nextRowRawData);
						}
						if (logger.isDebugEnabled())
							logger
									.debug("FormattedFileParserBuilder populated object : "
											+ instance);
					}
				}
				catch (Exception e) {
					if (logger.isErrorEnabled())
						logger.error("Unable to create bean instance for line: "
								+ decorateRowRawData(currentRowRawData), e);
					if (context.hasFileParserListener()) {
						// peek at next row
						if (nextRow == null) {
							nextRow = reader.next();
							nextRowRawData = inputDataFormat.getInputRawData();
						}
						if (!context.getFileParserListener().beanCreationFailure(
								context, nextRowRawData, e))
							if (listenerData != null) {
								listenerData.clear();
							}
					}
					continue;
				}

				if (context.hasFileParserListener()) {
					// peek at next row
					if (nextRow == null) {
						nextRow = reader.next();
						nextRowRawData = inputDataFormat.getInputRawData();
					}

					if (instance != null && listenerData != null)
						listenerData.add(instance);
					if (context.getFileParserListener().acceptData(context,
							nextRowRawData, listenerData)) {
						context.getFileParserListener().processData(context,
								listenerData);
						if (listenerData != null) {
							listenerData.clear();
						}
					}
				}
				else {
					if (instance != null)
						data.add(instance);
				}
			}
			if (listenerData != null && context.hasFileParserListener() && !listenerData.isEmpty()) {
				context.getFileParserListener().processData(context, listenerData);
			}
		}
		catch (Exception e) {
			throw new BuilderException("Unable to parse the file "
					+ fileSystemResource.getFilename(), e);
		}
		finally {
			reader.close();
		}

		return data;
	}

	private String decorateRowRawData(String rowRawData) {
		String decorated = rowRawData;
		if (inputDataFormat instanceof DelimitedFormat) {
			StringBuffer buf = new StringBuffer();
			String delimiter = ((DelimitedFormat) inputDataFormat).getDelimiter();
			String[] elements = rowRawData.split("\\" + delimiter);
			for (int i = 0; i < elements.length; i++) {
				if (i == 0)
					buf.append(elements[i]);
				else
					buf.append("{").append(i).append("}").append(elements[i]);
				buf.append(delimiter);
			}
			decorated = buf.toString();
		}
		return decorated;
	}

	protected boolean shouldContinueParsing(
			DataFileProcessExecutionContext context) {
		boolean shouldContinue = true;
		if (context.hasFileParserListener())
			shouldContinue = !context.getFileParserListener().isInterrupted(context);
		return shouldContinue;
	}

	protected Object populateBean(ClassMapping classMapping, DataRow dataRow,
			boolean isUsingSectionHeaders, String currentRowRawData)
			throws Exception {
		Iterator itr = dataRow.iterator();
		String val = null;
		int index = 0;
		FileFieldMetaData metaData = null;
		OrderedMap data = new ListOrderedMap(); // must be ordered map
		boolean firstValue = true;
		Object transformedVal = null;
		while (itr.hasNext()) {
			transformedVal = null;
			val = (String) itr.next();
			if (isUsingSectionHeaders && firstValue
					&& !classMapping.isSectionHeaderOnSeparateLine()) {
				firstValue = false;
				continue;
			}
			firstValue = false;

			if (!classMapping.hasFieldsMetaData())
				break;

			if (classMapping.isExplicitFieldsMetaDataOrdering()) {
				metaData = classMapping.getFieldMetaData(Integer.toString(index));
			}
			else {

				if (classMapping.getFieldsMetaData().size() > index)
					metaData = (FileFieldMetaData) classMapping.getFieldsMetaData()
							.get(index);
			}

			if (metaData != null) {
				val = metaData.validate(val); // this will catch any missing
				// required fields
				if (StringUtils.isNotBlank(val)) {
					transformedVal = metaData.transform(val);

					if (transformedVal != null) {
						// Found translated value
						data.put(metaData.getName(), transformedVal);
					}
					else {
						// No Translation
						data.put(metaData.getName(), val);
					}
				}
				else {
					// No Translation
					data.put(metaData.getName(), val);
				}
			}
			else {
				// no meta data, just use as is
				data.put(Integer.toString(index), val);
			}

			index++;
		}
		Object instance = classMapping.getTargetClass().newInstance();
		RawFileDataContainerUtils.autoRegisterConvertersForClass(instance.getClass());
		if (instance instanceof RawFileDataContainer) {
			// check for other (ie, not reachable by the instance graph) classes to register (as instructed by this RawFileDataContainer)
			RawFileDataContainerUtils.autoRegisterConvertersForClasses(((RawFileDataContainer) instance).getClassesForAutoRegisterConverters());
			
			if (!data.isEmpty()) {
				// send the transformed data (recall could have transformed data in
				// above loop) to the RawFileDataContainer impl
				DataRow newDataRow = new DataRow();
				MapIterator itr2 = data.mapIterator(); // now you see why this had
				// to be an OrderedMap!
				while (itr2.hasNext()) {
					itr2.next(); // name, ignored
					newDataRow.add(itr2.getValue());
				}
				dataRow = newDataRow;
			}
			((RawFileDataContainer) instance).setRawFileData(currentRowRawData,
					dataRow, classMapping);
		}
		else if (!data.isEmpty()) {			
			BeanUtils.copyProperties(instance, data);
		}
		return instance;
	}

	public void afterPropertiesSet() throws Exception {
		super.afterPropertiesSet();
		if (classMapping == null && classMappings.isEmpty())
			throw new InvalidConfigurationException(
					"Must have at least one ClassMapping instance");
		if (classMapping != null && !classMappings.isEmpty())
			throw new InvalidConfigurationException(
					"For multiple ClassMapping's, configure only the classMappings property");

		int[] fieldWidths = null;
		boolean isFixedFieldFormatter = false;
		/*
		 * FixedWidthFormat is not supported when dealing with sectionHeaders. In
		 * other words, if there will be section headers, it is not possible to
		 * have fixed width format since each section can differ.
		 */
		Iterator itr = null;
		FileFieldMetaData metaData = null;
		Class type = null;
		Object instance = null;
		if (classMapping != null && classMapping.hasFieldsMetaData()) {
			itr = classMapping.getFieldsMetaData().iterator();
			instance = classMapping.getTargetClass().newInstance();
			fieldWidths = new int[classMapping.getFieldsMetaData().size()];
			int i = 0;
			while (itr.hasNext()) {
				metaData = (FileFieldMetaData) itr.next();
				if (metaData.getFixedFieldWidth() != 0)
					isFixedFieldFormatter = true;
				fieldWidths[i++] = metaData.getFixedFieldWidth();
				type = PropertyUtils.getPropertyType(instance, metaData.getName());
				// doing this allows for class properties that are not defined
				// to be
				// of type Lookup
				if (type != null && Lookup.class.isAssignableFrom(type))
					ConvertUtils.register(getLookupConverter(), type);
			}
			instance = null;
		}
		if (classMappings != null) {
			Iterator classMappingsItr = classMappings.values().iterator();
			ClassMapping cm = null;
			while (classMappingsItr.hasNext()) {
				cm = (ClassMapping) classMappingsItr.next();
				if (!cm.hasFieldsMetaData())
					continue;
				instance = cm.getTargetClass().newInstance();
				itr = cm.getFieldsMetaData().iterator();
				while (itr.hasNext()) {
					metaData = (FileFieldMetaData) itr.next();
					type = PropertyUtils.getPropertyType(instance, metaData
							.getName());
					// doing this allows for class properties that are not
					// defined to be
					// of type Lookup
					if (Lookup.class.isAssignableFrom(type))
						ConvertUtils.register(getLookupConverter(), type);
				}
			}
		}
		if (isFixedFieldFormatter)
			inputDataFormat = new FixedWidthFormat(fieldWidths);
		else if (!StringUtils.isEmpty(delimiter))
			inputDataFormat = new SimpleDelimiterFormat(delimiter);

		if (inputDataFormat == null)
			inputDataFormat = new SimpleDelimiterFormat(DEFAULT_DELIMITER);
		if (isFixedFieldFormatter && classMappings.size() > 0)
			throw new InvalidConfigurationException(
					"Unable to use a uniform (ie, the same) FixedWidthFormat across varying classes");
	}

	/**
	 * @return Returns the delimiter.
	 */
	public String getDelimiter() {
		return delimiter;
	}

	/**
	 * @param delimiter
	 *           The delimiter to set.
	 */
	public void setDelimiter(String delimiter) {
		this.delimiter = delimiter;
	}

	/**
	 * @return Returns the inputDataFormat.
	 */
	public InputDataFormat getInputDataFormat() {
		return inputDataFormat;
	}

	/**
	 * @param inputDataFormat
	 *           The inputDataFormat to set.
	 */
	public void setInputDataFormat(InputDataFormat inputDataFormat) {
		this.inputDataFormat = inputDataFormat;
	}

	/**
	 * @return Returns the classMapping.
	 */
	public ClassMapping getClassMapping() {
		return classMapping;
	}

	/**
	 * @param classMapping
	 *           The classMapping to set.
	 */
	public void setClassMapping(ClassMapping classMapping) {
		this.classMapping = classMapping;
	}

	/**
	 * @return Returns the classMappings.
	 */
	public Map getClassMappings() {
		return classMappings;
	}

	/**
	 * @param classMappings
	 *           The classMappings to set.
	 */
	public void setClassMappings(Map classMappings) {
		this.classMappings = classMappings;
	}

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

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

}