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

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

import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.PropertyUtils;
import org.apache.commons.collections.MapIterator;
import org.apache.commons.collections.OrderedMap;
import org.apache.commons.lang.Validate;
import org.springframework.core.io.FileSystemResource;

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

import gov.va.med.fw.io.ClassMapping;
import gov.va.med.fw.io.DataFormat;
import gov.va.med.fw.io.DelimitedFormat;
import gov.va.med.fw.io.FieldTransformer;
import gov.va.med.fw.io.FileFieldMetaData;
import gov.va.med.fw.io.FileNameAppender;
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.service.AbstractComponent;
import gov.va.med.fw.util.InvalidConfigurationException;
import gov.va.med.fw.util.StringUtils;

/**
 * Component that writes formatted data to a file.  If no DataFormat is specified,
 * default delimiter will be assumed.
 * 
 * <p>The same DataFormat applies to each row, regardless of ClassMapping.
 * 
 * <p>The current implementation internally uses SourceForge.net project DataFile.
 * A custom request was made to enhance that open-source project such that it be
 * configurable if an extra carriage return is at the end of the written file.  Until that
 * is implemented in the DataFile root tree, a custom workaround is implemented here
 * that should be tested with new releases of SourceForge.net DataFile.
 * See the finally block of the writeData method.
 * 
 * Created Feb 16, 2006 5:18:50 PM
 * 
 * @author DNS   BOHMEG
 */
public class FormattedFileWriter extends AbstractComponent {
	private static String DEFAULT_DELIMITER = "^";

	private String fileLocation;

	private String delimiter;
	private boolean extraCarriageReturnAfterEachDataWrite = true;
	private DataFormat dataFormat;
	private List classMappings;
	private Map classMappingsMap = new HashMap();
	private boolean failOnUnmappedInstances = true;
	private boolean shouldConvertLookups = true;
	private FileNameAppender fileNameAppender;
	private int maxLineSize;
	private DateFieldTransformer globalDateTransformer;
	private FieldTransformer globalTransformer;
	private String fileEncoding;
	
	private String bulletinToList;
	
	public synchronized boolean appendData(List data) {
		DataFile writer = DataFile.createWriter(fileEncoding, true);
		writer.setDataFormat(dataFormat);
		return writeData(writer, data, null);
	}

	public synchronized boolean appendData(List data, Object fileNameSuffixData) {
		DataFile writer = DataFile.createWriter(fileEncoding, true);
		writer.setDataFormat(dataFormat);
		return writeData(writer, data, fileNameSuffixData);
	}
	
	public synchronized boolean writeData(List data) {
		DataFile writer = DataFile.createWriter(fileEncoding, false);
		writer.setDataFormat(dataFormat);
		return writeData(writer, data, null);
	}
	
	public synchronized boolean writeData(List data, Object fileNameSuffixData) {
		DataFile writer = DataFile.createWriter(fileEncoding, false);
		writer.setDataFormat(dataFormat);
		return writeData(writer, data, fileNameSuffixData);
	}

	public void changeFileExtensions(String fromExtn, String toExtn) throws IOException{
        String directoryPath = getIVMFilePath();
        RawFileDataContainerUtils.changeFileExtensions(fromExtn, toExtn, directoryPath);
    }
	public String getIVMFilePath() {
        String filePath = fileLocation;
		if (fileLocation != null && !fileLocation.endsWith("/")){
			int index = fileLocation.lastIndexOf("/");
			//below line is to work out in Windows environment
			index = index== -1 ? fileLocation.lastIndexOf("\\") : index;
			filePath = fileLocation.substring(0, index+1);
		}
        return filePath;
    }
	
	private final boolean writeData(DataFile writer, List data, Object fileNameSuffixData) {
		if(data == null)
			return true;
		
		boolean success = false;
		FileSystemResource resource = null;
		DataRow row = null;
		try {
		    //Some of the processes need to append dynamic extensions to the file name 
		    //e.g <fileLocation>date.txt. Call the appender to get any such extension

			resource = getFileSystemResource(fileNameSuffixData);
			writer.open(resource.getFile());

			Iterator itr = data.iterator();
			Object obj = null;
			
			OutputFileFieldMetaData meta = null;
			Iterator fieldsItr = null;
			Object value = null;
			int currentLineSize = 0;
			int delimiterSize = (dataFormat instanceof DelimitedFormat) ? ((DelimitedFormat) dataFormat).getDelimiter().length() : 0; 
			
			// get the object to write....
			ClassMapping targetClassMapping = null;
			while(itr.hasNext()) {
				obj = itr.next();
				// get a Line in the file...
				row = writer.next();
				currentLineSize = 0;
				
				// write the properties of each object...								
				
				if(obj instanceof RawFileDataContainer) {
					OrderedMap rowData = ((RawFileDataContainer) obj).getRawFileData();
					MapIterator fieldDataItr = rowData != null ? rowData.mapIterator() : null;
					String elementName = null;
					Object elementValue = null;
					FileFieldMetaData metaForTransform = null;
					while(fieldDataItr != null && fieldDataItr.hasNext()) {
						elementName = (String) fieldDataItr.next();
						elementValue = fieldDataItr.getValue();
						targetClassMapping = getTargetClassMapping(obj, false);
						elementValue = applyGlobalTransforms(elementName, elementValue);
						// determine if any transforms listed
						if(targetClassMapping != null && targetClassMapping.hasFieldsMetaData()) {
							metaForTransform = targetClassMapping.getFieldMetaData(elementName);
							if(metaForTransform != null) {
								Object transformed = metaForTransform.transform(elementValue);
								elementValue = transformed != null ? transformed.toString() : null;
							}
						}
						currentLineSize = addDataToRow(writer, row, elementValue, currentLineSize, delimiterSize, fieldDataItr);
					}
					continue;
				}
				
				targetClassMapping = getTargetClassMapping(obj, failOnUnmappedInstances);
								
				if(targetClassMapping == null || !targetClassMapping.hasFieldsMetaData()) {
					// special case of just print toString()
					currentLineSize = addDataToRow(writer, row, obj, currentLineSize, delimiterSize, null);
					continue;
				}
				
				fieldsItr = targetClassMapping.getFieldsMetaData().iterator();
				while(fieldsItr.hasNext()) {
					meta = (OutputFileFieldMetaData) fieldsItr.next();
					
					if(meta.isSectionHeaderField()) {
						row.add(meta.getSectionHeader());
						if(targetClassMapping.isSectionHeaderOnSeparateLine())
							row = writer.next();
					} else {
						if(meta.isBeginNewLine()) {
							if(dataFormat instanceof DelimitedFormat)
								row.add(StringUtils.EMPTY);
							row = writer.next();
						}
						
						if(meta.isFillerField())
							value = meta.getFillerValue();
						else {
							try {
								value = PropertyUtils.getProperty(obj, meta.getName());
							} catch(NullPointerException e) {
								// couldn't dereference "a.b.c" notation, some object is null (this could be normal)
								value = StringUtils.EMPTY;
							}
							value = applyGlobalTransforms(meta.getName(), value);
							value = meta.transform(value);							
						}
						
						currentLineSize = addDataToRow(writer, row, value, currentLineSize, delimiterSize, fieldsItr);
					}
				}
			}
			success = true;
		} catch (Exception e) {
			throw new RuntimeException("Unable to write to " + resource.getFilename(), e);
		} finally {
			/* custom work around since this is not yet configurable within SourceForge.net project DataFile */
			if(extraCarriageReturnAfterEachDataWrite || row == null) {
				writer.close(); // internally does a println on its PrintWriter
			} else {
				PrintWriter writerOut = getPrintWriter(writer);
				try {
					writerOut.print(this.dataFormat.format(row)); // this is where it is different (use print vs. println)
				} catch(Exception e) {
					RuntimeException e2 = new IllegalStateException("Custom workaround for DataFile shortcoming not working....perhaps assumes different version");
					e2.initCause(e);
					throw e2;
				} finally {
					if(writerOut != null)
						writerOut.close();  // key difference: we do not close the DataFile, just the PrintWriter
				}
			}
		}
		return success;
	}
	
	private PrintWriter getPrintWriter(DataFile writer) {
		try {
			Field outField = writer.getClass().getDeclaredField("out");
			outField.setAccessible(true);	
			return (PrintWriter) outField.get(writer);
		} catch(Exception e) {
			RuntimeException e2 = new IllegalStateException("Unable to get PrintWriter member variable from DataFile instance");
			e2.initCause(e);
			throw e2;
		}
	}
	
	public void println(Object fileNameSuffixData) {
		DataFile writer = DataFile.createWriter(fileEncoding, true); // append mode
		FileSystemResource resource = getFileSystemResource(fileNameSuffixData);
		PrintWriter writerOut = null;
		try {
			writer.open(resource.getFile());		
			writerOut = getPrintWriter(writer);
			
			writerOut.println(); 
		} catch(Exception e) {
			RuntimeException e2 = new IllegalStateException("Unable to println to DataFile");
			e2.initCause(e);
			throw e2;
		} finally {
			if(writerOut != null)
				writerOut.close(); // key difference: we do not close the DataFile, just the PrintWriter
		}		
	}
	
	/**
	 * Useful for client needs to know what the current file name is.  Uses stateful data that is
	 * initialized via dependency injection (so not really maintaining state).
	 * 
	 * @param fileNameSuffixData
	 * @return
	 */
	public FileSystemResource getFileSystemResource(Object fileNameSuffixData) {
		String targetFileLocation = fileNameAppender == null ? fileLocation
                : fileLocation.concat(fileNameAppender
                        .getFileNameSuffix(fileNameSuffixData));
	            
		 return new FileSystemResource(targetFileLocation);		
	}
	
	
	private Object applyGlobalTransforms(String fieldName, Object data) throws Exception {
		Object result = data;
		if(data instanceof Lookup && shouldConvertLookups)
			result = ((Lookup) data).getCode();
		else if(data instanceof Date && globalDateTransformer != null)
			result = globalDateTransformer.transformData(fieldName, data);
		else if(globalTransformer != null)
			result = globalTransformer.transformData(fieldName, data);
		
		return result;
	}
	
	private int addDataToRow(DataFile writer, DataRow row, Object value, int currentLineSize, int delimiterSize, Iterator fieldItr) throws Exception {
		String currentStringValue = (value == null ? StringUtils.EMPTY : value.toString());
		if(maxLineSize != 0) {
			// check if need to wrap line
			currentLineSize = currentLineSize + currentStringValue.length() + delimiterSize;
			// currently line wrapping does not apply to first element on line
			//Fix for CR_7676
			//Here if condition is added, When the element length reaches the maxLineSize and the next element is the last element and
			// if it is a null then do not get the next line in the file.
			if(row.size() != 0 && currentLineSize > maxLineSize) {
                if(!(currentStringValue.length()==0 && fieldItr!=null && !fieldItr.hasNext())){
                    row.add(StringUtils.EMPTY);
                    // get next Line in the file...
                    row = writer.next();
                    currentLineSize = currentStringValue.length() + delimiterSize;
                }
            }
		}
		row.add(currentStringValue);
		return currentLineSize;
	}
	
	private ClassMapping getTargetClassMapping(Object obj, boolean shouldFailIfNotFound) {
		ClassMapping cm = (ClassMapping) classMappingsMap.get(obj.getClass());
		if(cm == null) {
			// allow for subclasses and interface implementations
			Iterator itr = classMappingsMap.keySet().iterator();
			Class clazz = null;
			while(itr.hasNext()) {
				clazz = (Class) itr.next();
				if(clazz.isAssignableFrom(obj.getClass()))
					return (ClassMapping) classMappingsMap.get(clazz);
			}
		}
		
		if(cm == null) {
			if(shouldFailIfNotFound)
				throw new IllegalStateException("Unable to find ClassMapping for: " + obj.getClass());
		}
		return cm;
	}

	/**
	 * @return Returns the dataFormat.
	 */
	public DataFormat getDataFormat() {
		return dataFormat;
	}

	/**
	 * @param dataFormat
	 *            The dataFormat to set.
	 */
	public void setDataFormat(DataFormat dataFormat) {
		this.dataFormat = dataFormat;
	}

	/**
	 * @return Returns the fileLocation.
	 */
	public String getFileLocation() {
		return fileLocation;
	}

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

	/**
	 * @return Returns the fileEncoding.
	 */
	public String getFileEncoding() {
		return fileEncoding;
	}

	/**
	 * @param fileEncoding
	 *            The fileEncoding to set.
	 */
	public void setFileEncoding(String fileEncoding) {
		//fileEncoding is set as empty string then use it null instead to use default file encoding
		if (!StringUtils.isEmpty(fileEncoding)) {
			this.fileEncoding = fileEncoding;
		} else {		
			this.fileEncoding = null;
		}
	}
	
	public void afterPropertiesSet() {		
		Validate.notNull(fileLocation, "fileLocation is needed");
				
		/* FixedWidthFormat is not supported when dealing with different object types
		 * since each object widths will differ
		 */
		int[] fieldWidths = null;
		boolean isFixedFieldFormatter = false;
		Iterator itr = null;
		ClassMapping classMapping = null;
		if(classMappings != null && classMappings.size() == 1) {
			classMapping = (ClassMapping) classMappings.get(0);
			itr = classMapping.getFieldsMetaData().iterator();
			OutputFileFieldMetaData metaData = null;
			fieldWidths = new int[classMapping.getFieldsMetaData().size()];
			int i = 0;			
			while (itr.hasNext()) {
				metaData = (OutputFileFieldMetaData) itr.next();
				if (metaData.getFixedFieldWidth() != 0)
					isFixedFieldFormatter = true;
				fieldWidths[i++] = metaData.getFixedFieldWidth();
			}
		}
		
		if (isFixedFieldFormatter)
			dataFormat = new FixedWidthFormat(fieldWidths);
		else if (!StringUtils.isEmpty(delimiter))
			dataFormat = new SimpleDelimiterFormat(delimiter);
		
		if(dataFormat == null)
			dataFormat = new SimpleDelimiterFormat(DEFAULT_DELIMITER);		
		
		// iterate through classMappings and create bookkeeping
		if(classMappings != null) {
			itr = classMappings.iterator();
			while(itr.hasNext()) {
				classMapping = (ClassMapping) itr.next();
				classMappingsMap.put(classMapping.getTargetClass(), classMapping);
			}
		}
		
		// add default ClassMapping for String
		if(!classMappingsMap.containsKey(String.class)) {
			classMappingsMap.put(String.class, new ClassMapping());
		}
		
		if(!(dataFormat instanceof DelimitedFormat) && maxLineSize != 0) {
			throw new InvalidConfigurationException("Line wrapping is currently only available for instances of DelimitedFormat.");
		}
	}

	/**
	 * @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 classMappings.
	 */
	public List getClassMappings() {
		return classMappings;
	}

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

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

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

    /**
     * @return Returns the fileNameAppender.
     */
    public FileNameAppender getFileNameAppender()
    {
        return fileNameAppender;
    }
    /**
     * @param fileNameAppender The fileNameAppender to set.
     */
    public void setFileNameAppender(FileNameAppender fileNameAppender)
    {
        this.fileNameAppender = fileNameAppender;
    }

	/**
	 * @return Returns the maxLineSize.
	 */
	public int getMaxLineSize() {
		return maxLineSize;
	}

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

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

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

	/**
	 * @return Returns the globalDateTransformer.
	 */
	public DateFieldTransformer getGlobalDateTransformer() {
		return globalDateTransformer;
	}

	/**
	 * @param globalDateTransformer The globalDateTransformer to set.
	 */
	public void setGlobalDateTransformer(DateFieldTransformer globalDateTransformer) {
		this.globalDateTransformer = globalDateTransformer;
	}

	/**
	 * @return Returns the globalTransformer.
	 */
	public FieldTransformer getGlobalTransformer() {
		return globalTransformer;
	}

	/**
	 * @param globalTransformer The globalTransformer to set.
	 */
	public void setGlobalTransformer(FieldTransformer globalTransformer) {
		this.globalTransformer = globalTransformer;
	}

	/**
	 * @return the extraCarriageReturnAfterEachDataWrite
	 */
	public boolean isExtraCarriageReturnAfterEachDataWrite() {
		return extraCarriageReturnAfterEachDataWrite;
	}

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

	public String getBulletinToList() {
		return bulletinToList;
	}

	public void setBulletinToList(String bulletinToList) {
		this.bulletinToList = bulletinToList;
	}
}
